chore(deps): 升级mp-html为最新版2.5.2

This commit is contained in:
2025-12-30 15:57:30 +08:00
parent 8f0a13c473
commit a5740f53af
7 changed files with 3143 additions and 2749 deletions

View File

@@ -1,96 +1,144 @@
## 为减小组件包的大小默认组件包中不包含编辑、latex 公式等扩展功能,需要使用扩展功能的请参考下方的 插件扩展 栏的说明 # mp-html
> 一个强大的小程序富文本组件
![star](https://img.shields.io/github/stars/jin-yufeng/mp-html)
![forks](https://img.shields.io/github/forks/jin-yufeng/mp-html)
[![npm](https://img.shields.io/npm/v/mp-html)](https://www.npmjs.com/package/mp-html)
![downloads](https://img.shields.io/npm/dt/mp-html)
[![Coverage Status](https://coveralls.io/repos/github/jin-yufeng/mp-html/badge.svg?branch=master)](https://coveralls.io/github/jin-yufeng/mp-html?branch=master)
![license](https://img.shields.io/github/license/jin-yufeng/mp-html)
[![JavaScript Style Guide](https://img.shields.io/badge/code_style-standard-brightgreen.svg)](https://standardjs.com)
## 功能介绍 ## 功能介绍
- 全端支持(含 `v3、NVUE` - 支持在多个主流的小程序平台和 `uni-app` 中使用
- 支持丰富的标签(包括 `table``video``svg` 等) - 支持丰富的标签(包括 `table``video``svg` 等)
- 支持丰富的事件效果(自动预览图片、链接处理等) - 支持丰富的事件效果(自动预览图片、链接处理等)
- 支持设置占位图(加载中、出错时、预览时) - 支持设置占位图(加载中、出错时、预览时)
- 支持锚点跳转、长按复制等丰富功能 - 支持锚点跳转、长按复制等丰富功能
- 支持大部分 *html* 实体 - 支持大部分 *html* 实体
- 丰富的插件(关键词搜索、内容编辑、`latex` 公式等) - 丰富的插件(关键词搜索、内容编辑、`latex` 公式等)
- 效率高、容错性强且轻量化 - 效率高、容错性强且轻量化`≈25KB``9KB gzipped`
查看 [功能介绍](https://jin-yufeng.gitee.io/mp-html/#/overview/feature) 了解更多 查看 [功能介绍](https://jin-yufeng.github.io/mp-html/#/overview/feature) 了解更多
## 使用方法 ## 使用方法
- `uni_modules` 方式 ### 原生平台
1. 点击右上角的 `使用 HBuilder X 导入插件` 按钮直接导入项目或点击 `下载插件 ZIP` 按钮下载插件包并解压到项目的 `uni_modules/mp-html` 目录下 - `npm` 方式
2.需要使用页面的 `(n)vue` 文件中添加 1.项目目录下安装组件包
```html
<!-- 不需要引入,可直接使用 -->
<mp-html :content="html" />
```
```javascript
export default {
data() {
return {
html: '<div>Hello World!</div>'
}
}
}
```
3. 需要更新版本时在 `HBuilder X` 中右键 `uni_modules/mp-html` 目录选择 `从插件市场更新` 即可
- 源码方式
1. 从 [github](https://github.com/jin-yufeng/mp-html/tree/master/dist/uni-app) 或 [gitee](https://gitee.com/jin-yufeng/mp-html/tree/master/dist/uni-app) 下载源码
插件市场的 **非 uni_modules 版本** 无法更新,不建议从插件市场获取
2. 在需要使用页面的 `(n)vue` 文件中添加
```html
<mp-html :content="html" />
```
```javascript
import mpHtml from '@/components/mp-html/mp-html'
export default {
// HBuilderX 2.5.5+ 可以通过 easycom 自动引入
components: {
mpHtml
},
data() {
return {
html: '<div>Hello World!</div>'
}
}
}
```
- npm 方式
1. 在项目根目录下执行
```bash ```bash
npm install mp-html npm install mp-html
``` ```
2. 在需要使用页面的 `(n)vue` 文件中添加 2. 开发者工具中勾选 `使用 npm 模块`(若没有此选项则不需要)并点击 `工具 - 构建 npm`
```html 3. 在需要使用页面的 `json` 文件中添加
<mp-html :content="html" />
``` ```json
```javascript {
import mpHtml from 'mp-html/dist/uni-app/components/mp-html/mp-html' "usingComponents": {
export default { "mp-html": "mp-html"
// 不可省略
components: {
mpHtml
},
data() {
return {
html: '<div>Hello World!</div>'
}
} }
} }
``` ```
3. 需要更新版本时执行以下命令即可 4. 需要使用页面的 `wxml` 文件中添加
```bash
npm update mp-html ```html
<mp-html content="{{html}}" />
```
5. 在需要使用页面的 `js` 文件中添加
```javascript
Page({
onLoad () {
this.setData({
html: '<div>Hello World!</div>'
})
}
})
```
- 源码方式
1. 将源码中对应平台的代码包(`dist/platform`)拷贝到 `components` 目录下,更名为 `mp-html`
2. 在需要使用页面的 `json` 文件中添加
```json
{
"usingComponents": {
"mp-html": "/components/mp-html/index"
}
}
``` ```
使用 *cli* 方式运行的项目,通过 *npm* 方式引入时,需要在 *vue.config.js* 中配置 *transpileDependencies*,详情可见 [#330](https://github.com/jin-yufeng/mp-html/issues/330#issuecomment-913617687) 后续步骤同上
如果在 **nvue** 中使用还要将 `dist/uni-app/static` 目录下的内容拷贝到项目的 `static` 目录下,否则无法运行
查看 [快速开始](https://jin-yufeng.gitee.io/mp-html/#/overview/quickstart) 了解更多 查看 [快速开始](https://jin-yufeng.github.io/mp-html/#/overview/quickstart) 了解更多
### uni-app
- 源码方式
1. 将源码中 `dist/uni-app` 内的内容拷贝到项目根目录下
可以直接通过 [插件市场](https://ext.dcloud.net.cn/plugin?id=805) 引入
2. 在需要使用页面的 `vue` 文件中添加
```vue
<template>
<view>
<mp-html :content="html" />
</view>
</template>
<script>
import mpHtml from '@/components/mp-html/mp-html'
export default {
// HBuilderX 2.5.5+ 可以通过 easycom 自动引入
components: {
mpHtml
},
data () {
return {
html: '<div>Hello World!</div>'
}
}
}
</script>
```
- `npm` 方式
1. 在项目目录下安装组件包
```bash
npm install mp-html
```
2. 在需要使用页面的 `vue` 文件中添加
```vue
<template>
<view>
<mp-html :content="html" />
</view>
</template>
<script>
import mpHtml from 'mp-html/dist/uni-app/components/mp-html/mp-html'
export default {
// 不可省略
components: {
mpHtml
},
data () {
return {
html: '<div>Hello World!</div>'
}
}
}
</script>
```
使用 `cli` 方式运行的项目,通过 `npm` 方式引入时,需要在 `vue.config.js` 中配置 `transpileDependencies`,详情可见 [#330](https://github.com/jin-yufeng/mp-html/issues/330#issuecomment-913617687)
如果在 `nvue` 中使用还要将 `dist/uni-app/static` 目录下的内容拷贝到项目的 `static` 目录下,否则无法运行
查看 [快速开始](https://jin-yufeng.github.io/mp-html/#/overview/quickstart) 了解更多
## 组件属性 ## 组件属性
| 属性 | 类型 | 默认值 | 说明 | | 属性 | 类型 | 默认值 | 说明 |
|:---:|:---:|:---:|---| |:---:|:---:|:---:|---|
| container-style | String | | 容器的样式([2.1.0+](https://jin-yufeng.gitee.io/mp-html/#/changelog/changelog#v210) | | container-style | String | | 容器的样式([2.1.0+](https://jin-yufeng.github.io/mp-html/#/changelog/changelog#v210) |
| content | String | | 用于渲染的 html 字符串 | | content | String | | 用于渲染的 html 字符串 |
| copy-link | Boolean | true | 是否允许外部链接被点击时自动复制 | | copy-link | Boolean | true | 是否允许外部链接被点击时自动复制 |
| domain | String | | 主域名(用于链接拼接) | | domain | String | | 主域名(用于链接拼接) |
@@ -106,7 +154,7 @@
| tag-style | Object | | 设置标签的默认样式 | | tag-style | Object | | 设置标签的默认样式 |
| use-anchor | Boolean | false | 是否使用锚点链接 | | use-anchor | Boolean | false | 是否使用锚点链接 |
查看 [属性](https://jin-yufeng.gitee.io/mp-html/#/basic/prop) 了解更多 查看 [属性](https://jin-yufeng.github.io/mp-html/#/basic/prop) 了解更多
## 组件事件 ## 组件事件
@@ -117,9 +165,11 @@
| error | 发生渲染错误时 | | error | 发生渲染错误时 |
| imgtap | 图片被点击时 | | imgtap | 图片被点击时 |
| linktap | 链接被点击时 | | linktap | 链接被点击时 |
| play | 音视频播放时 | | play | 音视频播放时[2.3.0+](https://jin-yufeng.github.io/mp-html/#/changelog/changelog#v230) |
| pause | 音视频暂停时([2.5.2+](https://jin-yufeng.github.io/mp-html/#/changelog/changelog#v252) |
| fullscreenchange | 视频全屏变化时([2.5.2+](https://jin-yufeng.github.io/mp-html/#/changelog/changelog#v252) |
查看 [事件](https://jin-yufeng.gitee.io/mp-html/#/basic/event) 了解更多 查看 [事件](https://jin-yufeng.github.io/mp-html/#/basic/event) 了解更多
## api ## api
组件实例上提供了一些 `api` 方法可供调用 组件实例上提供了一些 `api` 方法可供调用
@@ -132,10 +182,10 @@
| getRect | 获取富文本内容的位置和大小 | | getRect | 获取富文本内容的位置和大小 |
| setContent | 设置富文本内容 | | setContent | 设置富文本内容 |
| imgList | 获取所有图片的数组 | | imgList | 获取所有图片的数组 |
| pauseMedia | 暂停播放音视频([2.2.2+](https://jin-yufeng.gitee.io/mp-html/#/changelog/changelog#v222) | | pauseMedia | 暂停播放音视频([2.2.2+](https://jin-yufeng.github.io/mp-html/#/changelog/changelog#v222) |
| setPlaybackRate | 设置音视频播放速率([2.4.0+](https://jin-yufeng.gitee.io/mp-html/#/changelog/changelog#v240) | | setPlaybackRate | 设置音视频播放速率([2.4.0+](https://jin-yufeng.github.io/mp-html/#/changelog/changelog#v240) |
查看 [api](https://jin-yufeng.gitee.io/mp-html/#/advanced/api) 了解更多 查看 [api](https://jin-yufeng.github.io/mp-html/#/advanced/api) 了解更多
## 插件扩展 ## 插件扩展
除基本功能外,本组件还提供了丰富的扩展,可按照需要选用 除基本功能外,本组件还提供了丰富的扩展,可按照需要选用
@@ -143,7 +193,7 @@
| 名称 | 作用 | | 名称 | 作用 |
|:---:|---| |:---:|---|
| audio | 音乐播放器 | | audio | 音乐播放器 |
| editable | 富文本 **编辑**[示例项目](https://mp-html.oss-cn-hangzhou.aliyuncs.com/editable.zip) | | editable | 富文本编辑 |
| emoji | 解析 emoji | | emoji | 解析 emoji |
| highlight | 代码块高亮显示 | | highlight | 代码块高亮显示 |
| markdown | 渲染 markdown | | markdown | 渲染 markdown |
@@ -152,43 +202,58 @@
| txv-video | 使用腾讯视频 | | txv-video | 使用腾讯视频 |
| img-cache | 图片缓存 by [@PentaTea](https://github.com/PentaTea) | | img-cache | 图片缓存 by [@PentaTea](https://github.com/PentaTea) |
| latex | 渲染 latex 公式 by [@Zeng-J](https://github.com/Zeng-J) | | latex | 渲染 latex 公式 by [@Zeng-J](https://github.com/Zeng-J) |
| card | 卡片展示 by [@whoooami](https://github.com/whoooami) |
从插件市场导入的包中 **不含有** 扩展插件,使用插件需通过微信小程序 `富文本插件` 获取或参考以下方法进行打包: 查看 [插件](https://jin-yufeng.github.io/mp-html/#/advanced/plugin) 了解更多
1. 获取完整组件包
```bash
npm install mp-html
```
2. 编辑 `tools/config.js` 中的 `plugins` 项,选择需要的插件
3. 生成新的组件包
在 `node_modules/mp-html` 目录下执行
```bash
npm install
npm run build:uni-app
```
4. 拷贝 `dist/uni-app` 中的内容到项目根目录
查看 [插件](https://jin-yufeng.gitee.io/mp-html/#/advanced/plugin) 了解更多 ## 使用案例
## 关于 nvue | [官方示例](https://github.com/jin-yufeng/mp-html-demo) | 欢喜商城 | 多么生活 | 食法查 | 微慕 | 科学复习 |
`nvue` 使用原生渲染,不支持部分 `css` 样式,为实现和 `html` 相同的效果,组件内部通过 `web-view` 进行渲染,性能上差于原生,根据 `weex` 官方建议,`web` 标签仅应用在非常规的降级场景。因此,如果通过原生的方式(如 `richtext`)能够满足需要,则不建议使用本组件,如果有较多的富文本内容,则可以直接使用 `vue` 页面 |:---:|:---:|:---:|:---:|:---:|:---:|
由于渲染方式与其他端不同,有以下限制: | ![富文本插件](docs/assets/case/富文本插件.jpg) | ![欢喜商城](docs/assets/case/欢喜商城.png) | ![多么生活](docs/assets/case/多么生活.jpg) | ![食法查](docs/assets/case/食法查.png) | ![微慕](docs/assets/case/微慕.jpg) | ![科学复习](docs/assets/case/科学复习.png) |
1. 不支持 `lazy-load` 属性
2. 视频不支持全屏播放
3. 如果在 `flex-direction: row` 的容器中使用,需要给组件设置宽度或设置 `flex: 1` 占满剩余宽度
纯 `nvue` 模式下,[此问题](https://ask.dcloud.net.cn/question/119678) 修复前,不支持通过 `uni_modules` 引入,需要本地引入(将 [dist/uni-app](https://github.com/jin-yufeng/mp-html/tree/master/dist/uni-app) 中的内容拷贝到项目根目录下) | [程序员技术之旅](https://github.com/fendoudebb/z-blog-wx) | 典典博客 | 优秀笔记 | 同城共享书 | [技术源 share](https://github.com/wangsrGit119/mini-blog-halo) | 你的代码写的真棒 |
|:---:|:---:|:---:|:---:|:---:|:---:|
| ![程序员技术之旅](docs/assets/case/程序员技术之旅.jpg) | ![典典博客](docs/assets/case/典典博客.jpg) | ![优秀笔记](docs/assets/case/优秀笔记.jpg) | ![同城共享书](docs/assets/case/同城共享书.jpg) | ![技术源share](docs/assets/case/技术源share.jpg) | ![你的代码写的真棒](docs/assets/case/你的代码写的真棒.jpg) |
## 立即体验 | 谛否 | 小莫唐尼 | [模版演示](https://github.com/zhihuifanqiechaodan/miniprogram-template) | AI瓦力 | 豆流便签 | 前端八股通 |
![富文本插件](https://mp-html.oss-cn-hangzhou.aliyuncs.com/qrcode.jpg) |:---:|:---:|:---:|:---:|:---:|:---:|
| ![谛否](docs/assets/case/谛否.jpg) | ![小莫唐尼](docs/assets/case/小莫唐尼.png) | ![MiniProgram模版演示](docs/assets/case/MiniProgram模版演示.jpg) | ![AI瓦力](docs/assets/case/AI瓦力.jpg) | ![豆流便签](docs/assets/case/豆流便签.jpg) | ![前端八股通](docs/assets/case/前端八股通.jpg) |
## 问题反馈 以上排名不分先后,更多可见 [使用案例收集](https://github.com/jin-yufeng/mp-html/issues/27)(欢迎添加)
遇到问题时,请先查阅 [常见问题](https://jin-yufeng.gitee.io/mp-html/#/question/faq) 和 [issue](https://github.com/jin-yufeng/mp-html/issues) 中是否已有相同的问题
可通过 [issue](https://github.com/jin-yufeng/mp-html/issues/new/choose) 、插件问答或发送邮件到 [mp_html@126.com](mailto:mp_html@126.com) 提问,不建议在评论区提问(不方便回复)
提问请严格按照 [issue 模板](https://github.com/jin-yufeng/mp-html/issues/new/choose) ,描述清楚使用环境、`html` 内容或可复现的 `demo` 项目以及复现方式,对于 **描述不清**、**无法复现** 或重复的问题将不予回复
欢迎加入 `QQ` 交流群: ## 许可与支持
群1已满`699734691` - 许可
群2已满`778239129` 您可以免费的使用(包括商用)、复制或修改本组件 [MIT License](https://github.com/jin-yufeng/mp-html/blob/master/LICENSE)
群3`960265313` 在用于生产环境前务必经过充分测试,由插件 `bug` 带来的损失概不负责(可以自行修改源码)
查看 [问题反馈](https://jin-yufeng.gitee.io/mp-html/#/question/feedback) 了解更多 - 联系
欢迎加入 `QQ` 交流群:
群1已满`699734691`
群2已满`778239129`
群3`960265313`
![group](docs/assets/group.jpg)
- 支持
![支持](docs/assets/sponsor.png)
## 更新日志
- v2.5.2 (20251214)
1. `A` 增加了音视频暂停 [pause](https://jin-yufeng.github.io/mp-html/#/basic/event?id=pause) 和视频全屏 [fullscreenchange](https://jin-yufeng.github.io/mp-html/#/basic/event?id=fullscreenchange) 事件 [#495](https://github.com/jin-yufeng/mp-html/issues/495) [#595](https://github.com/jin-yufeng/mp-html/issues/595)
2. `U` 优化了 [流式输出](https://jin-yufeng.github.io/mp-html/#/overview/feature?id=stream) 效果,通过差量更新解决闪烁问题 [详细](https://github.com/jin-yufeng/mp-html/issues/657)
3. `U` `latex` 插件更新字体文件 [详细](https://github.com/jin-yufeng/mp-html/pull/647) by [@JiuyeXD](https://github.com/JiuyeXD)
4. `U` 更新 `markdown` 插件中 `marked.js` 版本 [详细](https://github.com/jin-yufeng/mp-html/issues/672)
5. `U` 微信小程序替换遗漏的废弃 `api` `getSystemInfoSync` [详细](https://github.com/jin-yufeng/mp-html/pull/653) by [@zcSkr](https://github.com/zcSkr)
6. `F` 修复了 `markdown` 插件加粗文本遇到中文符号无效的问题 [详细](https://github.com/jin-yufeng/mp-html/pull/664) by [@qp666](https://github.com/qp666)
- v2.5.1 (20250420)
1. `U` `uni-app` 包适配鸿蒙 `APP` [详细](https://github.com/jin-yufeng/mp-html/issues/615)
2. `U` 微信小程序替换废弃 `api` `getSystemInfoSync` [详细](https://github.com/jin-yufeng/mp-html/issues/613)
3. `F` 修复了微信小程序 `glass-easel` 框架下真机换行异常的问题 [详细](https://github.com/jin-yufeng/mp-html/pull/607) by [@PaperStrike](https://github.com/PaperStrike)
4. `F` 修复了 `uni-app` 包 `app` 端播放视频可能报错的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/617)
5. `F` 修复了 `latex` 插件可能出现 `xxx can be used only in display mode` 的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/632)
6. `F` 修复了 `uni-app` 包 `latex` 公式可能不显示的问题 [#599](https://github.com/jin-yufeng/mp-html/issues/599)、[#627](https://github.com/jin-yufeng/mp-html/issues/627)
从 `1.x` 的升级方法可见 [更新指南](https://jin-yufeng.github.io/mp-html/#/changelog/changelog?id=v200)
查看 [更新日志](https://jin-yufeng.github.io/mp-html/#/changelog/changelog) 了解更多

View File

@@ -5,14 +5,14 @@
<node v-else :childs="nodes" :opts="[lazyLoad,loadingImg,errorImg,showImgMenu,selectable]" name="span" /> <node v-else :childs="nodes" :opts="[lazyLoad,loadingImg,errorImg,showImgMenu,selectable]" name="span" />
<!-- #endif --> <!-- #endif -->
<!-- #ifdef APP-PLUS-NVUE --> <!-- #ifdef APP-PLUS-NVUE -->
<web-view ref="web" src="/uni_modules/mp-html/static/app-plus/mp-html/local.html" :style="'margin-top:-2px;height:' + height + 'px'" @onPostMessage="_onMessage" /> <web-view ref="web" src="/static/app-plus/mp-html/local.html" :style="'margin-top:-2px;height:' + height + 'px'" @onPostMessage="_onMessage" />
<!-- #endif --> <!-- #endif -->
</view> </view>
</template> </template>
<script> <script>
/** /**
* mp-html v2.5.0 * mp-html v2.5.2
* @description 富文本组件 * @description 富文本组件
* @tutorial https://github.com/jin-yufeng/mp-html * @tutorial https://github.com/jin-yufeng/mp-html
* @property {String} container-style 容器的样式 * @property {String} container-style 容器的样式
@@ -36,12 +36,14 @@
* @event {Function} linktap 链接被点击时触发 * @event {Function} linktap 链接被点击时触发
* @event {Function} play 音视频播放时触发 * @event {Function} play 音视频播放时触发
* @event {Function} error 媒体加载出错时触发 * @event {Function} error 媒体加载出错时触发
* @event {Function} pause 音视频暂停时触发
* @event {Function} fullscreenchange 视频全屏状态变化时触发
*/ */
// #ifndef APP-PLUS-NVUE // #ifndef APP-PLUS-NVUE
import node from './node/node' import node from './node/node'
// #endif // #endif
import Parser from './parser' import Parser from './parser'
const plugins=[] const plugins = []
// #ifdef APP-PLUS-NVUE // #ifdef APP-PLUS-NVUE
const dom = weex.requireModule('dom') const dom = weex.requireModule('dom')
// #endif // #endif

View File

@@ -1,6 +1,6 @@
<template> <template>
<view :id="attrs.id" :class="'_block _'+name+' '+attrs.class" :style="attrs.style"> <view :id="attrs.id" :class="'_block _'+name+' '+attrs.class" :style="attrs.style">
<block v-for="(n, i) in childs" v-bind:key="i"> <block v-for="(n, i) in nodes" v-bind:key="i">
<!-- 图片 --> <!-- 图片 -->
<!-- 占位图 --> <!-- 占位图 -->
<image v-if="n.name==='img'&&!n.t&&((opts[1]&&!ctrl[i])||ctrl[i]<0)" class="_img" :style="n.attrs.style" :src="ctrl[i]<0?opts[2]:opts[1]" mode="widthFix" /> <image v-if="n.name==='img'&&!n.t&&((opts[1]&&!ctrl[i])||ctrl[i]<0)" class="_img" :style="n.attrs.style" :src="ctrl[i]<0?opts[2]:opts[1]" mode="widthFix" />
@@ -12,6 +12,9 @@
<!-- 表格中的图片使用 rich-text 防止大小不正确 --> <!-- 表格中的图片使用 rich-text 防止大小不正确 -->
<rich-text v-if="n.name==='img'&&n.t" :style="'display:'+n.t" :nodes="[{attrs:{style:n.attrs.style||'',src:n.attrs.src},name:'img'}]" :data-i="i" @tap.stop="imgTap" /> <rich-text v-if="n.name==='img'&&n.t" :style="'display:'+n.t" :nodes="[{attrs:{style:n.attrs.style||'',src:n.attrs.src},name:'img'}]" :data-i="i" @tap.stop="imgTap" />
<!-- #endif --> <!-- #endif -->
<!-- #ifdef APP-HARMONY -->
<image v-else-if="n.name==='img'" :id="n.attrs.id" :class="'_img '+n.attrs.class" :style="(ctrl[i]===-1?'display:none;':'')+'width:'+ctrl[i]+'px;'+n.attrs.style" :src="n.attrs.src||(ctrl.load?n.attrs['data-src']:'')" :mode="!n.h?'widthFix':(!n.w?'heightFix':(n.m||'scaleToFill'))" :data-i="i" @load="imgLoad" @error="mediaError" @tap.stop="imgTap" @longpress="imgLongTap" />
<!-- #endif -->
<!-- #ifndef H5 || APP-PLUS || MP-KUAISHOU --> <!-- #ifndef H5 || APP-PLUS || MP-KUAISHOU -->
<image v-else-if="n.name==='img'" :id="n.attrs.id" :class="'_img '+n.attrs.class" :style="(ctrl[i]===-1?'display:none;':'')+'width:'+(ctrl[i]||1)+'px;height:1px;'+n.attrs.style" :src="n.attrs.src" :mode="!n.h?'widthFix':(!n.w?'heightFix':(n.m||'scaleToFill'))" :lazy-load="opts[0]" :webp="n.webp" :show-menu-by-longpress="opts[3]&&!n.attrs.ignore" :image-menu-prevent="!opts[3]||n.attrs.ignore" :data-i="i" @load="imgLoad" @error="mediaError" @tap.stop="imgTap" @longpress="imgLongTap" /> <image v-else-if="n.name==='img'" :id="n.attrs.id" :class="'_img '+n.attrs.class" :style="(ctrl[i]===-1?'display:none;':'')+'width:'+(ctrl[i]||1)+'px;height:1px;'+n.attrs.style" :src="n.attrs.src" :mode="!n.h?'widthFix':(!n.w?'heightFix':(n.m||'scaleToFill'))" :lazy-load="opts[0]" :webp="n.webp" :show-menu-by-longpress="opts[3]&&!n.attrs.ignore" :image-menu-prevent="!opts[3]||n.attrs.ignore" :data-i="i" @load="imgLoad" @error="mediaError" @tap.stop="imgTap" @longpress="imgLongTap" />
<!-- #endif --> <!-- #endif -->
@@ -28,17 +31,17 @@
<!-- #ifndef MP-WEIXIN || MP-BAIDU || MP-ALIPAY || MP-TOUTIAO --> <!-- #ifndef MP-WEIXIN || MP-BAIDU || MP-ALIPAY || MP-TOUTIAO -->
<text v-else-if="n.text" decode>{{n.text}}</text> <text v-else-if="n.text" decode>{{n.text}}</text>
<!-- #endif --> <!-- #endif -->
<text v-else-if="n.name==='br'">\n</text> <text v-else-if="n.name==='br'">{{'\n'}}</text>
<!-- 链接 --> <!-- 链接 -->
<view v-else-if="n.name==='a'" :id="n.attrs.id" :class="(n.attrs.href?'_a ':'')+n.attrs.class" hover-class="_hover" :style="'display:inline;'+n.attrs.style" :data-i="i" @tap.stop="linkTap"> <view v-else-if="n.name==='a'" :id="n.attrs.id" :class="(n.attrs.href?'_a ':'')+n.attrs.class" hover-class="_hover" :style="'display:inline;'+n.attrs.style" :data-i="i" @tap.stop="linkTap">
<node name="span" :childs="n.children" :opts="opts" style="display:inherit" /> <node name="span" :childs="n.children" :opts="opts" style="display:inherit" />
</view> </view>
<!-- 视频 --> <!-- 视频 -->
<!-- #ifdef APP-PLUS --> <!-- #ifdef APP-PLUS -->
<view v-else-if="n.html" :id="n.attrs.id" :class="'_video '+n.attrs.class" :style="n.attrs.style" v-html="n.html" @vplay.stop="play" /> <view v-else-if="n.html" :id="n.attrs.id" :class="'_video '+n.attrs.class" :style="n.attrs.style" v-html="n.html" :data-i="i" @vplay.stop="play" />
<!-- #endif --> <!-- #endif -->
<!-- #ifndef APP-PLUS --> <!-- #ifndef APP-PLUS -->
<video v-else-if="n.name==='video'" :id="n.attrs.id" :class="n.attrs.class" :style="n.attrs.style" :autoplay="n.attrs.autoplay" :controls="n.attrs.controls" :loop="n.attrs.loop" :muted="n.attrs.muted" :object-fit="n.attrs['object-fit']" :poster="n.attrs.poster" :src="n.src[ctrl[i]||0]" :data-i="i" @play="play" @error="mediaError" /> <video v-else-if="n.name==='video'" :id="n.attrs.id" :class="n.attrs.class" :style="n.attrs.style" :autoplay="n.attrs.autoplay" :controls="n.attrs.controls" :loop="n.attrs.loop" :muted="n.attrs.muted" :object-fit="n.attrs['object-fit']" :poster="n.attrs.poster" :src="n.src[ctrl[i]||0]" :data-i="i" @play="play" @pause="mediaEvent" @fullscreenchange="mediaEvent" @error="mediaError" />
<!-- #endif --> <!-- #endif -->
<!-- #ifdef H5 || APP-PLUS --> <!-- #ifdef H5 || APP-PLUS -->
<iframe v-else-if="n.name==='iframe'" :style="n.attrs.style" :allowfullscreen="n.attrs.allowfullscreen" :frameborder="n.attrs.frameborder" :src="n.attrs.src" /> <iframe v-else-if="n.name==='iframe'" :style="n.attrs.style" :allowfullscreen="n.attrs.allowfullscreen" :frameborder="n.attrs.frameborder" :src="n.attrs.src" />
@@ -46,7 +49,7 @@
<!-- #endif --> <!-- #endif -->
<!-- #ifndef MP-TOUTIAO || ((H5 || APP-PLUS) && VUE3) --> <!-- #ifndef MP-TOUTIAO || ((H5 || APP-PLUS) && VUE3) -->
<!-- 音频 --> <!-- 音频 -->
<audio v-else-if="n.name==='audio'" :id="n.attrs.id" :class="n.attrs.class" :style="n.attrs.style" :author="n.attrs.author" :controls="n.attrs.controls" :loop="n.attrs.loop" :name="n.attrs.name" :poster="n.attrs.poster" :src="n.src[ctrl[i]||0]" :data-i="i" @play="play" @error="mediaError" /> <audio v-else-if="n.name==='audio'" :id="n.attrs.id" :class="n.attrs.class" :style="n.attrs.style" :author="n.attrs.author" :controls="n.attrs.controls" :loop="n.attrs.loop" :name="n.attrs.name" :poster="n.attrs.poster" :src="n.src[ctrl[i]||0]" :data-i="i" @play="play" @pause="mediaEvent" @error="mediaError" />
<!-- #endif --> <!-- #endif -->
<view v-else-if="(n.name==='table'&&n.c)||n.name==='li'" :id="n.attrs.id" :class="'_'+n.name+' '+n.attrs.class" :style="n.attrs.style"> <view v-else-if="(n.name==='table'&&n.c)||n.name==='li'" :id="n.attrs.id" :class="'_'+n.name+' '+n.attrs.class" :style="n.attrs.style">
<node v-if="n.name==='li'" :childs="n.children" :opts="opts" /> <node v-if="n.name==='li'" :childs="n.children" :opts="opts" />
@@ -64,7 +67,7 @@
</block> </block>
</view> </view>
</view> </view>
<!-- insert -->
<!-- 富文本 --> <!-- 富文本 -->
<!-- #ifdef H5 || ((MP-WEIXIN || MP-QQ || APP-PLUS || MP-360) && VUE2) --> <!-- #ifdef H5 || ((MP-WEIXIN || MP-QQ || APP-PLUS || MP-360) && VUE2) -->
<rich-text v-else-if="!n.c&&!handler.isInline(n.name, n.attrs.style)" :id="n.attrs.id" :style="n.f" :user-select="opts[4]" :nodes="[n]" /> <rich-text v-else-if="!n.c&&!handler.isInline(n.name, n.attrs.style)" :id="n.attrs.id" :style="n.f" :user-select="opts[4]" :nodes="[n]" />
@@ -109,7 +112,6 @@ module.exports = {
} }
</script> </script>
<script> <script>
import node from './node' import node from './node'
export default { export default {
name: 'node', name: 'node',
@@ -124,8 +126,9 @@ export default {
data () { data () {
return { return {
ctrl: {}, ctrl: {},
nodes: [],
// #ifdef MP-WEIXIN // #ifdef MP-WEIXIN
isiOS: uni.getSystemInfoSync().system.includes('iOS') isiOS: (uni.canIUse('getDeviceInfo') ? uni.getDeviceInfo() : uni.getSystemInfoSync()).system.includes('iOS')
// #endif // #endif
} }
}, },
@@ -140,9 +143,20 @@ export default {
childs: Array, childs: Array,
opts: Array opts: Array
}, },
watch: {
childs: {
handler (nodes) {
// 列表缩短会刷新整个列表,因此进行空填充
while (this.nodes.length > nodes.length) {
nodes.push({})
}
this.nodes = nodes
},
immediate: true
}
},
components: { components: {
// #ifndef ((H5 || APP-PLUS) && VUE3) || APP-HARMONY
// #ifndef (H5 || APP-PLUS) && VUE3
node node
// #endif // #endif
}, },
@@ -178,7 +192,7 @@ export default {
} }
// #endif // #endif
}, },
methods:{ methods: {
// #ifdef MP-WEIXIN // #ifdef MP-WEIXIN
toJSON () { return this }, toJSON () { return this },
// #endif // #endif
@@ -223,6 +237,22 @@ export default {
} }
// #endif // #endif
}, },
/**
* @description 音视频其他事件
* @param {Event} e
*/
mediaEvent (e) {
const i = e.currentTarget.dataset.i
const node = this.childs[i]
this.root.$emit(e.type, {
...e.detail,
source: node.name,
attrs: {
...node.attrs,
src: node.src[this.ctrl[i] || 0]
}
})
},
/** /**
* @description 图片点击事件 * @description 图片点击事件
@@ -238,7 +268,14 @@ export default {
// #ifdef H5 || APP-PLUS // #ifdef H5 || APP-PLUS
node.attrs.src = node.attrs.src || node.attrs['data-src'] node.attrs.src = node.attrs.src || node.attrs['data-src']
// #endif // #endif
// #ifndef APP-HARMONY
this.root.$emit('imgtap', node.attrs) this.root.$emit('imgtap', node.attrs)
// #endif
// #ifdef APP-HARMONY
this.root.$emit('imgtap', {
...node.attrs
})
// #endif
// 自动预览图片 // 自动预览图片
if (this.root.previewImg) { if (this.root.previewImg) {
uni.previewImage({ uni.previewImage({

View File

@@ -75,13 +75,20 @@ const config = {
foreignobject: 'foreignObject' foreignobject: 'foreignObject'
} }
} }
const tagSelector={} const tagSelector = {}
const { let windowWidth, system
windowWidth, // #ifdef MP-WEIXIN
if (uni.canIUse('getWindowInfo')) {
windowWidth = uni.getWindowInfo().windowWidth
system = uni.getDeviceInfo().system
} else {
// #endif
const systemInfo = uni.getSystemInfoSync()
windowWidth = systemInfo.windowWidth
// #ifdef MP-WEIXIN // #ifdef MP-WEIXIN
system system = systemInfo.system
// #endif }
} = uni.getSystemInfoSync() // #endif
const blankChar = makeMap(' ,\r,\n,\t,\f') const blankChar = makeMap(' ,\r,\n,\t,\f')
let idIndex = 0 let idIndex = 0

View File

@@ -1,76 +1,74 @@
{ {
"id": "mp-html", "name": "mp-html",
"displayName": "mp-html 富文本组件【全端支持支持编辑、latex等扩展】", "version": "2.5.2",
"version": "v2.5.0", "description": "小程序富文本组件",
"description": "一个强大的富文本组件,高效轻量,功能丰富", "miniprogram": "dist/mp-weixin",
"keywords": [ "repository": "https://github.com/jin-yufeng/mp-html",
"富文本", "author": "Jin Yufeng",
"编辑器", "license": "MIT",
"html", "keywords": [
"rich-text", "miniprogram",
"editor" "rich-text",
], "html"
"repository": "https://github.com/jin-yufeng/mp-html", ],
"dcloudext": { "standard": {
"sale": { "globals": ["App", "Page", "Component", "wx", "requirePlugin", "uni", "plus", "weex"],
"regular": { "envs": ["jest", "browser"]
"price": "0.00" },
}, "jest": {
"sourcecode": { "testEnvironment": "jsdom",
"price": "0.00" "collectCoverageFrom": [
} "dev/mp-weixin/components/mp-html/**/*.js"
}, ]
"contact": { },
"qq": "" "scripts": {
}, "lint": "node lint.js",
"declaration": { "lintcss": "npx stylelint src/**/*.wxss",
"ads": "无", "build:weixin": "gulp build --mp-weixin",
"data": "无", "build:qq": "gulp build --mp-qq",
"permissions": "无" "build:baidu": "gulp build --mp-baidu",
}, "build:alipay": "gulp build --mp-alipay",
"npmurl": "https://www.npmjs.com/package/mp-html", "build:toutiao": "gulp build --mp-toutiao",
"type": "component-vue" "build:uni-app": "gulp build --uni-app",
}, "build": "gulp build --mp-weixin & gulp build --mp-qq & gulp build --mp-baidu & gulp build --mp-alipay & gulp build --mp-toutiao & gulp build --uni-app",
"uni_modules": { "watch:weixin": "gulp watch --mp-weixin --dev",
"platforms": { "watch:qq": "gulp watch --mp-qq --dev",
"cloud": { "watch:baidu": "gulp watch --mp-baidu --dev",
"tcb": "y", "watch:alipay": "gulp watch --mp-alipay --dev",
"aliyun": "y" "watch:toutiao": "gulp watch --mp-toutiao --dev",
}, "watch:uni-app": "gulp watch --uni-app --dev",
"client": { "dev:weixin": "gulp dev --mp-weixin --dev",
"App": { "dev:qq": "gulp dev --mp-qq --dev",
"app-vue": "y", "dev:baidu": "gulp dev --mp-baidu --dev",
"app-nvue": "y" "dev:alipay": "gulp dev --mp-alipay --dev",
}, "dev:toutiao": "gulp dev --mp-toutiao --dev",
"H5-mobile": { "dev:uni-app": "gulp dev --uni-app --dev",
"Safari": "y", "test": "gulp dev --mp-weixin --dev && npx jest",
"Android Browser": "y", "coverage": "gulp dev --mp-weixin --dev && npx jest --coverage",
"微信浏览器(Android)": "y", "coveralls": "npx coveralls < coverage/lcov.info",
"QQ浏览器(Android)": "y" "clean": "gulp clean --all",
}, "clean:dev": "gulp clean --all --dev"
"H5-pc": { },
"Chrome": "y", "devDependencies": {
"IE": "u", "@babel/preset-env": "^7.12.1",
"Edge": "y", "coveralls": "^3.1.0",
"Firefox": "y", "gulp": "^4.0.0",
"Safari": "y" "gulp-babel": "^8.0.0",
}, "gulp-clean": "^0.4.0",
"小程序": { "gulp-clean-css": "^4.3.0",
"微信": "y", "gulp-htmlmin": "^5.0.1",
"阿里": "y", "gulp-if": "^3.0.0",
"百度": "y", "gulp-plumber": "^1.2.1",
"字节跳动": "y", "gulp-size": "^3.0.0",
"QQ": "y" "gulp-uglify": "^2.1.2",
}, "jest": "^26.6.1",
"快应用": { "miniprogram-simulate": "^1.2.7",
"华为": "y", "standard": "^16.0.3",
"联盟": "y" "stylelint": "^13.7.2",
}, "stylelint-config-recess-order": "^2.3.0",
"Vue": { "stylelint-config-standard": "^20.0.0",
"vue2": "y", "through2": "^4.0.2",
"vue3": "y" "uglify-js": "^2.8.29"
} },
} "dependencies": {}
}
}
} }

View File

@@ -1 +1,254 @@
"use strict";function t(t){for(var e=Object.create(null),n=t.attributes.length;n--;)e[t.attributes[n].name]=t.attributes[n].value;return e}function e(){a[1]&&(this.src=a[1],this.onerror=null),this.onclick=null,this.ontouchstart=null,uni.postMessage({data:{action:"onError",source:"img",attrs:t(this)}})}function n(){window.unloadimgs-=1,0===window.unloadimgs&&uni.postMessage({data:{action:"onReady"}})}function o(r,s,c){for(var d=0;d<r.length;d++)!function(d){var u=r[d],l=void 0;if(u.type&&"node"!==u.type)l=document.createTextNode(u.text.replace(/&amp;/g,"&"));else{var g=u.name;"svg"===g&&(c="http://www.w3.org/2000/svg"),"html"!==g&&"body"!==g||(g="div"),l=c?document.createElementNS(c,g):document.createElement(g);for(var p in u.attrs)l.setAttribute(p,u.attrs[p]);if(u.children&&o(u.children,l,c),"img"===g){if(window.unloadimgs+=1,l.onload=n,l.onerror=n,!l.src&&l.getAttribute("data-src")&&(l.src=l.getAttribute("data-src")),u.attrs.ignore||(l.onclick=function(e){e.stopPropagation(),uni.postMessage({data:{action:"onImgTap",attrs:t(this)}})}),a[2]){var h=new Image;h.src=l.src,l.src=a[2],h.onload=function(){l.src=this.src},h.onerror=function(){l.onerror()}}l.onerror=e}else if("a"===g)l.addEventListener("click",function(e){e.stopPropagation(),e.preventDefault();var n,o=this.getAttribute("href");o&&"#"===o[0]&&(n=(document.getElementById(o.substr(1))||{}).offsetTop),uni.postMessage({data:{action:"onLinkTap",attrs:t(this),offset:n}})},!0);else if("video"===g||"audio"===g)i.push(l),u.attrs.autoplay||u.attrs.controls||l.setAttribute("controls","true"),l.onplay=function(){if(uni.postMessage({data:{action:"onPlay"}}),a[3])for(var t=0;t<i.length;t++)i[t]!==this&&i[t].pause()},l.onerror=function(){uni.postMessage({data:{action:"onError",source:g,attrs:t(this)}})};else if("table"===g&&a[4]&&!l.style.cssText.includes("inline")){var f=document.createElement("div");f.style.overflow="auto",f.appendChild(l),l=f}else"svg"===g&&(c=void 0)}s.appendChild(l)}(d)}document.addEventListener("UniAppJSBridgeReady",function(){document.body.onclick=function(){return uni.postMessage({data:{action:"onClick"}})},uni.postMessage({data:{action:"onJSBridgeReady"}})});var a,i=[];window.setContent=function(t,e,n){var r=document.getElementById("content");e[0]&&(document.body.style.cssText=e[0]),e[5]||(r.style.userSelect="none"),n||(r.innerHTML="",i=[]),a=e,window.unloadimgs=0;var s=document.createDocumentFragment();o(t,s),r.appendChild(s);var c=r.scrollHeight;uni.postMessage({data:{action:"onLoad",height:c}}),window.unloadimgs||uni.postMessage({data:{action:"onReady",height:c}}),clearInterval(window.timer),window.timer=setInterval(function(){r.scrollHeight!==c&&(c=r.scrollHeight,uni.postMessage({data:{action:"onHeightChange",height:c}}))},350)},window.onunload=function(){clearInterval(window.timer)}; // 等待初始化完毕
document.addEventListener('UniAppJSBridgeReady', () => {
document.body.onclick = () =>
uni.postMessage({
data: {
action: 'onClick'
}
})
uni.postMessage({
data: {
action: 'onJSBridgeReady'
}
})
})
let options
let medias = []
/**
* @description 获取标签的所有属性
* @param {Element} ele
*/
function getAttrs (ele) {
const attrs = Object.create(null)
for (let i = ele.attributes.length; i--;) {
attrs[ele.attributes[i].name] = ele.attributes[i].value
}
return attrs
}
/**
* @description 图片加载出错
*/
function onImgError () {
if (options[1]) {
this.src = options[1]
this.onerror = null
}
// 取消监听点击
this.onclick = null
this.ontouchstart = null
uni.postMessage({
data: {
action: 'onError',
source: 'img',
attrs: getAttrs(this)
}
})
}
/**
* @description 检查是否所有图片加载完毕
*/
function checkReady () {
window.unloadimgs -= 1
if (window.unloadimgs === 0) {
// 所有图片加载完毕
uni.postMessage({
data: {
action: 'onReady'
}
})
}
}
/**
* @description 创建 dom 结构
* @param {object[]} nodes 节点数组
* @param {Element} parent 父节点
* @param {string} namespace 命名空间
*/
function createDom (nodes, parent, namespace) {
for (let i = 0; i < nodes.length; i++) {
const node = nodes[i]
let ele
if (!node.type || node.type === 'node') {
let name = node.name
// svg 需要设置 namespace
if (name === 'svg') {
namespace = 'http://www.w3.org/2000/svg'
}
if (name === 'html' || name === 'body') {
name = 'div'
}
// 创建标签
if (!namespace) {
ele = document.createElement(name)
} else {
ele = document.createElementNS(namespace, name)
}
// 设置属性
for (const item in node.attrs) {
ele.setAttribute(item, node.attrs[item])
}
// 递归创建子节点
if (node.children) {
createDom(node.children, ele, namespace)
}
// 处理图片
if (name === 'img') {
window.unloadimgs += 1
ele.onload = checkReady
ele.onerror = checkReady
if (!ele.src && ele.getAttribute('data-src')) {
ele.src = ele.getAttribute('data-src')
}
if (!node.attrs.ignore) {
// 监听图片点击事件
ele.onclick = function (e) {
e.stopPropagation()
uni.postMessage({
data: {
action: 'onImgTap',
attrs: getAttrs(this)
}
})
}
}
if (options[2]) {
const image = new Image()
image.src = ele.src
ele.src = options[2]
image.onload = function () {
ele.src = this.src
}
image.onerror = function () {
ele.onerror()
}
}
ele.onerror = onImgError
} else if (name === 'a') {
// 处理链接
ele.addEventListener('click', function (e) {
e.stopPropagation()
e.preventDefault() // 阻止默认跳转
const href = this.getAttribute('href')
let offset
if (href && href[0] === '#') {
offset = (document.getElementById(href.substr(1)) || {}).offsetTop
}
uni.postMessage({
data: {
action: 'onLinkTap',
attrs: getAttrs(this),
offset
}
})
}, true)
} else if (name === 'video' || name === 'audio') {
// 处理音视频
medias.push(ele)
if (!node.attrs.autoplay && !node.attrs.controls) {
ele.setAttribute('controls', 'true')
}
ele.onplay = function () {
uni.postMessage({
data: {
action: 'onPlay'
}
})
if (options[3]) {
for (let i = 0; i < medias.length; i++) {
if (medias[i] !== this) {
medias[i].pause()
}
}
}
}
ele.onerror = function () {
uni.postMessage({
data: {
action: 'onError',
source: name,
attrs: getAttrs(this)
}
})
}
} else if (name === 'table' && options[4] && !ele.style.cssText.includes('inline')) {
// 处理表格
const div = document.createElement('div')
div.style.overflow = 'auto'
div.appendChild(ele)
ele = div
} else if (name === 'svg') {
namespace = undefined
}
} else {
ele = document.createTextNode(node.text.replace(/&amp;/g, '&'))
}
parent.appendChild(ele)
}
}
// 设置 html 内容
window.setContent = function (nodes, opts, append) {
const ele = document.getElementById('content')
// 容器样式
if (opts[0]) {
document.body.style.cssText = opts[0]
}
// 长按复制
if (!opts[5]) {
ele.style.userSelect = 'none'
}
if (!append) {
ele.innerHTML = '' // 不追加则先清空
medias = []
}
options = opts
window.unloadimgs = 0
const fragment = document.createDocumentFragment()
createDom(nodes, fragment)
ele.appendChild(fragment)
// 触发事件
let height = ele.scrollHeight
uni.postMessage({
data: {
action: 'onLoad',
height
}
})
if (!window.unloadimgs) {
uni.postMessage({
data: {
action: 'onReady',
height
}
})
}
clearInterval(window.timer)
window.timer = setInterval(() => {
if (ele.scrollHeight !== height) {
height = ele.scrollHeight
uni.postMessage({
data: {
action: 'onHeightChange',
height: height
}
})
}
}, 350)
}
// 回收计时器
window.onunload = function () {
clearInterval(window.timer)
}

View File

@@ -1 +1,33 @@
<head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no"><style>body,html{width:100%;height:100%;overflow-x:scroll;overflow-y:hidden}body{margin:0}video{width:300px;height:225px}img{max-width:100%;-webkit-touch-callout:none}</style></head><body><div id="content" style="overflow:hidden"></div><script type="text/javascript" src="./js/uni.webview.min.js"></script><script type="text/javascript" src="./js/handler.js"></script></body> <head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no">
<style>
html,
body {
width: 100%;
height: 100%;
overflow-x: scroll;
overflow-y: hidden;
}
body {
margin: 0;
}
video {
width: 300px;
height: 225px;
}
img {
max-width: 100%;
-webkit-touch-callout: none;
}
</style>
</head>
<body>
<div id="content" style="overflow: hidden;"></div>
<script type="text/javascript" src="./js/uni.webview.min.js"></script>
<script type="text/javascript" src="./js/handler.js"></script>
</body>