Commit b8b21020 authored by weishengfei's avatar weishengfei

feat(productionCalendar): 优化日历页面数据获取逻辑

- 保留 checkupList 中状态为 completed 的数据
- 添加数据获取后的日志输出
- 在 productionDetails 页面中使用 mp-html 组件替换 rich-text 组件
parent ad188cf7
......@@ -409,7 +409,13 @@ const getInfoFn = async () => {
const {code,success, message, data } = await getInfo()
console.log('日历页面获取信息', code, success, message, data)
if (success) {
info.value = data
// 保留 data 里面 checkupList 里面 status 等于 completed 的数据
info.value = {
...data,
checkupList: data.checkupList.filter(item => item.status === 'completed')
}
console.log('日历获取数据:', info.value)
} else {
uni.showToast({
title: message,
......
......@@ -57,8 +57,8 @@
</view>
<view class="project-content">
<rich-text class="desc" :nodes="infoData.content"></rich-text>
<!-- <rich-text class="desc" :nodes="infoData.content"></rich-text> -->
<mp-html :content="infoData.content" ></mp-html>
</view>
</view>
<!-- 产检项目 -->
......
## 为减小组件包的大小,默认组件包中不包含编辑、latex 公式等扩展功能,需要使用扩展功能的请参考下方的 插件扩展 栏的说明
## 功能介绍
- 全端支持(含 `v3、NVUE`
- 支持丰富的标签(包括 `table``video``svg` 等)
- 支持丰富的事件效果(自动预览图片、链接处理等)
- 支持设置占位图(加载中、出错时、预览时)
- 支持锚点跳转、长按复制等丰富功能
- 支持大部分 *html* 实体
- 丰富的插件(关键词搜索、内容编辑、`latex` 公式等)
- 效率高、容错性强且轻量化
查看 [功能介绍](https://jin-yufeng.github.io/mp-html/#/overview/feature) 了解更多
## 使用方法
- `uni_modules` 方式
1. 点击右上角的 `使用 HBuilder X 导入插件` 按钮直接导入项目或点击 `下载插件 ZIP` 按钮下载插件包并解压到项目的 `uni_modules/mp-html` 目录下
2. 在需要使用页面的 `(n)vue` 文件中添加
```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
npm install mp-html
```
2. 在需要使用页面的 `(n)vue` 文件中添加
```html
<mp-html :content="html" />
```
```javascript
import mpHtml from 'mp-html/dist/uni-app/components/mp-html/mp-html'
export default {
// 不可省略
components: {
mpHtml
},
data() {
return {
html: '<div>Hello World!</div>'
}
}
}
```
3. 需要更新版本时执行以下命令即可
```bash
npm update mp-html
```
使用 *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.github.io/mp-html/#/changelog/changelog#v210)) |
| content | String | | 用于渲染的 html 字符串 |
| copy-link | Boolean | true | 是否允许外部链接被点击时自动复制 |
| domain | String | | 主域名(用于链接拼接) |
| error-img | String | | 图片出错时的占位图链接 |
| lazy-load | Boolean | false | 是否开启图片懒加载 |
| loading-img | String | | 图片加载过程中的占位图链接 |
| pause-video | Boolean | true | 是否在播放一个视频时自动暂停其他视频 |
| preview-img | Boolean | true | 是否允许图片被点击时自动预览 |
| scroll-table | Boolean | false | 是否给每个表格添加一个滚动层使其能单独横向滚动 |
| selectable | Boolean | false | 是否开启文本长按复制 |
| set-title | Boolean | true | 是否将 title 标签的内容设置到页面标题 |
| show-img-menu | Boolean | true | 是否允许图片被长按时显示菜单 |
| tag-style | Object | | 设置标签的默认样式 |
| use-anchor | Boolean | false | 是否使用锚点链接 |
查看 [属性](https://jin-yufeng.github.io/mp-html/#/basic/prop) 了解更多
## 组件事件
| 名称 | 触发时机 |
|:---:|---|
| load | dom 树加载完毕时 |
| ready | 图片加载完毕时 |
| error | 发生渲染错误时 |
| imgtap | 图片被点击时 |
| linktap | 链接被点击时 |
| play | 音视频播放时 |
查看 [事件](https://jin-yufeng.github.io/mp-html/#/basic/event) 了解更多
## api
组件实例上提供了一些 `api` 方法可供调用
| 名称 | 作用 |
|:---:|---|
| in | 将锚点跳转的范围限定在一个 scroll-view 内 |
| navigateTo | 锚点跳转 |
| getText | 获取文本内容 |
| getRect | 获取富文本内容的位置和大小 |
| setContent | 设置富文本内容 |
| imgList | 获取所有图片的数组 |
| pauseMedia | 暂停播放音视频([2.2.2+](https://jin-yufeng.github.io/mp-html/#/changelog/changelog#v222)) |
| setPlaybackRate | 设置音视频播放速率([2.4.0+](https://jin-yufeng.github.io/mp-html/#/changelog/changelog#v240)) |
查看 [api](https://jin-yufeng.github.io/mp-html/#/advanced/api) 了解更多
## 插件扩展
除基本功能外,本组件还提供了丰富的扩展,可按照需要选用
| 名称 | 作用 |
|:---:|---|
| audio | 音乐播放器 |
| editable | 富文本 **编辑**[示例项目](https://mp-html.oss-cn-hangzhou.aliyuncs.com/editable.zip)) |
| emoji | 解析 emoji |
| highlight | 代码块高亮显示 |
| markdown | 渲染 markdown |
| search | 关键词搜索 |
| style | 匹配 style 标签中的样式 |
| txv-video | 使用腾讯视频 |
| img-cache | 图片缓存 by [@PentaTea](https://github.com/PentaTea) |
| latex | 渲染 latex 公式 by [@Zeng-J](https://github.com/Zeng-J) |
从插件市场导入的包中 **不含有** 扩展插件,使用插件需通过微信小程序 `富文本插件` 获取或参考以下方法进行打包:
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.github.io/mp-html/#/advanced/plugin) 了解更多
## 关于 nvue
`nvue` 使用原生渲染,不支持部分 `css` 样式,为实现和 `html` 相同的效果,组件内部通过 `web-view` 进行渲染,性能上差于原生,根据 `weex` 官方建议,`web` 标签仅应用在非常规的降级场景。因此,如果通过原生的方式(如 `richtext`)能够满足需要,则不建议使用本组件,如果有较多的富文本内容,则可以直接使用 `vue` 页面
由于渲染方式与其他端不同,有以下限制:
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://jin-yufeng.github.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`
群3:`960265313`
查看 [问题反馈](https://jin-yufeng.github.io/mp-html/#/question/feedback) 了解更多
## v2.5.1(2025-04-20)
1. `U` 适配鸿蒙 `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` 修复了 `app` 端播放视频可能报错的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/617)
4. `F` 修复了 `latex` 插件可能出现 `xxx can be used only in display mode` 的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/632)
5. `F` 修复了 `uni-app``latex` 公式可能不显示的问题 [#599](https://github.com/jin-yufeng/mp-html/issues/599)[#627](https://github.com/jin-yufeng/mp-html/issues/627)
## v2.5.0(2024-04-22)
1. `U` `play` 事件增加返回 `src` 等信息 [详细](https://github.com/jin-yufeng/mp-html/issues/526)
2. `U` `preview-img` 属性支持设置为 `all` 开启 `base64` 图片预览 [详细](https://github.com/jin-yufeng/mp-html/issues/536)
3. `U` `editable` 插件增加简易模式(点击文字直接编辑)
4. `U` `latex` 插件支持块级公式 [详细](https://github.com/jin-yufeng/mp-html/issues/582)
5. `F` 修复了表格部分情况下背景丢失的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/587)
6. `F` 修复了部分 `svg` 无法显示的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/591)
7. `F` 修复了 `h5``app` 端部分情况下样式无法识别的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/518)
8. `F` 修复了 `latex` 插件部分情况下显示不正确的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/580)
9. `F` 修复了 `editable` 插件表格无法删除的问题
10. `F` 修复了 `editable` 插件 `vue3` `h5` 端点击图片报错的问题
11. `F` 修复了 `editable` 插件点击表格没有菜单栏的问题
## v2.4.3(2024-01-21)
1. `A` 增加 [card](https://jin-yufeng.gitee.io/mp-html/#/advanced/plugin#card) 插件 [详细](https://github.com/jin-yufeng/mp-html/pull/533) by [@whoooami](https://github.com/whoooami)
2. `F` 修复了 `svg` 中包含 `foreignobject` 可能不显示的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/523)
3. `F` 修复了合并单元格的表格部分情况下显示不正确的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/561)
4. `F` 修复了 `img` 标签设置 `object-fit` 无效的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/567)
5. `F` 修复了 `latex` 插件公式会换行的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/540)
6. `F` 修复了 `editable``audio` 插件共用时点击 `audio` 无法编辑的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/529) by [@whoooami](https://github.com/whoooami)
7. `F` 修复了微信小程序部分情况下图片会报错 `replace of undefined` 的问题
8. `F` 修复了快手小程序图片不显示的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/571)
## v2.4.2(2023-05-14)
1. `A` `editable` 插件支持修改文字颜色 [详细](https://github.com/jin-yufeng/mp-html/issues/254)
2. `F` 修复了 `svg` 中有 `style` 不生效的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/505)
3. `F` 修复了使用旧版编译器可能报错 `Bad attr nodes` 的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/472)
4. `F` 修复了 `app` 端可能出现无法读取 `lazyLoad` 的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/513)
5. `F` 修复了 `editable` 插件在点击换图时未拼接 `domain` 的问题 [详细](https://github.com/jin-yufeng/mp-html/pull/497) by [@TwoKe945](https://github.com/TwoKe945)
6. `F` 修复了 `latex` 插件部分情况下不显示的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/515)
7. `F` 修复了 `editable` 插件点击音视频时其他标签框不消失的问题
## v2.4.1(2022-12-25)
1. `F` 修复了没有图片时 `ready` 事件可能不触发的问题
2. `F` 修复了加载过程中可能出现 `Root label not found` 错误的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/470)
3. `F` 修复了 `audio` 插件退出页面可能会报错的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/457)
4. `F` 修复了 `vue3` 运行到 `app``HBuilder X 3.6.10` 以上报错的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/480)
5. `F` 修复了 `nvue` 端链接中包含 `%22` 时可能无法显示的问题
6. `F` 修复了 `vue3` 使用 `highlight` 插件可能报错的问题
## v2.4.0(2022-08-27)
1. `A` 增加了 [setPlaybackRate](https://jin-yufeng.gitee.io/mp-html/#/advanced/api#setPlaybackRate)`api`,可以设置音视频的播放速率 [详细](https://github.com/jin-yufeng/mp-html/issues/452)
2. `A` 示例小程序代码开源 [详细](https://github.com/jin-yufeng/mp-html-demo)
3. `U` 优化 `ready` 事件触发时机,未设置懒加载的情况下基本可以准确触发 [详细](https://github.com/jin-yufeng/mp-html/issues/195)
4. `U` `highlight` 插件在编辑状态下不进行高亮处理,便于编辑
5. `F` 修复了 `flex` 布局下图片大小可能不正确的问题
6. `F` 修复了 `selectable` 属性没有设置 `force` 也可能出现渲染异常的问题
7. `F` 修复了表格中的图片大小可能不正确的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/448)
8. `F` 修复了含有合并单元格的表格可能无法设置竖直对齐的问题
9. `F` 修复了 `editable` 插件在 `scroll-view` 中使用时工具条位置可能不正确的问题
10. `F` 修复了 `vue3` 使用 [search](advanced/plugin#search) 插件可能导致错误换行的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/449)
## v2.3.2(2022-08-13)
1. `A` 增加 [latex](https://jin-yufeng.gitee.io/mp-html/#/advanced/plugin#latex) 插件,可以渲染数学公式 [详细](https://github.com/jin-yufeng/mp-html/pull/447) by [@Zeng-J](https://github.com/Zeng-J)
2. `U` 优化根节点下有很多标签的长内容渲染速度
3. `U` `highlight` 插件适配 `lang-xxx` 格式
4. `F` 修复了 `table` 标签设置 `border` 属性后可能无法修改边框样式的问题 [详细](https://github.com/jin-yufeng/mp-html/pull/439) by [@zouxingjie](https://github.com/zouxingjie)
5. `F` 修复了 `editable` 插件输入连续空格无效的问题
6. `F` 修复了 `vue3` 图片设置 `inline` 会报错的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/438)
7. `F` 修复了 `vue3` 使用 `table` 可能报错的问题
## v2.3.1(2022-05-20)
1. `U` `app` 端支持使用本地图片
2. `U` 优化了微信小程序 `selectable` 属性在 `ios` 端的处理 [详细](https://jin-yufeng.gitee.io/mp-html/#/basic/prop#selectable)
3. `F` 修复了 `editable` 插件不在顶部时 `tooltip` 位置可能错误的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/430)
4. `F` 修复了 `vue3` 运行到微信小程序可能报错丢失内容的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/414)
5. `F` 修复了 `vue3` 部分标签可能被错误换行的问题
6. `F` 修复了 `editable` 插件 `app` 端插入视频无法预览的问题
## v2.3.0(2022-04-01)
1. `A` 增加了 `play` 事件,音视频播放时触发,可用于与页面其他音视频进行互斥播放 [详细](basic/event#play)
2. `U` `show-img-menu` 属性支持控制预览时是否长按弹出菜单
3. `U` 优化 `wxs` 处理,提高渲染性能 [详细](https://developers.weixin.qq.com/community/develop/article/doc/0006cc2b204740f601bd43fa25a413)
4. `U` `video` 标签支持 `object-fit` 属性
5. `U` 增加支持一些常用实体编码 [详细](https://github.com/jin-yufeng/mp-html/issues/418)
6. `F` 修复了图片仅设置高度可能不显示的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/410)
7. `F` 修复了 `video` 标签高度设置为 `auto` 不显示的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/411)
8. `F` 修复了使用 `grid` 布局时可能样式错误的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/413)
9. `F` 修复了含有合并单元格的表格部分情况下显示异常的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/417)
10. `F` 修复了 `editable` 插件连续插入内容时顺序不正确的问题
11. `F` 修复了 `uni-app``vue3` 使用 `audio` 插件报错的问题
12. `F` 修复了 `uni-app``highlight` 插件使用自定义的 `prism.min.js` 报错的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/416)
## v2.2.2(2022-02-26)
1. `A` 增加了 [pauseMedia](https://jin-yufeng.gitee.io/mp-html/#/advanced/api#pauseMedia)`api`,可用于暂停播放音视频 [详细](https://github.com/jin-yufeng/mp-html/issues/317)
2. `U` 优化了长内容的加载速度
3. `U` 适配 `vue3` [#389](https://github.com/jin-yufeng/mp-html/issues/389)[#398](https://github.com/jin-yufeng/mp-html/pull/398) by [@zhouhuafei](https://github.com/zhouhuafei)[#400](https://github.com/jin-yufeng/mp-html/issues/400)
4. `F` 修复了小程序端图片高度设置为百分比时可能不显示的问题
5. `F` 修复了 `highlight` 插件部分情况下可能显示不完整的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/403)
## v2.2.1(2021-12-24)
1. `A` `editable` 插件增加上下移动标签功能
2. `U` `editable` 插件支持在文本中间光标处插入内容
3. `F` 修复了 `nvue` 端设置 `margin` 后可能导致高度不正确的问题
4. `F` 修复了 `highlight` 插件使用压缩版的 `prism.css` 可能导致背景失效的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/367)
5. `F` 修复了编辑状态下使用 `emoji` 插件内容为空时可能报错的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/371)
6. `F` 修复了使用 `editable` 插件后将 `selectable` 属性设置为 `force` 不生效的问题
## v2.2.0(2021-10-12)
1. `A` 增加 `customElements` 配置项,便于添加自定义功能性标签 [详细](https://github.com/jin-yufeng/mp-html/issues/350)
2. `A` `editable` 插件增加切换音视频自动播放状态的功能 [详细](https://github.com/jin-yufeng/mp-html/pull/341) by [@leeseett](https://github.com/leeseett)
3. `A` `editable` 插件删除媒体标签时触发 `remove` 事件,便于删除已上传的文件
4. `U` `editable` 插件 `insertImg` 方法支持同时插入多张图片 [详细](https://github.com/jin-yufeng/mp-html/issues/342)
5. `U` `editable` 插入图片和音视频时支持拼接 `domian` 主域名
6. `F` 修复了内部链接参数中包含 `://` 时被认为是外部链接的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/356)
7. `F` 修复了部分 `svg` 标签名或属性名大小写不正确时不生效的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/351)
8. `F` 修复了 `nvue` 页面运行到非 `app` 平台时可能样式错误的问题
## v2.1.5(2021-08-13)
1. `A` 增加支持标签的 `dir` 属性
2. `F` 修复了 `ruby` 标签文字与拼音没有居中对齐的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/325)
3. `F` 修复了音视频标签内有 `a` 标签时可能无法播放的问题
4. `F` 修复了 `externStyle` 中的 `class` 名包含下划线或数字时可能失效的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/326)
5. `F` 修复了 `h5` 端引入 `externStyle` 可能不生效的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/326)
## v2.1.4(2021-07-14)
1. `F` 修复了 `rt` 标签无法设置样式的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/318)
2. `F` 修复了表格中有单元格同时合并行和列时可能显示不正确的问题
3. `F` 修复了 `app` 端无法关闭图片长按菜单的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/322)
4. `F` 修复了 `editable` 插件只能添加图片链接不能修改的问题 [详细](https://github.com/jin-yufeng/mp-html/pull/312) by [@leeseett](https://github.com/leeseett)
## v2.1.3(2021-06-12)
1. `A` `editable` 插件增加 `insertTable` 方法
2. `U` `editable` 插件支持编辑表格中的空白单元格 [详细](https://github.com/jin-yufeng/mp-html/issues/310)
3. `F` 修复了 `externStyle` 中使用伪类可能失效的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/298)
4. `F` 修复了多个组件同时使用时 `tag-style` 属性时可能互相影响的问题 [详细](https://github.com/jin-yufeng/mp-html/pull/305) by [@woodguoyu](https://github.com/woodguoyu)
5. `F` 修复了包含 `linearGradient``svg` 可能无法显示的问题
6. `F` 修复了编译到头条小程序时可能报错的问题
7. `F` 修复了 `nvue` 端不触发 `click` 事件的问题
8. `F` 修复了 `editable` 插件尾部插入时无法撤销的问题
9. `F` 修复了 `editable` 插件的 `insertHtml` 方法只能在末尾插入的问题
10. `F` 修复了 `editable` 插件插入音频不显示的问题
## v2.1.2(2021-04-24)
1. `A` 增加了 [img-cache](https://jin-yufeng.gitee.io/mp-html/#/advanced/plugin#img-cache) 插件,可以在 `app` 端缓存图片 [详细](https://github.com/jin-yufeng/mp-html/issues/292) by [@PentaTea](https://github.com/PentaTea)
2. `U` 支持通过 `container-style` 属性设置 `white-space` 来保留连续空格和换行符 [详细](https://jin-yufeng.gitee.io/mp-html/#/question/faq#space)
3. `U` 代码风格符合 [standard](https://standardjs.com) 标准
4. `U` `editable` 插件编辑状态下支持预览视频 [详细](https://github.com/jin-yufeng/mp-html/issues/286)
5. `F` 修复了 `svg` 标签内嵌 `svg` 时无法显示的问题
6. `F` 修复了编译到支付宝和头条小程序时部分区域不可复制的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/291)
## v2.1.1(2021-04-09)
1. 修复了对 `p` 标签设置 `tag-style` 可能不生效的问题
2. 修复了 `svg` 标签中的文本无法显示的问题
3. 修复了使用 `editable` 插件编辑表格时可能报错的问题
4. 修复了使用 `highlight` 插件运行到头条小程序时可能没有样式的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/280)
5. 修复了使用 `editable` 插件 `editable` 属性为 `false` 时会报错的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/284)
6. 修复了 `style` 插件连续子选择器失效的问题
7. 修复了 `editable` 插件无法修改图片和字体大小的问题
## v2.1.0.2(2021-03-21)
修复了 `nvue` 端使用可能报错的问题
## v2.1.0(2021-03-20)
1. `A` 增加了 [container-style](https://jin-yufeng.gitee.io/mp-html/#/basic/prop#container-style) 属性 [详细](https://gitee.com/jin-yufeng/mp-html/pulls/1)
2. `A` 增加支持 `strike` 标签
3. `A` `editable` 插件增加 `placeholder` 属性 [详细](https://jin-yufeng.gitee.io/mp-html/#/advanced/plugin#editable)
4. `A` `editable` 插件增加 `insertHtml` 方法 [详细](https://jin-yufeng.gitee.io/mp-html/#/advanced/plugin#editable)
5. `U` 外部样式支持标签名选择器 [详细](https://jin-yufeng.gitee.io/mp-html/#/overview/quickstart#setting)
6. `F` 修复了 `nvue` 端部分情况下可能不显示的问题
## v2.0.5(2021-03-12)
1. `U` [linktap](https://jin-yufeng.gitee.io/mp-html/#/basic/event#linktap) 事件增加返回内部文本内容 `innerText` [详细](https://github.com/jin-yufeng/mp-html/issues/271)
2. `U` [selectable](https://jin-yufeng.gitee.io/mp-html/#/basic/prop#selectable) 属性设置为 `force` 时能够在微信 `iOS` 端生效(文本块会变成 `inline-block`[详细](https://github.com/jin-yufeng/mp-html/issues/267)
3. `F` 修复了部分情况下竖向无法滚动的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/182)
4. `F` 修复了多次修改富文本数据时部分内容可能不显示的问题
5. `F` 修复了 [腾讯视频](https://jin-yufeng.gitee.io/mp-html/#/advanced/plugin#txv-video) 插件可能无法播放的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/265)
6. `F` 修复了 [highlight](https://jin-yufeng.gitee.io/mp-html/#/advanced/plugin#highlight) 插件没有设置高亮语言时没有应用默认样式的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/276) by [@fuzui](https://github.com/fuzui)
<template>
<view id="_root" :class="(selectable?'_select ':'')+'_root'" :style="containerStyle">
<slot v-if="!nodes[0]" />
<!-- #ifndef APP-PLUS-NVUE -->
<node v-else :childs="nodes" :opts="[lazyLoad,loadingImg,errorImg,showImgMenu,selectable]" name="span" />
<!-- #endif -->
<!-- #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" />
<!-- #endif -->
</view>
</template>
<script>
/**
* mp-html v2.5.1
* @description 富文本组件
* @tutorial https://github.com/jin-yufeng/mp-html
* @property {String} container-style 容器的样式
* @property {String} content 用于渲染的 html 字符串
* @property {Boolean} copy-link 是否允许外部链接被点击时自动复制
* @property {String} domain 主域名,用于拼接链接
* @property {String} error-img 图片出错时的占位图链接
* @property {Boolean} lazy-load 是否开启图片懒加载
* @property {string} loading-img 图片加载过程中的占位图链接
* @property {Boolean} pause-video 是否在播放一个视频时自动暂停其他视频
* @property {Boolean} preview-img 是否允许图片被点击时自动预览
* @property {Boolean} scroll-table 是否给每个表格添加一个滚动层使其能单独横向滚动
* @property {Boolean | String} selectable 是否开启长按复制
* @property {Boolean} set-title 是否将 title 标签的内容设置到页面标题
* @property {Boolean} show-img-menu 是否允许图片被长按时显示菜单
* @property {Object} tag-style 标签的默认样式
* @property {Boolean | Number} use-anchor 是否使用锚点链接
* @event {Function} load dom 结构加载完毕时触发
* @event {Function} ready 所有图片加载完毕时触发
* @event {Function} imgtap 图片被点击时触发
* @event {Function} linktap 链接被点击时触发
* @event {Function} play 音视频播放时触发
* @event {Function} error 媒体加载出错时触发
*/
// #ifndef APP-PLUS-NVUE
import node from './node/node'
// #endif
import Parser from './parser'
const plugins=[]
// #ifdef APP-PLUS-NVUE
const dom = weex.requireModule('dom')
// #endif
export default {
name: 'mp-html',
data () {
return {
nodes: [],
// #ifdef APP-PLUS-NVUE
height: 3
// #endif
}
},
props: {
containerStyle: {
type: String,
default: ''
},
content: {
type: String,
default: ''
},
copyLink: {
type: [Boolean, String],
default: true
},
domain: String,
errorImg: {
type: String,
default: ''
},
lazyLoad: {
type: [Boolean, String],
default: false
},
loadingImg: {
type: String,
default: ''
},
pauseVideo: {
type: [Boolean, String],
default: true
},
previewImg: {
type: [Boolean, String],
default: true
},
scrollTable: [Boolean, String],
selectable: [Boolean, String],
setTitle: {
type: [Boolean, String],
default: true
},
showImgMenu: {
type: [Boolean, String],
default: true
},
tagStyle: Object,
useAnchor: [Boolean, Number]
},
// #ifdef VUE3
emits: ['load', 'ready', 'imgtap', 'linktap', 'play', 'error'],
// #endif
// #ifndef APP-PLUS-NVUE
components: {
node
},
// #endif
watch: {
content (content) {
this.setContent(content)
}
},
created () {
this.plugins = []
for (let i = plugins.length; i--;) {
this.plugins.push(new plugins[i](this))
}
},
mounted () {
if (this.content && !this.nodes.length) {
this.setContent(this.content)
}
},
beforeDestroy () {
this._hook('onDetached')
},
methods: {
/**
* @description 将锚点跳转的范围限定在一个 scroll-view 内
* @param {Object} page scroll-view 所在页面的示例
* @param {String} selector scroll-view 的选择器
* @param {String} scrollTop scroll-view scroll-top 属性绑定的变量名
*/
in (page, selector, scrollTop) {
// #ifndef APP-PLUS-NVUE
if (page && selector && scrollTop) {
this._in = {
page,
selector,
scrollTop
}
}
// #endif
},
/**
* @description 锚点跳转
* @param {String} id 要跳转的锚点 id
* @param {Number} offset 跳转位置的偏移量
* @returns {Promise}
*/
navigateTo (id, offset) {
return new Promise((resolve, reject) => {
if (!this.useAnchor) {
reject(Error('Anchor is disabled'))
return
}
offset = offset || parseInt(this.useAnchor) || 0
// #ifdef APP-PLUS-NVUE
if (!id) {
dom.scrollToElement(this.$refs.web, {
offset
})
resolve()
} else {
this._navigateTo = {
resolve,
reject,
offset
}
this.$refs.web.evalJs('uni.postMessage({data:{action:"getOffset",offset:(document.getElementById(' + id + ')||{}).offsetTop}})')
}
// #endif
// #ifndef APP-PLUS-NVUE
let deep = ' '
// #ifdef MP-WEIXIN || MP-QQ || MP-TOUTIAO
deep = '>>>'
// #endif
const selector = uni.createSelectorQuery()
// #ifndef MP-ALIPAY
.in(this._in ? this._in.page : this)
// #endif
.select((this._in ? this._in.selector : '._root') + (id ? `${deep}#${id}` : '')).boundingClientRect()
if (this._in) {
selector.select(this._in.selector).scrollOffset()
.select(this._in.selector).boundingClientRect()
} else {
// 获取 scroll-view 的位置和滚动距离
selector.selectViewport().scrollOffset() // 获取窗口的滚动距离
}
selector.exec(res => {
if (!res[0]) {
reject(Error('Label not found'))
return
}
const scrollTop = res[1].scrollTop + res[0].top - (res[2] ? res[2].top : 0) + offset
if (this._in) {
// scroll-view 跳转
this._in.page[this._in.scrollTop] = scrollTop
} else {
// 页面跳转
uni.pageScrollTo({
scrollTop,
duration: 300
})
}
resolve()
})
// #endif
})
},
/**
* @description 获取文本内容
* @return {String}
*/
getText (nodes) {
let text = '';
(function traversal (nodes) {
for (let i = 0; i < nodes.length; i++) {
const node = nodes[i]
if (node.type === 'text') {
text += node.text.replace(/&amp;/g, '&')
} else if (node.name === 'br') {
text += '\n'
} else {
// 块级标签前后加换行
const isBlock = node.name === 'p' || node.name === 'div' || node.name === 'tr' || node.name === 'li' || (node.name[0] === 'h' && node.name[1] > '0' && node.name[1] < '7')
if (isBlock && text && text[text.length - 1] !== '\n') {
text += '\n'
}
// 递归获取子节点的文本
if (node.children) {
traversal(node.children)
}
if (isBlock && text[text.length - 1] !== '\n') {
text += '\n'
} else if (node.name === 'td' || node.name === 'th') {
text += '\t'
}
}
}
})(nodes || this.nodes)
return text
},
/**
* @description 获取内容大小和位置
* @return {Promise}
*/
getRect () {
return new Promise((resolve, reject) => {
uni.createSelectorQuery()
// #ifndef MP-ALIPAY
.in(this)
// #endif
.select('#_root').boundingClientRect().exec(res => res[0] ? resolve(res[0]) : reject(Error('Root label not found')))
})
},
/**
* @description 暂停播放媒体
*/
pauseMedia () {
for (let i = (this._videos || []).length; i--;) {
this._videos[i].pause()
}
// #ifdef APP-PLUS
const command = 'for(var e=document.getElementsByTagName("video"),i=e.length;i--;)e[i].pause()'
// #ifndef APP-PLUS-NVUE
let page = this.$parent
while (!page.$scope) page = page.$parent
page.$scope.$getAppWebview().evalJS(command)
// #endif
// #ifdef APP-PLUS-NVUE
this.$refs.web.evalJs(command)
// #endif
// #endif
},
/**
* @description 设置媒体播放速率
* @param {Number} rate 播放速率
*/
setPlaybackRate (rate) {
this.playbackRate = rate
for (let i = (this._videos || []).length; i--;) {
this._videos[i].playbackRate(rate)
}
// #ifdef APP-PLUS
const command = 'for(var e=document.getElementsByTagName("video"),i=e.length;i--;)e[i].playbackRate=' + rate
// #ifndef APP-PLUS-NVUE
let page = this.$parent
while (!page.$scope) page = page.$parent
page.$scope.$getAppWebview().evalJS(command)
// #endif
// #ifdef APP-PLUS-NVUE
this.$refs.web.evalJs(command)
// #endif
// #endif
},
/**
* @description 设置内容
* @param {String} content html 内容
* @param {Boolean} append 是否在尾部追加
*/
setContent (content, append) {
if (!append || !this.imgList) {
this.imgList = []
}
const nodes = new Parser(this).parse(content)
// #ifdef APP-PLUS-NVUE
if (this._ready) {
this._set(nodes, append)
}
// #endif
this.$set(this, 'nodes', append ? (this.nodes || []).concat(nodes) : nodes)
// #ifndef APP-PLUS-NVUE
this._videos = []
this.$nextTick(() => {
this._hook('onLoad')
this.$emit('load')
})
if (this.lazyLoad || this.imgList._unloadimgs < this.imgList.length / 2) {
// 设置懒加载,每 350ms 获取高度,不变则认为加载完毕
let height = 0
const callback = rect => {
if (!rect || !rect.height) rect = {}
// 350ms 总高度无变化就触发 ready 事件
if (rect.height === height) {
this.$emit('ready', rect)
} else {
height = rect.height
setTimeout(() => {
this.getRect().then(callback).catch(callback)
}, 350)
}
}
this.getRect().then(callback).catch(callback)
} else {
// 未设置懒加载,等待所有图片加载完毕
if (!this.imgList._unloadimgs) {
this.getRect().then(rect => {
this.$emit('ready', rect)
}).catch(() => {
this.$emit('ready', {})
})
}
}
// #endif
},
/**
* @description 调用插件钩子函数
*/
_hook (name) {
for (let i = plugins.length; i--;) {
if (this.plugins[i][name]) {
this.plugins[i][name]()
}
}
},
// #ifdef APP-PLUS-NVUE
/**
* @description 设置内容
*/
_set (nodes, append) {
this.$refs.web.evalJs('setContent(' + JSON.stringify(nodes).replace(/%22/g, '') + ',' + JSON.stringify([this.containerStyle.replace(/(?:margin|padding)[^;]+/g, ''), this.errorImg, this.loadingImg, this.pauseVideo, this.scrollTable, this.selectable]) + ',' + append + ')')
},
/**
* @description 接收到 web-view 消息
*/
_onMessage (e) {
const message = e.detail.data[0]
switch (message.action) {
// web-view 初始化完毕
case 'onJSBridgeReady':
this._ready = true
if (this.nodes) {
this._set(this.nodes)
}
break
// 内容 dom 加载完毕
case 'onLoad':
this.height = message.height
this._hook('onLoad')
this.$emit('load')
break
// 所有图片加载完毕
case 'onReady':
this.getRect().then(res => {
this.$emit('ready', res)
}).catch(() => {
this.$emit('ready', {})
})
break
// 总高度发生变化
case 'onHeightChange':
this.height = message.height
break
// 图片点击
case 'onImgTap':
this.$emit('imgtap', message.attrs)
if (this.previewImg) {
uni.previewImage({
current: parseInt(message.attrs.i),
urls: this.imgList
})
}
break
// 链接点击
case 'onLinkTap': {
const href = message.attrs.href
this.$emit('linktap', message.attrs)
if (href) {
// 锚点跳转
if (href[0] === '#') {
if (this.useAnchor) {
dom.scrollToElement(this.$refs.web, {
offset: message.offset
})
}
} else if (href.includes('://')) {
// 打开外链
if (this.copyLink) {
plus.runtime.openWeb(href)
}
} else {
uni.navigateTo({
url: href,
fail () {
uni.switchTab({
url: href
})
}
})
}
}
break
}
case 'onPlay':
this.$emit('play')
break
// 获取到锚点的偏移量
case 'getOffset':
if (typeof message.offset === 'number') {
dom.scrollToElement(this.$refs.web, {
offset: message.offset + this._navigateTo.offset
})
this._navigateTo.resolve()
} else {
this._navigateTo.reject(Error('Label not found'))
}
break
// 点击
case 'onClick':
this.$emit('tap')
this.$emit('click')
break
// 出错
case 'onError':
this.$emit('error', {
source: message.source,
attrs: message.attrs
})
}
}
// #endif
}
}
</script>
<style>
/* #ifndef APP-PLUS-NVUE */
/* 根节点样式 */
._root {
padding: 1px 0;
overflow-x: auto;
overflow-y: hidden;
-webkit-overflow-scrolling: touch;
}
/* 长按复制 */
._select {
user-select: text;
}
/* #endif */
</style>
<template>
<view :id="attrs.id" :class="'_block _'+name+' '+attrs.class" :style="attrs.style">
<block v-for="(n, i) in childs" 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" />
<!-- 显示图片 -->
<!-- #ifdef H5 || (APP-PLUS && VUE2) -->
<img v-if="n.name==='img'" :id="n.attrs.id" :class="'_img '+n.attrs.class" :style="(ctrl[i]===-1?'display:none;':'')+n.attrs.style" :src="n.attrs.src||(ctrl.load?n.attrs['data-src']:'')" :data-i="i" @load="imgLoad" @error="mediaError" @tap.stop="imgTap" @longpress="imgLongTap" />
<!-- #endif -->
<!-- #ifndef H5 || (APP-PLUS && VUE2) -->
<!-- 表格中的图片,使用 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" />
<!-- #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 -->
<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 -->
<!-- #ifdef MP-KUAISHOU -->
<image v-else-if="n.name==='img'" :id="n.attrs.id" :class="'_img '+n.attrs.class" :style="(ctrl[i]===-1?'display:none;':'')+n.attrs.style" :src="n.attrs.src" :lazy-load="opts[0]" :data-i="i" @load="imgLoad" @error="mediaError" @tap.stop="imgTap"></image>
<!-- #endif -->
<!-- #ifdef APP-PLUS && VUE3 -->
<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;'+n.attrs.style" :src="n.attrs.src||(ctrl.load?n.attrs['data-src']:'')" :mode="!n.h?'widthFix':(!n.w?'heightFix':(n.m||''))" :data-i="i" @load="imgLoad" @error="mediaError" @tap.stop="imgTap" @longpress="imgLongTap" />
<!-- #endif -->
<!-- 文本 -->
<!-- #ifdef MP-WEIXIN -->
<text v-else-if="n.text" :user-select="opts[4]=='force'&&isiOS" decode>{{n.text}}</text>
<!-- #endif -->
<!-- #ifndef MP-WEIXIN || MP-BAIDU || MP-ALIPAY || MP-TOUTIAO -->
<text v-else-if="n.text" decode>{{n.text}}</text>
<!-- #endif -->
<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">
<node name="span" :childs="n.children" :opts="opts" style="display:inherit" />
</view>
<!-- 视频 -->
<!-- #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" :data-i="i" @vplay.stop="play" />
<!-- #endif -->
<!-- #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" />
<!-- #endif -->
<!-- #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" />
<embed v-else-if="n.name==='embed'" :style="n.attrs.style" :src="n.attrs.src" />
<!-- #endif -->
<!-- #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" />
<!-- #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">
<node v-if="n.name==='li'" :childs="n.children" :opts="opts" />
<view v-else v-for="(tbody, x) in n.children" v-bind:key="x" :class="'_'+tbody.name+' '+tbody.attrs.class" :style="tbody.attrs.style">
<node v-if="tbody.name==='td'||tbody.name==='th'" :childs="tbody.children" :opts="opts" />
<block v-else v-for="(tr, y) in tbody.children" v-bind:key="y">
<view v-if="tr.name==='td'||tr.name==='th'" :class="'_'+tr.name+' '+tr.attrs.class" :style="tr.attrs.style">
<node :childs="tr.children" :opts="opts" />
</view>
<view v-else :class="'_'+tr.name+' '+tr.attrs.class" :style="tr.attrs.style">
<view v-for="(td, z) in tr.children" v-bind:key="z" :class="'_'+td.name+' '+td.attrs.class" :style="td.attrs.style">
<node :childs="td.children" :opts="opts" />
</view>
</view>
</block>
</view>
</view>
<!-- 富文本 -->
<!-- #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]" />
<!-- #endif -->
<!-- #ifndef H5 || ((MP-WEIXIN || MP-QQ || APP-PLUS || MP-360) && VUE2) -->
<rich-text v-else-if="!n.c" :id="n.attrs.id" :style="'display:inline;'+n.f" :preview="false" :selectable="opts[4]" :user-select="opts[4]" :nodes="[n]" />
<!-- #endif -->
<!-- 继续递归 -->
<view v-else-if="n.c===2" :id="n.attrs.id" :class="'_block _'+n.name+' '+n.attrs.class" :style="n.f+';'+n.attrs.style">
<node v-for="(n2, j) in n.children" v-bind:key="j" :style="n2.f" :name="n2.name" :attrs="n2.attrs" :childs="n2.children" :opts="opts" />
</view>
<node v-else :style="n.f" :name="n.name" :attrs="n.attrs" :childs="n.children" :opts="opts" />
</block>
</view>
</template>
<script module="handler" lang="wxs">
// 行内标签列表
var inlineTags = {
abbr: true,
b: true,
big: true,
code: true,
del: true,
em: true,
i: true,
ins: true,
label: true,
q: true,
small: true,
span: true,
strong: true,
sub: true,
sup: true
}
/**
* @description 判断是否为行内标签
*/
module.exports = {
isInline: function (tagName, style) {
return inlineTags[tagName] || (style || '').indexOf('display:inline') !== -1
}
}
</script>
<script>
import node from './node'
export default {
name: 'node',
options: {
// #ifdef MP-WEIXIN
virtualHost: true,
// #endif
// #ifdef MP-TOUTIAO
addGlobalClass: false
// #endif
},
data () {
return {
ctrl: {},
// #ifdef MP-WEIXIN
isiOS: uni.getSystemInfoSync().system.includes('iOS')
// #endif
}
},
props: {
name: String,
attrs: {
type: Object,
default () {
return {}
}
},
childs: Array,
opts: Array
},
components: {
// #ifndef ((H5 || APP-PLUS) && VUE3) || APP-HARMONY
node
// #endif
},
mounted () {
this.$nextTick(() => {
for (this.root = this.$parent; this.root.$options.name !== 'mp-html'; this.root = this.root.$parent);
})
// #ifdef H5 || APP-PLUS
if (this.opts[0]) {
let i
for (i = this.childs.length; i--;) {
if (this.childs[i].name === 'img') break
}
if (i !== -1) {
this.observer = uni.createIntersectionObserver(this).relativeToViewport({
top: 500,
bottom: 500
})
this.observer.observe('._img', res => {
if (res.intersectionRatio) {
this.$set(this.ctrl, 'load', 1)
this.observer.disconnect()
}
})
}
}
// #endif
},
beforeDestroy () {
// #ifdef H5 || APP-PLUS
if (this.observer) {
this.observer.disconnect()
}
// #endif
},
methods:{
// #ifdef MP-WEIXIN
toJSON () { return this },
// #endif
/**
* @description 播放视频事件
* @param {Event} e
*/
play (e) {
const i = e.currentTarget.dataset.i
const node = this.childs[i]
this.root.$emit('play', {
source: node.name,
attrs: {
...node.attrs,
src: node.src[this.ctrl[i] || 0]
}
})
// #ifndef APP-PLUS
if (this.root.pauseVideo) {
let flag = false
const id = e.target.id
for (let i = this.root._videos.length; i--;) {
if (this.root._videos[i].id === id) {
flag = true
} else {
this.root._videos[i].pause() // 自动暂停其他视频
}
}
// 将自己加入列表
if (!flag) {
const ctx = uni.createVideoContext(id
// #ifndef MP-BAIDU
, this
// #endif
)
ctx.id = id
if (this.root.playbackRate) {
ctx.playbackRate(this.root.playbackRate)
}
this.root._videos.push(ctx)
}
}
// #endif
},
/**
* @description 图片点击事件
* @param {Event} e
*/
imgTap (e) {
const node = this.childs[e.currentTarget.dataset.i]
if (node.a) {
this.linkTap(node.a)
return
}
if (node.attrs.ignore) return
// #ifdef H5 || APP-PLUS
node.attrs.src = node.attrs.src || node.attrs['data-src']
// #endif
// #ifndef APP-HARMONY
this.root.$emit('imgtap', node.attrs)
// #endif
// #ifdef APP-HARMONY
this.root.$emit('imgtap', {
...node.attrs
})
// #endif
// 自动预览图片
if (this.root.previewImg) {
uni.previewImage({
// #ifdef MP-WEIXIN
showmenu: this.root.showImgMenu,
// #endif
// #ifdef MP-ALIPAY
enablesavephoto: this.root.showImgMenu,
enableShowPhotoDownload: this.root.showImgMenu,
// #endif
current: parseInt(node.attrs.i),
urls: this.root.imgList
})
}
},
/**
* @description 图片长按
*/
imgLongTap (e) {
// #ifdef APP-PLUS
const attrs = this.childs[e.currentTarget.dataset.i].attrs
if (this.opts[3] && !attrs.ignore) {
uni.showActionSheet({
itemList: ['保存图片'],
success: () => {
const save = path => {
uni.saveImageToPhotosAlbum({
filePath: path,
success () {
uni.showToast({
title: '保存成功'
})
}
})
}
if (this.root.imgList[attrs.i].startsWith('http')) {
uni.downloadFile({
url: this.root.imgList[attrs.i],
success: res => save(res.tempFilePath)
})
} else {
save(this.root.imgList[attrs.i])
}
}
})
}
// #endif
},
/**
* @description 图片加载完成事件
* @param {Event} e
*/
imgLoad (e) {
const i = e.currentTarget.dataset.i
/* #ifndef H5 || (APP-PLUS && VUE2) */
if (!this.childs[i].w) {
// 设置原宽度
this.$set(this.ctrl, i, e.detail.width)
} else /* #endif */ if ((this.opts[1] && !this.ctrl[i]) || this.ctrl[i] === -1) {
// 加载完毕,取消加载中占位图
this.$set(this.ctrl, i, 1)
}
this.checkReady()
},
/**
* @description 检查是否所有图片加载完毕
*/
checkReady () {
if (this.root && !this.root.lazyLoad) {
this.root._unloadimgs -= 1
if (!this.root._unloadimgs) {
setTimeout(() => {
this.root.getRect().then(rect => {
this.root.$emit('ready', rect)
}).catch(() => {
this.root.$emit('ready', {})
})
}, 350)
}
}
},
/**
* @description 链接点击事件
* @param {Event} e
*/
linkTap (e) {
const node = e.currentTarget ? this.childs[e.currentTarget.dataset.i] : {}
const attrs = node.attrs || e
const href = attrs.href
this.root.$emit('linktap', Object.assign({
innerText: this.root.getText(node.children || []) // 链接内的文本内容
}, attrs))
if (href) {
if (href[0] === '#') {
// 跳转锚点
this.root.navigateTo(href.substring(1)).catch(() => { })
} else if (href.split('?')[0].includes('://')) {
// 复制外部链接
if (this.root.copyLink) {
// #ifdef H5
window.open(href)
// #endif
// #ifdef MP
uni.setClipboardData({
data: href,
success: () =>
uni.showToast({
title: '链接已复制'
})
})
// #endif
// #ifdef APP-PLUS
plus.runtime.openWeb(href)
// #endif
}
} else {
// 跳转页面
uni.navigateTo({
url: href,
fail () {
uni.switchTab({
url: href,
fail () { }
})
}
})
}
}
},
/**
* @description 错误事件
* @param {Event} e
*/
mediaError (e) {
const i = e.currentTarget.dataset.i
const node = this.childs[i]
// 加载其他源
if (node.name === 'video' || node.name === 'audio') {
let index = (this.ctrl[i] || 0) + 1
if (index > node.src.length) {
index = 0
}
if (index < node.src.length) {
this.$set(this.ctrl, i, index)
return
}
} else if (node.name === 'img') {
// #ifdef H5 && VUE3
if (this.opts[0] && !this.ctrl.load) return
// #endif
// 显示错误占位图
if (this.opts[2]) {
this.$set(this.ctrl, i, -1)
}
this.checkReady()
}
if (this.root) {
this.root.$emit('error', {
source: node.name,
attrs: node.attrs,
// #ifndef H5 && VUE3
errMsg: e.detail.errMsg
// #endif
})
}
}
}
}
</script>
<style>
/* a 标签默认效果 */
._a {
padding: 1.5px 0 1.5px 0;
color: #366092;
word-break: break-all;
}
/* a 标签点击态效果 */
._hover {
text-decoration: underline;
opacity: 0.7;
}
/* 图片默认效果 */
._img {
max-width: 100%;
-webkit-touch-callout: none;
}
/* 内部样式 */
._block {
display: block;
}
._b,
._strong {
font-weight: bold;
}
._code {
font-family: monospace;
}
._del {
text-decoration: line-through;
}
._em,
._i {
font-style: italic;
}
._h1 {
font-size: 2em;
}
._h2 {
font-size: 1.5em;
}
._h3 {
font-size: 1.17em;
}
._h5 {
font-size: 0.83em;
}
._h6 {
font-size: 0.67em;
}
._h1,
._h2,
._h3,
._h4,
._h5,
._h6 {
display: block;
font-weight: bold;
}
._image {
height: 1px;
}
._ins {
text-decoration: underline;
}
._li {
display: list-item;
}
._ol {
list-style-type: decimal;
}
._ol,
._ul {
display: block;
padding-left: 40px;
margin: 1em 0;
}
._q::before {
content: '"';
}
._q::after {
content: '"';
}
._sub {
font-size: smaller;
vertical-align: sub;
}
._sup {
font-size: smaller;
vertical-align: super;
}
._thead,
._tbody,
._tfoot {
display: table-row-group;
}
._tr {
display: table-row;
}
._td,
._th {
display: table-cell;
vertical-align: middle;
}
._th {
font-weight: bold;
text-align: center;
}
._ul {
list-style-type: disc;
}
._ul ._ul {
margin: 0;
list-style-type: circle;
}
._ul ._ul ._ul {
list-style-type: square;
}
._abbr,
._b,
._code,
._del,
._em,
._i,
._ins,
._label,
._q,
._span,
._strong,
._sub,
._sup {
display: inline;
}
/* #ifdef APP-PLUS */
._video {
width: 300px;
height: 225px;
}
/* #endif */
</style>
\ No newline at end of file
/**
* @fileoverview html 解析器
*/
// 配置
const config = {
// 信任的标签(保持标签名不变)
trustTags: makeMap('a,abbr,ad,audio,b,blockquote,br,code,col,colgroup,dd,del,dl,dt,div,em,fieldset,h1,h2,h3,h4,h5,h6,hr,i,img,ins,label,legend,li,ol,p,q,ruby,rt,source,span,strong,sub,sup,table,tbody,td,tfoot,th,thead,tr,title,ul,video'),
// 块级标签(转为 div,其他的非信任标签转为 span)
blockTags: makeMap('address,article,aside,body,caption,center,cite,footer,header,html,nav,pre,section'),
// #ifdef (MP-WEIXIN || MP-QQ || APP-PLUS || MP-360) && VUE3
// 行内标签
inlineTags: makeMap('abbr,b,big,code,del,em,i,ins,label,q,small,span,strong,sub,sup'),
// #endif
// 要移除的标签
ignoreTags: makeMap('area,base,canvas,embed,frame,head,iframe,input,link,map,meta,param,rp,script,source,style,textarea,title,track,wbr'),
// 自闭合的标签
voidTags: makeMap('area,base,br,col,circle,ellipse,embed,frame,hr,img,input,line,link,meta,param,path,polygon,rect,source,track,use,wbr'),
// html 实体
entities: {
lt: '<',
gt: '>',
quot: '"',
apos: "'",
ensp: '\u2002',
emsp: '\u2003',
nbsp: '\xA0',
semi: ';',
ndash: '–',
mdash: '—',
middot: '·',
lsquo: '‘',
rsquo: '’',
ldquo: '“',
rdquo: '”',
bull: '•',
hellip: '…',
larr: '←',
uarr: '↑',
rarr: '→',
darr: '↓'
},
// 默认的标签样式
tagStyle: {
// #ifndef APP-PLUS-NVUE
address: 'font-style:italic',
big: 'display:inline;font-size:1.2em',
caption: 'display:table-caption;text-align:center',
center: 'text-align:center',
cite: 'font-style:italic',
dd: 'margin-left:40px',
mark: 'background-color:yellow',
pre: 'font-family:monospace;white-space:pre',
s: 'text-decoration:line-through',
small: 'display:inline;font-size:0.8em',
strike: 'text-decoration:line-through',
u: 'text-decoration:underline'
// #endif
},
// svg 大小写对照表
svgDict: {
animatetransform: 'animateTransform',
lineargradient: 'linearGradient',
viewbox: 'viewBox',
attributename: 'attributeName',
repeatcount: 'repeatCount',
repeatdur: 'repeatDur',
foreignobject: 'foreignObject'
}
}
const tagSelector={}
let windowWidth, system
// #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
system = systemInfo.system
}
// #endif
const blankChar = makeMap(' ,\r,\n,\t,\f')
let idIndex = 0
// #ifdef H5 || APP-PLUS
config.ignoreTags.iframe = undefined
config.trustTags.iframe = true
config.ignoreTags.embed = undefined
config.trustTags.embed = true
// #endif
// #ifdef APP-PLUS-NVUE
config.ignoreTags.source = undefined
config.ignoreTags.style = undefined
// #endif
/**
* @description 创建 map
* @param {String} str 逗号分隔
*/
function makeMap (str) {
const map = Object.create(null)
const list = str.split(',')
for (let i = list.length; i--;) {
map[list[i]] = true
}
return map
}
/**
* @description 解码 html 实体
* @param {String} str 要解码的字符串
* @param {Boolean} amp 要不要解码 &amp;
* @returns {String} 解码后的字符串
*/
function decodeEntity (str, amp) {
let i = str.indexOf('&')
while (i !== -1) {
const j = str.indexOf(';', i + 3)
let code
if (j === -1) break
if (str[i + 1] === '#') {
// &#123; 形式的实体
code = parseInt((str[i + 2] === 'x' ? '0' : '') + str.substring(i + 2, j))
if (!isNaN(code)) {
str = str.substr(0, i) + String.fromCharCode(code) + str.substr(j + 1)
}
} else {
// &nbsp; 形式的实体
code = str.substring(i + 1, j)
if (config.entities[code] || (code === 'amp' && amp)) {
str = str.substr(0, i) + (config.entities[code] || '&') + str.substr(j + 1)
}
}
i = str.indexOf('&', i + 1)
}
return str
}
/**
* @description 合并多个块级标签,加快长内容渲染
* @param {Array} nodes 要合并的标签数组
*/
function mergeNodes (nodes) {
let i = nodes.length - 1
for (let j = i; j >= -1; j--) {
if (j === -1 || nodes[j].c || !nodes[j].name || (nodes[j].name !== 'div' && nodes[j].name !== 'p' && nodes[j].name[0] !== 'h') || (nodes[j].attrs.style || '').includes('inline')) {
if (i - j >= 5) {
nodes.splice(j + 1, i - j, {
name: 'div',
attrs: {},
children: nodes.slice(j + 1, i + 1)
})
}
i = j - 1
}
}
}
/**
* @description html 解析器
* @param {Object} vm 组件实例
*/
function Parser (vm) {
this.options = vm || {}
this.tagStyle = Object.assign({}, config.tagStyle, this.options.tagStyle)
this.imgList = vm.imgList || []
this.imgList._unloadimgs = 0
this.plugins = vm.plugins || []
this.attrs = Object.create(null)
this.stack = []
this.nodes = []
this.pre = (this.options.containerStyle || '').includes('white-space') && this.options.containerStyle.includes('pre') ? 2 : 0
}
/**
* @description 执行解析
* @param {String} content 要解析的文本
*/
Parser.prototype.parse = function (content) {
// 插件处理
for (let i = this.plugins.length; i--;) {
if (this.plugins[i].onUpdate) {
content = this.plugins[i].onUpdate(content, config) || content
}
}
new Lexer(this).parse(content)
// 出栈未闭合的标签
while (this.stack.length) {
this.popNode()
}
if (this.nodes.length > 50) {
mergeNodes(this.nodes)
}
return this.nodes
}
/**
* @description 将标签暴露出来(不被 rich-text 包含)
*/
Parser.prototype.expose = function () {
// #ifndef APP-PLUS-NVUE
for (let i = this.stack.length; i--;) {
const item = this.stack[i]
if (item.c || item.name === 'a' || item.name === 'video' || item.name === 'audio') return
item.c = 1
}
// #endif
}
/**
* @description 处理插件
* @param {Object} node 要处理的标签
* @returns {Boolean} 是否要移除此标签
*/
Parser.prototype.hook = function (node) {
for (let i = this.plugins.length; i--;) {
if (this.plugins[i].onParse && this.plugins[i].onParse(node, this) === false) {
return false
}
}
return true
}
/**
* @description 将链接拼接上主域名
* @param {String} url 需要拼接的链接
* @returns {String} 拼接后的链接
*/
Parser.prototype.getUrl = function (url) {
const domain = this.options.domain
if (url[0] === '/') {
if (url[1] === '/') {
// // 开头的补充协议名
url = (domain ? domain.split('://')[0] : 'http') + ':' + url
} else if (domain) {
// 否则补充整个域名
url = domain + url
} /* #ifdef APP-PLUS */ else {
url = plus.io.convertLocalFileSystemURL(url)
} /* #endif */
} else if (!url.includes('data:') && !url.includes('://')) {
if (domain) {
url = domain + '/' + url
} /* #ifdef APP-PLUS */ else {
url = plus.io.convertLocalFileSystemURL(url)
} /* #endif */
}
return url
}
/**
* @description 解析样式表
* @param {Object} node 标签
* @returns {Object}
*/
Parser.prototype.parseStyle = function (node) {
const attrs = node.attrs
const list = (this.tagStyle[node.name] || '').split(';').concat((attrs.style || '').split(';'))
const styleObj = {}
let tmp = ''
if (attrs.id && !this.xml) {
// 暴露锚点
if (this.options.useAnchor) {
this.expose()
} else if (node.name !== 'img' && node.name !== 'a' && node.name !== 'video' && node.name !== 'audio') {
attrs.id = undefined
}
}
// 转换 width 和 height 属性
if (attrs.width) {
styleObj.width = parseFloat(attrs.width) + (attrs.width.includes('%') ? '%' : 'px')
attrs.width = undefined
}
if (attrs.height) {
styleObj.height = parseFloat(attrs.height) + (attrs.height.includes('%') ? '%' : 'px')
attrs.height = undefined
}
for (let i = 0, len = list.length; i < len; i++) {
const info = list[i].split(':')
if (info.length < 2) continue
const key = info.shift().trim().toLowerCase()
let value = info.join(':').trim()
if ((value[0] === '-' && value.lastIndexOf('-') > 0) || value.includes('safe')) {
// 兼容性的 css 不压缩
tmp += `;${key}:${value}`
} else if (!styleObj[key] || value.includes('import') || !styleObj[key].includes('import')) {
// 重复的样式进行覆盖
if (value.includes('url')) {
// 填充链接
let j = value.indexOf('(') + 1
if (j) {
while (value[j] === '"' || value[j] === "'" || blankChar[value[j]]) {
j++
}
value = value.substr(0, j) + this.getUrl(value.substr(j))
}
} else if (value.includes('rpx')) {
// 转换 rpx(rich-text 内部不支持 rpx)
value = value.replace(/[0-9.]+\s*rpx/g, $ => parseFloat($) * windowWidth / 750 + 'px')
}
styleObj[key] = value
}
}
node.attrs.style = tmp
return styleObj
}
/**
* @description 解析到标签名
* @param {String} name 标签名
* @private
*/
Parser.prototype.onTagName = function (name) {
this.tagName = this.xml ? name : name.toLowerCase()
if (this.tagName === 'svg') {
this.xml = (this.xml || 0) + 1 // svg 标签内大小写敏感
config.ignoreTags.style = undefined // svg 标签内 style 可用
}
}
/**
* @description 解析到属性名
* @param {String} name 属性名
* @private
*/
Parser.prototype.onAttrName = function (name) {
name = this.xml ? name : name.toLowerCase()
// #ifdef (VUE3 && (H5 || APP-PLUS)) || APP-PLUS-NVUE
if (name.includes('?') || name.includes(';')) {
this.attrName = undefined
return
}
// #endif
if (name.substr(0, 5) === 'data-') {
if (name === 'data-src' && !this.attrs.src) {
// data-src 自动转为 src
this.attrName = 'src'
} else if (this.tagName === 'img' || this.tagName === 'a') {
// a 和 img 标签保留 data- 的属性,可以在 imgtap 和 linktap 事件中使用
this.attrName = name
} else {
// 剩余的移除以减小大小
this.attrName = undefined
}
} else {
this.attrName = name
this.attrs[name] = 'T' // boolean 型属性缺省设置
}
}
/**
* @description 解析到属性值
* @param {String} val 属性值
* @private
*/
Parser.prototype.onAttrVal = function (val) {
const name = this.attrName || ''
if (name === 'style' || name === 'href') {
// 部分属性进行实体解码
this.attrs[name] = decodeEntity(val, true)
} else if (name.includes('src')) {
// 拼接主域名
this.attrs[name] = this.getUrl(decodeEntity(val, true))
} else if (name) {
this.attrs[name] = val
}
}
/**
* @description 解析到标签开始
* @param {Boolean} selfClose 是否有自闭合标识 />
* @private
*/
Parser.prototype.onOpenTag = function (selfClose) {
// 拼装 node
const node = Object.create(null)
node.name = this.tagName
node.attrs = this.attrs
// 避免因为自动 diff 使得 type 被设置为 null 导致部分内容不显示
if (this.options.nodes.length) {
node.type = 'node'
}
this.attrs = Object.create(null)
const attrs = node.attrs
const parent = this.stack[this.stack.length - 1]
const siblings = parent ? parent.children : this.nodes
const close = this.xml ? selfClose : config.voidTags[node.name]
// 替换标签名选择器
if (tagSelector[node.name]) {
attrs.class = tagSelector[node.name] + (attrs.class ? ' ' + attrs.class : '')
}
// 转换 embed 标签
if (node.name === 'embed') {
// #ifndef H5 || APP-PLUS
const src = attrs.src || ''
// 按照后缀名和 type 将 embed 转为 video 或 audio
if (src.includes('.mp4') || src.includes('.3gp') || src.includes('.m3u8') || (attrs.type || '').includes('video')) {
node.name = 'video'
} else if (src.includes('.mp3') || src.includes('.wav') || src.includes('.aac') || src.includes('.m4a') || (attrs.type || '').includes('audio')) {
node.name = 'audio'
}
if (attrs.autostart) {
attrs.autoplay = 'T'
}
attrs.controls = 'T'
// #endif
// #ifdef H5 || APP-PLUS
this.expose()
// #endif
}
// #ifndef APP-PLUS-NVUE
// 处理音视频
if (node.name === 'video' || node.name === 'audio') {
// 设置 id 以便获取 context
if (node.name === 'video' && !attrs.id) {
attrs.id = 'v' + idIndex++
}
// 没有设置 controls 也没有设置 autoplay 的自动设置 controls
if (!attrs.controls && !attrs.autoplay) {
attrs.controls = 'T'
}
// 用数组存储所有可用的 source
node.src = []
if (attrs.src) {
node.src.push(attrs.src)
attrs.src = undefined
}
this.expose()
}
// #endif
// 处理自闭合标签
if (close) {
if (!this.hook(node) || config.ignoreTags[node.name]) {
// 通过 base 标签设置主域名
if (node.name === 'base' && !this.options.domain) {
this.options.domain = attrs.href
} /* #ifndef APP-PLUS-NVUE */ else if (node.name === 'source' && parent && (parent.name === 'video' || parent.name === 'audio') && attrs.src) {
// 设置 source 标签(仅父节点为 video 或 audio 时有效)
parent.src.push(attrs.src)
} /* #endif */
return
}
// 解析 style
const styleObj = this.parseStyle(node)
// 处理图片
if (node.name === 'img') {
if (attrs.src) {
// 标记 webp
if (attrs.src.includes('webp')) {
node.webp = 'T'
}
// data url 图片如果没有设置 original-src 默认为不可预览的小图片
if (attrs.src.includes('data:') && this.options.previewImg !== 'all' && !attrs['original-src']) {
attrs.ignore = 'T'
}
if (!attrs.ignore || node.webp || attrs.src.includes('cloud://')) {
for (let i = this.stack.length; i--;) {
const item = this.stack[i]
if (item.name === 'a') {
node.a = item.attrs
}
if (item.name === 'table' && !node.webp && !attrs.src.includes('cloud://')) {
if (!styleObj.display || styleObj.display.includes('inline')) {
node.t = 'inline-block'
} else {
node.t = styleObj.display
}
styleObj.display = undefined
}
// #ifndef H5 || APP-PLUS
const style = item.attrs.style || ''
if (style.includes('flex:') && !style.includes('flex:0') && !style.includes('flex: 0') && (!styleObj.width || parseInt(styleObj.width) > 100)) {
styleObj.width = '100% !important'
styleObj.height = ''
for (let j = i + 1; j < this.stack.length; j++) {
this.stack[j].attrs.style = (this.stack[j].attrs.style || '').replace('inline-', '')
}
} else if (style.includes('flex') && styleObj.width === '100%') {
for (let j = i + 1; j < this.stack.length; j++) {
const style = this.stack[j].attrs.style || ''
if (!style.includes(';width') && !style.includes(' width') && style.indexOf('width') !== 0) {
styleObj.width = ''
break
}
}
} else if (style.includes('inline-block')) {
if (styleObj.width && styleObj.width[styleObj.width.length - 1] === '%') {
item.attrs.style += ';max-width:' + styleObj.width
styleObj.width = ''
} else {
item.attrs.style += ';max-width:100%'
}
}
// #endif
item.c = 1
}
attrs.i = this.imgList.length.toString()
let src = attrs['original-src'] || attrs.src
// #ifndef H5 || MP-ALIPAY || APP-PLUS || MP-360
if (this.imgList.includes(src)) {
// 如果有重复的链接则对域名进行随机大小写变换避免预览时错位
let i = src.indexOf('://')
if (i !== -1) {
i += 3
let newSrc = src.substr(0, i)
for (; i < src.length; i++) {
if (src[i] === '/') break
newSrc += Math.random() > 0.5 ? src[i].toUpperCase() : src[i]
}
newSrc += src.substr(i)
src = newSrc
}
}
// #endif
this.imgList.push(src)
if (!node.t) {
this.imgList._unloadimgs += 1
}
// #ifdef H5 || APP-PLUS
if (this.options.lazyLoad) {
attrs['data-src'] = attrs.src
attrs.src = undefined
}
// #endif
}
}
if (styleObj.display === 'inline') {
styleObj.display = ''
}
// #ifndef APP-PLUS-NVUE
if (attrs.ignore) {
styleObj['max-width'] = styleObj['max-width'] || '100%'
attrs.style += ';-webkit-touch-callout:none'
}
// #endif
// 设置的宽度超出屏幕,为避免变形,高度转为自动
if (parseInt(styleObj.width) > windowWidth) {
styleObj.height = undefined
}
// 记录是否设置了宽高
if (!isNaN(parseInt(styleObj.width))) {
node.w = 'T'
}
if (!isNaN(parseInt(styleObj.height)) && (!styleObj.height.includes('%') || (parent && (parent.attrs.style || '').includes('height')))) {
node.h = 'T'
}
if (node.w && node.h && styleObj['object-fit']) {
if (styleObj['object-fit'] === 'contain') {
node.m = 'aspectFit'
} else if (styleObj['object-fit'] === 'cover') {
node.m = 'aspectFill'
}
}
} else if (node.name === 'svg') {
siblings.push(node)
this.stack.push(node)
this.popNode()
return
}
for (const key in styleObj) {
if (styleObj[key]) {
attrs.style += `;${key}:${styleObj[key].replace(' !important', '')}`
}
}
attrs.style = attrs.style.substr(1) || undefined
// #ifdef (MP-WEIXIN || MP-QQ) && VUE3
if (!attrs.style) {
delete attrs.style
}
// #endif
} else {
if ((node.name === 'pre' || ((attrs.style || '').includes('white-space') && attrs.style.includes('pre'))) && this.pre !== 2) {
this.pre = node.pre = 1
}
node.children = []
this.stack.push(node)
}
// 加入节点树
siblings.push(node)
}
/**
* @description 解析到标签结束
* @param {String} name 标签名
* @private
*/
Parser.prototype.onCloseTag = function (name) {
// 依次出栈到匹配为止
name = this.xml ? name : name.toLowerCase()
let i
for (i = this.stack.length; i--;) {
if (this.stack[i].name === name) break
}
if (i !== -1) {
while (this.stack.length > i) {
this.popNode()
}
} else if (name === 'p' || name === 'br') {
const siblings = this.stack.length ? this.stack[this.stack.length - 1].children : this.nodes
siblings.push({
name,
attrs: {
class: tagSelector[name] || '',
style: this.tagStyle[name] || ''
}
})
}
}
/**
* @description 处理标签出栈
* @private
*/
Parser.prototype.popNode = function () {
const node = this.stack.pop()
let attrs = node.attrs
const children = node.children
const parent = this.stack[this.stack.length - 1]
const siblings = parent ? parent.children : this.nodes
if (!this.hook(node) || config.ignoreTags[node.name]) {
// 获取标题
if (node.name === 'title' && children.length && children[0].type === 'text' && this.options.setTitle) {
uni.setNavigationBarTitle({
title: children[0].text
})
}
siblings.pop()
return
}
if (node.pre && this.pre !== 2) {
// 是否合并空白符标识
this.pre = node.pre = undefined
for (let i = this.stack.length; i--;) {
if (this.stack[i].pre) {
this.pre = 1
}
}
}
const styleObj = {}
// 转换 svg
if (node.name === 'svg') {
if (this.xml > 1) {
// 多层 svg 嵌套
this.xml--
return
}
// #ifdef APP-PLUS-NVUE
(function traversal (node) {
if (node.name) {
// 调整 svg 的大小写
node.name = config.svgDict[node.name] || node.name
for (const item in node.attrs) {
if (config.svgDict[item]) {
node.attrs[config.svgDict[item]] = node.attrs[item]
node.attrs[item] = undefined
}
}
for (let i = 0; i < (node.children || []).length; i++) {
traversal(node.children[i])
}
}
})(node)
// #endif
// #ifndef APP-PLUS-NVUE
let src = ''
const style = attrs.style
attrs.style = ''
attrs.xmlns = 'http://www.w3.org/2000/svg';
(function traversal (node) {
if (node.type === 'text') {
src += node.text
return
}
const name = config.svgDict[node.name] || node.name
if (name === 'foreignObject') {
for (const child of (node.children || [])) {
if (child.attrs && !child.attrs.xmlns) {
child.attrs.xmlns = 'http://www.w3.org/1999/xhtml'
break
}
}
}
src += '<' + name
for (const item in node.attrs) {
const val = node.attrs[item]
if (val) {
src += ` ${config.svgDict[item] || item}="${val.replace(/"/g, '')}"`
}
}
if (!node.children) {
src += '/>'
} else {
src += '>'
for (let i = 0; i < node.children.length; i++) {
traversal(node.children[i])
}
src += '</' + name + '>'
}
})(node)
node.name = 'img'
node.attrs = {
src: 'data:image/svg+xml;utf8,' + src.replace(/#/g, '%23'),
style,
ignore: 'T'
}
node.children = undefined
// #endif
this.xml = false
config.ignoreTags.style = true
return
}
// #ifndef APP-PLUS-NVUE
// 转换 align 属性
if (attrs.align) {
if (node.name === 'table') {
if (attrs.align === 'center') {
styleObj['margin-inline-start'] = styleObj['margin-inline-end'] = 'auto'
} else {
styleObj.float = attrs.align
}
} else {
styleObj['text-align'] = attrs.align
}
attrs.align = undefined
}
// 转换 dir 属性
if (attrs.dir) {
styleObj.direction = attrs.dir
attrs.dir = undefined
}
// 转换 font 标签的属性
if (node.name === 'font') {
if (attrs.color) {
styleObj.color = attrs.color
attrs.color = undefined
}
if (attrs.face) {
styleObj['font-family'] = attrs.face
attrs.face = undefined
}
if (attrs.size) {
let size = parseInt(attrs.size)
if (!isNaN(size)) {
if (size < 1) {
size = 1
} else if (size > 7) {
size = 7
}
styleObj['font-size'] = ['x-small', 'small', 'medium', 'large', 'x-large', 'xx-large', 'xxx-large'][size - 1]
}
attrs.size = undefined
}
}
// #endif
// 一些编辑器的自带 class
if ((attrs.class || '').includes('align-center')) {
styleObj['text-align'] = 'center'
}
Object.assign(styleObj, this.parseStyle(node))
if (node.name !== 'table' && parseInt(styleObj.width) > windowWidth) {
styleObj['max-width'] = '100%'
styleObj['box-sizing'] = 'border-box'
}
// #ifndef APP-PLUS-NVUE
if (config.blockTags[node.name]) {
node.name = 'div'
} else if (!config.trustTags[node.name] && !this.xml) {
// 未知标签转为 span,避免无法显示
node.name = 'span'
}
if (node.name === 'a' || node.name === 'ad'
// #ifdef H5 || APP-PLUS
|| node.name === 'iframe' // eslint-disable-line
// #endif
) {
this.expose()
} else if (node.name === 'video') {
if ((styleObj.height || '').includes('auto')) {
styleObj.height = undefined
}
/* #ifdef APP-PLUS */
let str = '<video style="width:100%;height:100%"'
for (const item in attrs) {
if (attrs[item]) {
str += ' ' + item + '="' + attrs[item] + '"'
}
}
if (this.options.pauseVideo) {
str += ' onplay="this.dispatchEvent(new CustomEvent(\'vplay\',{bubbles:!0}));for(var e=document.getElementsByTagName(\'video\'),t=0;t<e.length;t++)e[t]!=this&&e[t].pause()"'
}
str += '>'
for (let i = 0; i < node.src.length; i++) {
str += '<source src="' + node.src[i] + '">'
}
str += '</video>'
node.html = str
/* #endif */
} else if ((node.name === 'ul' || node.name === 'ol') && node.c) {
// 列表处理
const types = {
a: 'lower-alpha',
A: 'upper-alpha',
i: 'lower-roman',
I: 'upper-roman'
}
if (types[attrs.type]) {
attrs.style += ';list-style-type:' + types[attrs.type]
attrs.type = undefined
}
for (let i = children.length; i--;) {
if (children[i].name === 'li') {
children[i].c = 1
}
}
} else if (node.name === 'table') {
// 表格处理
// cellpadding、cellspacing、border 这几个常用表格属性需要通过转换实现
let padding = parseFloat(attrs.cellpadding)
let spacing = parseFloat(attrs.cellspacing)
const border = parseFloat(attrs.border)
const bordercolor = styleObj['border-color']
const borderstyle = styleObj['border-style']
if (node.c) {
// padding 和 spacing 默认 2
if (isNaN(padding)) {
padding = 2
}
if (isNaN(spacing)) {
spacing = 2
}
}
if (border) {
attrs.style += `;border:${border}px ${borderstyle || 'solid'} ${bordercolor || 'gray'}`
}
if (node.flag && node.c) {
// 有 colspan 或 rowspan 且含有链接的表格通过 grid 布局实现
styleObj.display = 'grid'
if (styleObj['border-collapse'] === 'collapse') {
styleObj['border-collapse'] = undefined
spacing = 0
}
if (spacing) {
styleObj['grid-gap'] = spacing + 'px'
styleObj.padding = spacing + 'px'
} else if (border) {
// 无间隔的情况下避免边框重叠
attrs.style += ';border-left:0;border-top:0'
}
const width = [] // 表格的列宽
const trList = [] // tr 列表
const cells = [] // 保存新的单元格
const map = {}; // 被合并单元格占用的格子
(function traversal (nodes) {
for (let i = 0; i < nodes.length; i++) {
if (nodes[i].name === 'tr') {
trList.push(nodes[i])
} else if (nodes[i].name === 'colgroup') {
let colI = 1
for (const col of (nodes[i].children || [])) {
if (col.name === 'col') {
const style = col.attrs.style || ''
const start = style.indexOf('width') ? style.indexOf(';width') : 0
// 提取出宽度
if (start !== -1) {
let end = style.indexOf(';', start + 6)
if (end === -1) {
end = style.length
}
width[colI] = style.substring(start ? start + 7 : 6, end)
}
colI += 1
}
}
} else {
traversal(nodes[i].children || [])
}
}
})(children)
for (let row = 1; row <= trList.length; row++) {
let col = 1
for (let j = 0; j < trList[row - 1].children.length; j++) {
const td = trList[row - 1].children[j]
if (td.name === 'td' || td.name === 'th') {
// 这个格子被上面的单元格占用,则列号++
while (map[row + '.' + col]) {
col++
}
let style = td.attrs.style || ''
let start = style.indexOf('width') ? style.indexOf(';width') : 0
// 提取出 td 的宽度
if (start !== -1) {
let end = style.indexOf(';', start + 6)
if (end === -1) {
end = style.length
}
if (!td.attrs.colspan) {
width[col] = style.substring(start ? start + 7 : 6, end)
}
style = style.substr(0, start) + style.substr(end)
}
// 设置竖直对齐
style += ';display:flex'
start = style.indexOf('vertical-align')
if (start !== -1) {
const val = style.substr(start + 15, 10)
if (val.includes('middle')) {
style += ';align-items:center'
} else if (val.includes('bottom')) {
style += ';align-items:flex-end'
}
} else {
style += ';align-items:center'
}
// 设置水平对齐
start = style.indexOf('text-align')
if (start !== -1) {
const val = style.substr(start + 11, 10)
if (val.includes('center')) {
style += ';justify-content: center'
} else if (val.includes('right')) {
style += ';justify-content: right'
}
}
style = (border ? `;border:${border}px ${borderstyle || 'solid'} ${bordercolor || 'gray'}` + (spacing ? '' : ';border-right:0;border-bottom:0') : '') + (padding ? `;padding:${padding}px` : '') + ';' + style
// 处理列合并
if (td.attrs.colspan) {
style += `;grid-column-start:${col};grid-column-end:${col + parseInt(td.attrs.colspan)}`
if (!td.attrs.rowspan) {
style += `;grid-row-start:${row};grid-row-end:${row + 1}`
}
col += parseInt(td.attrs.colspan) - 1
}
// 处理行合并
if (td.attrs.rowspan) {
style += `;grid-row-start:${row};grid-row-end:${row + parseInt(td.attrs.rowspan)}`
if (!td.attrs.colspan) {
style += `;grid-column-start:${col};grid-column-end:${col + 1}`
}
// 记录下方单元格被占用
for (let rowspan = 1; rowspan < td.attrs.rowspan; rowspan++) {
for (let colspan = 0; colspan < (td.attrs.colspan || 1); colspan++) {
map[(row + rowspan) + '.' + (col - colspan)] = 1
}
}
}
if (style) {
td.attrs.style = style
}
cells.push(td)
col++
}
}
if (row === 1) {
let temp = ''
for (let i = 1; i < col; i++) {
temp += (width[i] ? width[i] : 'auto') + ' '
}
styleObj['grid-template-columns'] = temp
}
}
node.children = cells
} else {
// 没有使用合并单元格的表格通过 table 布局实现
if (node.c) {
styleObj.display = 'table'
}
if (!isNaN(spacing)) {
styleObj['border-spacing'] = spacing + 'px'
}
if (border || padding) {
// 遍历
(function traversal (nodes) {
for (let i = 0; i < nodes.length; i++) {
const td = nodes[i]
if (td.name === 'th' || td.name === 'td') {
if (border) {
td.attrs.style = `border:${border}px ${borderstyle || 'solid'} ${bordercolor || 'gray'};${td.attrs.style || ''}`
}
if (padding) {
td.attrs.style = `padding:${padding}px;${td.attrs.style || ''}`
}
} else if (td.children) {
traversal(td.children)
}
}
})(children)
}
}
// 给表格添加一个单独的横向滚动层
if (this.options.scrollTable && !(attrs.style || '').includes('inline')) {
const table = Object.assign({}, node)
node.name = 'div'
node.attrs = {
style: 'overflow:auto'
}
node.children = [table]
attrs = table.attrs
}
} else if ((node.name === 'tbody' || node.name === 'tr') && node.flag && node.c) {
node.flag = undefined;
(function traversal (nodes) {
for (let i = 0; i < nodes.length; i++) {
if (nodes[i].name === 'td') {
// 颜色样式设置给单元格避免丢失
for (const style of ['color', 'background', 'background-color']) {
if (styleObj[style]) {
nodes[i].attrs.style = style + ':' + styleObj[style] + ';' + (nodes[i].attrs.style || '')
}
}
} else {
traversal(nodes[i].children || [])
}
}
})(children)
} else if ((node.name === 'td' || node.name === 'th') && (attrs.colspan || attrs.rowspan)) {
for (let i = this.stack.length; i--;) {
if (this.stack[i].name === 'table' || this.stack[i].name === 'tbody' || this.stack[i].name === 'tr') {
this.stack[i].flag = 1 // 指示含有合并单元格
}
}
} else if (node.name === 'ruby') {
// 转换 ruby
node.name = 'span'
for (let i = 0; i < children.length - 1; i++) {
if (children[i].type === 'text' && children[i + 1].name === 'rt') {
children[i] = {
name: 'div',
attrs: {
style: 'display:inline-block;text-align:center'
},
children: [{
name: 'div',
attrs: {
style: 'font-size:50%;' + (children[i + 1].attrs.style || '')
},
children: children[i + 1].children
}, children[i]]
}
children.splice(i + 1, 1)
}
}
} else if (node.c) {
(function traversal (node) {
node.c = 2
for (let i = node.children.length; i--;) {
const child = node.children[i]
// #ifdef (MP-WEIXIN || MP-QQ || APP-PLUS || MP-360) && VUE3
if (child.name && (config.inlineTags[child.name] || ((child.attrs.style || '').includes('inline') && child.children)) && !child.c) {
traversal(child)
}
// #endif
if (!child.c || child.name === 'table') {
node.c = 1
}
}
})(node)
}
if ((styleObj.display || '').includes('flex') && !node.c) {
for (let i = children.length; i--;) {
const item = children[i]
if (item.f) {
item.attrs.style = (item.attrs.style || '') + item.f
item.f = undefined
}
}
}
// flex 布局时部分样式需要提取到 rich-text 外层
const flex = parent && ((parent.attrs.style || '').includes('flex') || (parent.attrs.style || '').includes('grid'))
// #ifdef MP-WEIXIN
// 检查基础库版本 virtualHost 是否可用
&& !(node.c && wx.getNFCAdapter) // eslint-disable-line
// #endif
// #ifndef MP-WEIXIN || MP-QQ || MP-BAIDU || MP-TOUTIAO
&& !node.c // eslint-disable-line
// #endif
if (flex) {
node.f = ';max-width:100%'
}
if (children.length >= 50 && node.c && !(styleObj.display || '').includes('flex')) {
mergeNodes(children)
}
// #endif
for (const key in styleObj) {
if (styleObj[key]) {
const val = `;${key}:${styleObj[key].replace(' !important', '')}`
/* #ifndef APP-PLUS-NVUE */
if (flex && ((key.includes('flex') && key !== 'flex-direction') || key === 'align-self' || key.includes('grid') || styleObj[key][0] === '-' || (key.includes('width') && val.includes('%')))) {
node.f += val
if (key === 'width') {
attrs.style += ';width:100%'
}
} else /* #endif */ {
attrs.style += val
}
}
}
attrs.style = attrs.style.substr(1) || undefined
// #ifdef (MP-WEIXIN || MP-QQ) && VUE3
for (const key in attrs) {
if (!attrs[key]) {
delete attrs[key]
}
}
// #endif
}
/**
* @description 解析到文本
* @param {String} text 文本内容
*/
Parser.prototype.onText = function (text) {
if (!this.pre) {
// 合并空白符
let trim = ''
let flag
for (let i = 0, len = text.length; i < len; i++) {
if (!blankChar[text[i]]) {
trim += text[i]
} else {
if (trim[trim.length - 1] !== ' ') {
trim += ' '
}
if (text[i] === '\n' && !flag) {
flag = true
}
}
}
// 去除含有换行符的空串
if (trim === ' ') {
if (flag) return
// #ifdef VUE3
else {
const parent = this.stack[this.stack.length - 1]
if (parent && parent.name[0] === 't') return
}
// #endif
}
text = trim
}
const node = Object.create(null)
node.type = 'text'
// #ifdef (MP-BAIDU || MP-ALIPAY || MP-TOUTIAO) && VUE3
node.attrs = {}
// #endif
node.text = decodeEntity(text)
if (this.hook(node)) {
// #ifdef MP-WEIXIN
if (this.options.selectable === 'force' && system.includes('iOS') && !uni.canIUse('rich-text.user-select')) {
this.expose()
}
// #endif
const siblings = this.stack.length ? this.stack[this.stack.length - 1].children : this.nodes
siblings.push(node)
}
}
/**
* @description html 词法分析器
* @param {Object} handler 高层处理器
*/
function Lexer (handler) {
this.handler = handler
}
/**
* @description 执行解析
* @param {String} content 要解析的文本
*/
Lexer.prototype.parse = function (content) {
this.content = content || ''
this.i = 0 // 标记解析位置
this.start = 0 // 标记一个单词的开始位置
this.state = this.text // 当前状态
for (let len = this.content.length; this.i !== -1 && this.i < len;) {
this.state()
}
}
/**
* @description 检查标签是否闭合
* @param {String} method 如果闭合要进行的操作
* @returns {Boolean} 是否闭合
* @private
*/
Lexer.prototype.checkClose = function (method) {
const selfClose = this.content[this.i] === '/'
if (this.content[this.i] === '>' || (selfClose && this.content[this.i + 1] === '>')) {
if (method) {
this.handler[method](this.content.substring(this.start, this.i))
}
this.i += selfClose ? 2 : 1
this.start = this.i
this.handler.onOpenTag(selfClose)
if (this.handler.tagName === 'script') {
this.i = this.content.indexOf('</', this.i)
if (this.i !== -1) {
this.i += 2
this.start = this.i
}
this.state = this.endTag
} else {
this.state = this.text
}
return true
}
return false
}
/**
* @description 文本状态
* @private
*/
Lexer.prototype.text = function () {
this.i = this.content.indexOf('<', this.i) // 查找最近的标签
if (this.i === -1) {
// 没有标签了
if (this.start < this.content.length) {
this.handler.onText(this.content.substring(this.start, this.content.length))
}
return
}
const c = this.content[this.i + 1]
if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')) {
// 标签开头
if (this.start !== this.i) {
this.handler.onText(this.content.substring(this.start, this.i))
}
this.start = ++this.i
this.state = this.tagName
} else if (c === '/' || c === '!' || c === '?') {
if (this.start !== this.i) {
this.handler.onText(this.content.substring(this.start, this.i))
}
const next = this.content[this.i + 2]
if (c === '/' && ((next >= 'a' && next <= 'z') || (next >= 'A' && next <= 'Z'))) {
// 标签结尾
this.i += 2
this.start = this.i
this.state = this.endTag
return
}
// 处理注释
let end = '-->'
if (c !== '!' || this.content[this.i + 2] !== '-' || this.content[this.i + 3] !== '-') {
end = '>'
}
this.i = this.content.indexOf(end, this.i)
if (this.i !== -1) {
this.i += end.length
this.start = this.i
}
} else {
this.i++
}
}
/**
* @description 标签名状态
* @private
*/
Lexer.prototype.tagName = function () {
if (blankChar[this.content[this.i]]) {
// 解析到标签名
this.handler.onTagName(this.content.substring(this.start, this.i))
while (blankChar[this.content[++this.i]]);
if (this.i < this.content.length && !this.checkClose()) {
this.start = this.i
this.state = this.attrName
}
} else if (!this.checkClose('onTagName')) {
this.i++
}
}
/**
* @description 属性名状态
* @private
*/
Lexer.prototype.attrName = function () {
let c = this.content[this.i]
if (blankChar[c] || c === '=') {
// 解析到属性名
this.handler.onAttrName(this.content.substring(this.start, this.i))
let needVal = c === '='
const len = this.content.length
while (++this.i < len) {
c = this.content[this.i]
if (!blankChar[c]) {
if (this.checkClose()) return
if (needVal) {
// 等号后遇到第一个非空字符
this.start = this.i
this.state = this.attrVal
return
}
if (this.content[this.i] === '=') {
needVal = true
} else {
this.start = this.i
this.state = this.attrName
return
}
}
}
} else if (!this.checkClose('onAttrName')) {
this.i++
}
}
/**
* @description 属性值状态
* @private
*/
Lexer.prototype.attrVal = function () {
const c = this.content[this.i]
const len = this.content.length
if (c === '"' || c === "'") {
// 有冒号的属性
this.start = ++this.i
this.i = this.content.indexOf(c, this.i)
if (this.i === -1) return
this.handler.onAttrVal(this.content.substring(this.start, this.i))
} else {
// 没有冒号的属性
for (; this.i < len; this.i++) {
if (blankChar[this.content[this.i]]) {
this.handler.onAttrVal(this.content.substring(this.start, this.i))
break
} else if (this.checkClose('onAttrVal')) return
}
}
while (blankChar[this.content[++this.i]]);
if (this.i < len && !this.checkClose()) {
this.start = this.i
this.state = this.attrName
}
}
/**
* @description 结束标签状态
* @returns {String} 结束的标签名
* @private
*/
Lexer.prototype.endTag = function () {
const c = this.content[this.i]
if (blankChar[c] || c === '>' || c === '/') {
this.handler.onCloseTag(this.content.substring(this.start, this.i))
if (c !== '>') {
this.i = this.content.indexOf('>', this.i)
if (this.i === -1) return
}
this.start = ++this.i
this.state = this.text
} else {
this.i++
}
}
export default Parser
{
"id": "mp-html",
"displayName": "mp-html 富文本组件【全端支持,支持编辑、latex等扩展】",
"version": "v2.5.1",
"description": "一个强大的富文本组件,高效轻量,功能丰富",
"keywords": [
"富文本",
"编辑器",
"html",
"rich-text",
"editor"
],
"repository": "https://github.com/jin-yufeng/mp-html",
"dcloudext": {
"sale": {
"regular": {
"price": "0.00"
},
"sourcecode": {
"price": "0.00"
}
},
"contact": {
"qq": ""
},
"declaration": {
"ads": "无",
"data": "无",
"permissions": "无"
},
"npmurl": "https://www.npmjs.com/package/mp-html",
"type": "component-vue"
},
"uni_modules": {
"platforms": {
"cloud": {
"tcb": "y",
"aliyun": "y",
"alipay": "n"
},
"client": {
"App": {
"app-vue": "y",
"app-nvue": "y",
"app-harmony": "u",
"app-uvue": "u"
},
"H5-mobile": {
"Safari": "y",
"Android Browser": "y",
"微信浏览器(Android)": "y",
"QQ浏览器(Android)": "y"
},
"H5-pc": {
"Chrome": "y",
"IE": "u",
"Edge": "y",
"Firefox": "y",
"Safari": "y"
},
"小程序": {
"微信": "y",
"阿里": "y",
"百度": "y",
"字节跳动": "y",
"QQ": "y"
},
"快应用": {
"华为": "y",
"联盟": "y"
},
"Vue": {
"vue2": "y",
"vue3": "y"
}
}
}
}
}
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment