@mteditor/renderer-mini 0.0.0-stage → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +150 -2
- package/components/README.md +92 -0
- package/components/mt-node/index.js +123 -0
- package/components/mt-node/index.json +6 -0
- package/components/mt-node/index.wxml +95 -0
- package/components/mt-node/index.wxss +163 -0
- package/components/mt-renderer/index.js +51 -0
- package/components/mt-renderer/index.json +6 -0
- package/components/mt-renderer/index.wxml +24 -0
- package/components/mt-renderer/index.wxss +14 -0
- package/components/mt-theme.wxss +82 -0
- package/dist/alipay/index.cjs +4 -0
- package/dist/alipay/index.cjs.map +1 -0
- package/dist/alipay/index.d.cts +2 -0
- package/dist/alipay/index.d.ts +2 -0
- package/dist/alipay/index.js +3 -0
- package/dist/alipay/index.js.map +1 -0
- package/dist/arkts/index.cjs +4 -0
- package/dist/arkts/index.cjs.map +1 -0
- package/dist/arkts/index.d.cts +2 -0
- package/dist/arkts/index.d.ts +2 -0
- package/dist/arkts/index.js +3 -0
- package/dist/arkts/index.js.map +1 -0
- package/dist/bytedance/index.cjs +4 -0
- package/dist/bytedance/index.cjs.map +1 -0
- package/dist/bytedance/index.d.cts +2 -0
- package/dist/bytedance/index.d.ts +2 -0
- package/dist/bytedance/index.js +3 -0
- package/dist/bytedance/index.js.map +1 -0
- package/dist/index.cjs +259 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +101 -0
- package/dist/index.d.ts +101 -0
- package/dist/index.js +247 -0
- package/dist/index.js.map +1 -0
- package/dist/taro/index.cjs +4 -0
- package/dist/taro/index.cjs.map +1 -0
- package/dist/taro/index.d.cts +2 -0
- package/dist/taro/index.d.ts +2 -0
- package/dist/taro/index.js +3 -0
- package/dist/taro/index.js.map +1 -0
- package/dist/theme-CXitJYrs.d.cts +214 -0
- package/dist/theme-CXitJYrs.d.ts +214 -0
- package/dist/uni-app/index.cjs +4 -0
- package/dist/uni-app/index.cjs.map +1 -0
- package/dist/uni-app/index.d.cts +2 -0
- package/dist/uni-app/index.d.ts +2 -0
- package/dist/uni-app/index.js +3 -0
- package/dist/uni-app/index.js.map +1 -0
- package/dist/wechat/index.cjs +259 -0
- package/dist/wechat/index.cjs.map +1 -0
- package/dist/wechat/index.d.cts +61 -0
- package/dist/wechat/index.d.ts +61 -0
- package/dist/wechat/index.js +250 -0
- package/dist/wechat/index.js.map +1 -0
- package/package.json +142 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 MtEditor contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,151 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @mteditor/renderer-mini
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
> 小程序 / uni-app / Taro / 鸿蒙 ArkTS 只读渲染器 —— 子路径导出多端。
|
|
4
|
+
|
|
5
|
+
## 定位
|
|
6
|
+
|
|
7
|
+
> **小程序端不提供完整富文本编辑。** 小程序无稳定 `contenteditable`,业界无可靠原生富文本方案。
|
|
8
|
+
> MtEditor 在小程序端提供 **100% 保真的只读渲染**;如需编辑,引导用户跳转 H5 / App 编辑页,
|
|
9
|
+
> 编辑后回传 `MtDocument`。
|
|
10
|
+
|
|
11
|
+
本包只做三件事,**不自己序列化 nodes**:
|
|
12
|
+
|
|
13
|
+
| 职责 | API |
|
|
14
|
+
|---|---|
|
|
15
|
+
| ① 通道解析(`auto` → 阈值判定) | `resolveChannel` |
|
|
16
|
+
| ② 主题字面量注入(小程序无 CSS 变量) | `resolveThemeTokens` / `MT_MINI_TOKENS_*` |
|
|
17
|
+
| ③ 平台能力表(准入) | `resolveCapabilities` / `MT_MINI_CAPABILITIES` |
|
|
18
|
+
|
|
19
|
+
`nodes` 一律由 `@mteditor/document` 的 `serializeDocumentToMini` 产出,
|
|
20
|
+
保证「编辑、详情页、小程序、导出」四处看到的是同一份真相。
|
|
21
|
+
|
|
22
|
+
## 可用平台
|
|
23
|
+
|
|
24
|
+
| 子路径 | 状态 |
|
|
25
|
+
|---|---|
|
|
26
|
+
| `@mteditor/renderer-mini/wechat` | ✅ 可用(微信小程序) |
|
|
27
|
+
| `/alipay` `/bytedance` `/uni-app` `/taro` `/arkts` | ⏳ 规划中。各端 `rich-text` 白名单不一致,**未实测前拒绝执行**(调用会抛 `RangeError`),不拿微信表硬跑 |
|
|
28
|
+
|
|
29
|
+
## 安装
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
npm i @mteditor/renderer-mini
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## 用法
|
|
36
|
+
|
|
37
|
+
```ts
|
|
38
|
+
import { render } from '@mteditor/renderer-mini/wechat'
|
|
39
|
+
|
|
40
|
+
const { mode, nodes, report, stats, autoSwitched } = render(doc, { theme: 'dark' })
|
|
41
|
+
|
|
42
|
+
// mode 是「实际使用的通道」,不是入参 —— 宿主必须按它分支
|
|
43
|
+
// nodes 直接喂给 <rich-text nodes="{{nodes}}"> 或递归组件
|
|
44
|
+
// report 为空数组表示内容无损;autoSwitched 非 null 表示触发了阈值降级
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
通用入口(自己传 `platform`):
|
|
48
|
+
|
|
49
|
+
```ts
|
|
50
|
+
import { renderMiniDocument } from '@mteditor/renderer-mini'
|
|
51
|
+
|
|
52
|
+
const result = renderMiniDocument(doc, { platform: 'wechat', mode: 'auto' })
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### 返回值(`MtMiniRenderResult`)
|
|
56
|
+
|
|
57
|
+
| 字段 | 说明 |
|
|
58
|
+
|---|---|
|
|
59
|
+
| `platform` | 目标平台 |
|
|
60
|
+
| `mode` | **实际使用**的通道;入参 `'auto'` 时这里是解析结果 |
|
|
61
|
+
| `nodes` | 小程序 nodes(**Array 形式**) |
|
|
62
|
+
| `report` | 降级报告(`MT_DEG_*`),无降级时为空数组。**始终返回、不可关闭** |
|
|
63
|
+
| `stats` | 序列化统计(节点数 / 最大深度 / 字节数) |
|
|
64
|
+
| `autoSwitched` | 自动通道切换记录(`from` / `to` / `nodeCount` / `maxNodes`);未触发为 `null` |
|
|
65
|
+
|
|
66
|
+
> `report` 与 `autoSwitched` 刻意分开:前者是**内容表达损失**,后者是**渲染策略**。
|
|
67
|
+
> 本包**不提供关闭降级报告的开关**:`rich-text` 通道会把 `video` / `audio` / 公式降级成文本,
|
|
68
|
+
> 这类损失用户直接看得见,留开关等于给静默留后门。
|
|
69
|
+
|
|
70
|
+
### 选项(`MtMiniRenderOptions`)
|
|
71
|
+
|
|
72
|
+
| 选项 | 缺省 | 说明 |
|
|
73
|
+
|---|---|---|
|
|
74
|
+
| `platform` | `'wechat'` | 目标平台;未实测核实的平台会被拒绝(抛 `RangeError`) |
|
|
75
|
+
| `mode` | `'auto'` | `'auto' \| 'richText' \| 'component'` |
|
|
76
|
+
| `maxNodes` | `3000` | `auto` 的降级阈值,单位是**文档节点数**;判定在序列化**之前** |
|
|
77
|
+
| `theme` | `'light'` | 内置主题名,或自定义字面量表(键含 `--` 前缀) |
|
|
78
|
+
| `themeTokens` | — | 主题字面量补充 / 覆盖,与 `theme` 合并后**后者优先** |
|
|
79
|
+
| `preRendered` | — | 预渲染产物索引(公式 SVG / 代码高亮) |
|
|
80
|
+
|
|
81
|
+
## 双通道渲染
|
|
82
|
+
|
|
83
|
+
| 通道 | 实现 | 适用 | 样式来源 |
|
|
84
|
+
|---|---|---|---|
|
|
85
|
+
| **B. 递归自定义组件**(默认) | JSON → 递归 `view` / `text` 树 | 复杂内容、需要交互(图片预览、表格横滑) | 组件 wxss + CSS 变量 |
|
|
86
|
+
| **A. `rich-text` 原生组件** | JSON → `nodes` 数组 | 内容简单、性能优先 | 序列化时**内联的字面量** |
|
|
87
|
+
|
|
88
|
+
`mode: 'auto'` 的判据是**文档节点数**(与 `maxNodes` 比较),**判定发生在序列化之前** ——
|
|
89
|
+
判定的意义正是避免一次昂贵序列化。触发降级时 `autoSwitched` 会带上切换前后的通道与节点数。
|
|
90
|
+
|
|
91
|
+
## 小程序组件(随包分发)
|
|
92
|
+
|
|
93
|
+
通道 B 的实现就在包里,**它是唯一权威副本**:
|
|
94
|
+
|
|
95
|
+
| 文件 | 作用 |
|
|
96
|
+
|---|---|
|
|
97
|
+
| `components/mt-renderer/` | 入口组件:按 `render()` 返回的 `mode` 在 `<rich-text>` 与递归组件之间分支 |
|
|
98
|
+
| `components/mt-node/` | 递归节点组件:按 `node.name` 分派到 `<view>` / `<text>` / `<image>` / `<video>` / `<audio>` |
|
|
99
|
+
| `components/mt-theme.wxss` | 主题变量表(宿主 `@import` 一次),亮色默认 + `mt-theme-dark` 覆盖 |
|
|
100
|
+
|
|
101
|
+
接入三步:
|
|
102
|
+
|
|
103
|
+
1. **复制组件**:小程序无法从 `node_modules` 直读组件,把 `components/` 下的
|
|
104
|
+
`mt-renderer` / `mt-node` 复制进项目(如 `components/mt/`),组件的 `usingComponents`
|
|
105
|
+
用的是相对路径,复制后无需改动;
|
|
106
|
+
2. **页面声明**:`"usingComponents": { "mt-renderer": "/components/mt/mt-renderer/index" }`;
|
|
107
|
+
3. **渲染**:
|
|
108
|
+
|
|
109
|
+
```js
|
|
110
|
+
import { render } from '@mteditor/renderer-mini/wechat'
|
|
111
|
+
|
|
112
|
+
Page({
|
|
113
|
+
data: { mode: 'component', nodes: [] },
|
|
114
|
+
|
|
115
|
+
onLoad() {
|
|
116
|
+
const { mode, nodes, report, autoSwitched } = render(doc, { theme: 'dark' })
|
|
117
|
+
this.setData({ mode, nodes })
|
|
118
|
+
if (report.length > 0) console.warn('内容有降级', report)
|
|
119
|
+
if (autoSwitched) console.warn('已自动切换通道', autoSwitched)
|
|
120
|
+
},
|
|
121
|
+
|
|
122
|
+
/** 图片点击 → 预览;附件点击 → 下载 */
|
|
123
|
+
onItemTap(event) {
|
|
124
|
+
const { kind, value } = event.detail
|
|
125
|
+
if (kind === 'image') wx.previewImage({ urls: [value] })
|
|
126
|
+
if (kind === 'attachment') wx.downloadFile({ url: value })
|
|
127
|
+
},
|
|
128
|
+
})
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
```xml
|
|
132
|
+
<mt-renderer mode="{{mode}}" nodes="{{nodes}}" bind:mttap="onItemTap" />
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
`mode` **必须是 `render()` 返回的值**,不要传你传进去的 `'auto'` ——
|
|
136
|
+
`'auto'` 的解析结果只有渲染器知道。两条通道的样式接入方式与组件选项(`virtualHost` +
|
|
137
|
+
`addGlobalClass`)见 [`components/README.md`](./components/README.md)。
|
|
138
|
+
|
|
139
|
+
## 注意事项
|
|
140
|
+
|
|
141
|
+
- **最低基础库 2.21.0**。
|
|
142
|
+
- **主题必须走字面量**:`rich-text` 内的 `style` 不参与宿主 CSS 变量继承,
|
|
143
|
+
所以 `theme` / `themeTokens` 是唯一入口(组件通道则用 `mt-theme.wxss` + CSS 变量)。
|
|
144
|
+
- **降级必须可见**:`video` / `audio` / `mention` 等在 `rich-text` 下不可表达的节点会写入 `report`,
|
|
145
|
+
禁止静默丢弃;`autoSwitched` 是「策略降级」的载体(本包不持有事件总线,故不用 `console.warn`)。
|
|
146
|
+
- 各端 `rich-text` 白名单**不完全一致**,映射表逐端独立维护;未实测的平台不猜。
|
|
147
|
+
- 小程序没有 `window`:本包不含任何 DOM 依赖,可在 Node 侧先算好 `nodes` 再下发。
|
|
148
|
+
|
|
149
|
+
## 许可
|
|
150
|
+
|
|
151
|
+
MIT
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# renderer-mini 的小程序组件
|
|
2
|
+
|
|
3
|
+
> 「递归自定义组件」通道的实现,随包分发 —— 通道的名字就是它,所以它是渲染器的一部分,
|
|
4
|
+
> 不是某个示例的私事。
|
|
5
|
+
|
|
6
|
+
## 目录
|
|
7
|
+
|
|
8
|
+
| 文件 | 作用 |
|
|
9
|
+
|---|---|
|
|
10
|
+
| `mt-renderer/` | 入口组件:按 `render()` 返回的 `mode` 在 `<rich-text>` 与递归组件之间分支 |
|
|
11
|
+
| `mt-node/` | 递归节点组件:按 `node.name` 分派到 `<view>` / `<text>` / `<image>` / `<video>` / `<audio>` |
|
|
12
|
+
| `mt-theme.wxss` | 主题变量表(宿主 `@import` 一次),亮色默认 + `mt-theme-dark` 覆盖 |
|
|
13
|
+
|
|
14
|
+
## 接入(三步)
|
|
15
|
+
|
|
16
|
+
**1. 复制或引用组件**
|
|
17
|
+
|
|
18
|
+
小程序无法从 `node_modules` 直接读组件,把这两个目录复制进项目(如 `components/mt/`)即可。
|
|
19
|
+
组件的 `usingComponents` 用的是相对路径,复制后无需改动。
|
|
20
|
+
|
|
21
|
+
**2. 页面声明组件**
|
|
22
|
+
|
|
23
|
+
```json
|
|
24
|
+
{
|
|
25
|
+
"usingComponents": {
|
|
26
|
+
"mt-renderer": "/components/mt/mt-renderer/index"
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
**3. 渲染**
|
|
32
|
+
|
|
33
|
+
```js
|
|
34
|
+
import { render } from '@mteditor/renderer-mini/wechat'
|
|
35
|
+
|
|
36
|
+
Page({
|
|
37
|
+
data: { mode: 'component', nodes: [] },
|
|
38
|
+
|
|
39
|
+
onLoad() {
|
|
40
|
+
const { mode, nodes, report, autoSwitched } = render(doc, { theme: 'dark' })
|
|
41
|
+
this.setData({ mode, nodes })
|
|
42
|
+
if (report.length > 0) console.warn('内容有降级', report)
|
|
43
|
+
if (autoSwitched) console.warn('已自动切换通道', autoSwitched)
|
|
44
|
+
},
|
|
45
|
+
|
|
46
|
+
/** 图片点击 → 预览;附件点击 → 下载 */
|
|
47
|
+
onItemTap(event) {
|
|
48
|
+
const { kind, value } = event.detail
|
|
49
|
+
if (kind === 'image') wx.previewImage({ urls: [value] })
|
|
50
|
+
if (kind === 'attachment') wx.downloadFile({ url: value })
|
|
51
|
+
},
|
|
52
|
+
})
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
```xml
|
|
56
|
+
<mt-renderer mode="{{mode}}" nodes="{{nodes}}" bind:mttap="onItemTap" />
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
`mode` **必须是 `render()` 返回的值**,不要传你传进去的 `'auto'` ——
|
|
60
|
+
`'auto'` 的解析结果只有渲染器知道。
|
|
61
|
+
|
|
62
|
+
## 两条通道的样式来源不同(重要)
|
|
63
|
+
|
|
64
|
+
| 通道 | 样式来源 | 主题切换 |
|
|
65
|
+
|---|---|---|
|
|
66
|
+
| `component`(默认) | 组件 wxss + CSS 变量 | 覆盖 `page { --mt-*: … }`,或加 `mt-theme-dark` class |
|
|
67
|
+
| `richText` | 序列化时**内联的字面量** | `render(doc, { theme, themeTokens })` |
|
|
68
|
+
|
|
69
|
+
原因是 `rich-text` 内的 `style` 不参与宿主 CSS 变量继承,
|
|
70
|
+
所以那条通道的主题必须在序列化阶段就解析成字面量。
|
|
71
|
+
|
|
72
|
+
## 三处有意的额外包裹
|
|
73
|
+
|
|
74
|
+
跨端一致性要求「无多余包裹」,下面三处是**明确记录在案**的例外,
|
|
75
|
+
都是小程序能力约束所致:
|
|
76
|
+
|
|
77
|
+
1. **表格**:外包一层 `<scroll-view scroll-x>` —— 宽表格必须能横滑;
|
|
78
|
+
2. **公式**:`<view class="mt-math">` 里的 SVG 文本由组件转成 `data:image/svg+xml,` URI 后交给 `<image>`;
|
|
79
|
+
3. **附件**:独立分支以便绑定点击。
|
|
80
|
+
|
|
81
|
+
## 组件选项
|
|
82
|
+
|
|
83
|
+
两个组件都开了:
|
|
84
|
+
|
|
85
|
+
- `virtualHost: true` —— 组件自身不产生包裹节点(基础库 ≥ 2.19.2,本包基线 2.21.0);
|
|
86
|
+
- `addGlobalClass: true` —— 允许宿主的全局样式作用进组件,宿主才有办法微调外观。
|
|
87
|
+
|
|
88
|
+
最低基础库:**2.21.0**。
|
|
89
|
+
|
|
90
|
+
## 许可
|
|
91
|
+
|
|
92
|
+
MIT
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `mt-node` —— 递归节点组件(「通道 B:递归自定义组件」)。
|
|
3
|
+
*
|
|
4
|
+
* ## 输入契约
|
|
5
|
+
*
|
|
6
|
+
* `node` 就是一个 `MtMiniNode`(由 `renderMiniDocument` / `render` 产出):
|
|
7
|
+
*
|
|
8
|
+
* ```ts
|
|
9
|
+
* { type: 'node', name: 'view', attrs: { class: 'mt-p' }, children: [...] }
|
|
10
|
+
* { type: 'text', text: '段落内容' }
|
|
11
|
+
* ```
|
|
12
|
+
*
|
|
13
|
+
* 本组件**不做任何数据解释**(不解析 JSON、不拼字符串),只按 `name` 分派
|
|
14
|
+
* 到对应的小程序标签 —— 所有结构决策都在 `@mteditor/document` 里做完了。
|
|
15
|
+
*
|
|
16
|
+
* ## 两个必须显式打开的能力
|
|
17
|
+
*
|
|
18
|
+
* `virtualHost: true`:组件自身不产生包裹节点。 要求跨端「节点数与层级结构
|
|
19
|
+
* 一一对应,无多余包裹」,默认的包裹行为会让 35 篇基线全部多出一层。
|
|
20
|
+
* 基础库要求 ≥ 2.19.2,本包基线 2.21.0满足。
|
|
21
|
+
* - `addGlobalClass: true`:允许宿主的全局样式(`app.wxss`)作用到本组件内,
|
|
22
|
+
* 宿主才有办法微调外观而不必改组件源码。
|
|
23
|
+
*
|
|
24
|
+
* ## 事件
|
|
25
|
+
*
|
|
26
|
+
* 只冒泡一种事件 `mttap`,`detail` 形如:
|
|
27
|
+
*
|
|
28
|
+
* ```ts
|
|
29
|
+
* { kind: 'image' | 'attachment', value: string }
|
|
30
|
+
* ```
|
|
31
|
+
*
|
|
32
|
+
* 递归链上每一层都原样转发,最终由 `mt-renderer` 抛给页面 ——
|
|
33
|
+
* 节点树有多深,事件都能一路冒上来。
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
Component({
|
|
37
|
+
options: {
|
|
38
|
+
virtualHost: true,
|
|
39
|
+
addGlobalClass: true,
|
|
40
|
+
},
|
|
41
|
+
|
|
42
|
+
properties: {
|
|
43
|
+
/** 待渲染的单个节点(`MtMiniNode`) */
|
|
44
|
+
node: { type: null, value: null },
|
|
45
|
+
/** 是否允许长按选中文本,缺省允许(只读页里用户常需要复制) */
|
|
46
|
+
selectable: { type: Boolean, value: true },
|
|
47
|
+
},
|
|
48
|
+
|
|
49
|
+
data: {
|
|
50
|
+
/** `mt-math` 节点转换出的 data URI;空串表示未转换 */
|
|
51
|
+
mathSrc: '',
|
|
52
|
+
},
|
|
53
|
+
|
|
54
|
+
observers: {
|
|
55
|
+
node(value) {
|
|
56
|
+
this.refreshMath(value)
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
|
|
60
|
+
methods: {
|
|
61
|
+
/**
|
|
62
|
+
* 公式节点预处理。
|
|
63
|
+
*
|
|
64
|
+
* `document` 把预渲染好的 SVG 以**文本载荷**挂在 `<view class="mt-math">` 下
|
|
65
|
+
* (它不假设宿主如何渲染 SVG)。小程序不能内联 SVG 标签,但 `<image>` 可以吃
|
|
66
|
+
* `data:image/svg+xml,` 开头的 data URI —— 转换放在这里,因为它属于「渲染」。
|
|
67
|
+
*
|
|
68
|
+
* @param {object} node 当前节点
|
|
69
|
+
*/
|
|
70
|
+
refreshMath(node) {
|
|
71
|
+
const isMath = node && node.attrs && node.attrs.class === 'mt-math'
|
|
72
|
+
if (!isMath) {
|
|
73
|
+
if (this.data.mathSrc !== '') this.setData({ mathSrc: '' })
|
|
74
|
+
return
|
|
75
|
+
}
|
|
76
|
+
const first = node.children && node.children[0]
|
|
77
|
+
const svg = first && first.type === 'text' ? first.text : ''
|
|
78
|
+
const src = svg ? `data:image/svg+xml,${encodeURIComponent(svg)}` : ''
|
|
79
|
+
if (src !== this.data.mathSrc) this.setData({ mathSrc: src })
|
|
80
|
+
},
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* 向上转发子节点的事件。
|
|
84
|
+
*
|
|
85
|
+
* @param {object} event 子组件抛出的 `mttap`
|
|
86
|
+
*/
|
|
87
|
+
onBubble(event) {
|
|
88
|
+
this.triggerEvent('mttap', event.detail)
|
|
89
|
+
},
|
|
90
|
+
|
|
91
|
+
/** 图片点击:把 `src` 交给页面去 `previewImage` */
|
|
92
|
+
onImageTap() {
|
|
93
|
+
this.emit('image', this.attr('src'))
|
|
94
|
+
},
|
|
95
|
+
|
|
96
|
+
/** 附件点击:把下载地址交给页面 */
|
|
97
|
+
onAttachmentTap() {
|
|
98
|
+
this.emit('attachment', this.attr('data-src'))
|
|
99
|
+
},
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* 读取当前节点的某个属性。
|
|
103
|
+
*
|
|
104
|
+
* @param {string} key 属性名
|
|
105
|
+
* @returns {string} 属性值;缺失时为空串
|
|
106
|
+
*/
|
|
107
|
+
attr(key) {
|
|
108
|
+
const node = this.data.node
|
|
109
|
+
return (node && node.attrs && node.attrs[key]) || ''
|
|
110
|
+
},
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* 抛出 `mttap`。
|
|
114
|
+
*
|
|
115
|
+
* @param {'image' | 'attachment'} kind 事件种类
|
|
116
|
+
* @param {string} value 载荷(通常是 URL)
|
|
117
|
+
*/
|
|
118
|
+
emit(kind, value) {
|
|
119
|
+
if (!value) return
|
|
120
|
+
this.triggerEvent('mttap', { kind, value })
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
})
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
mt-node —— 递归渲染单个节点(通道 B)
|
|
3
|
+
|
|
4
|
+
分派规则与「递归组件通道」一列逐行对应
|
|
5
|
+
文本 → <text>;图片/视频/音频 → 原生组件;其余 → <view> 并递归 children。
|
|
6
|
+
|
|
7
|
+
三处**有意的额外包裹**,都是小程序的能力约束所致,不是设计疏漏:
|
|
8
|
+
1. 表格:包一层 <scroll-view scroll-x> —— 宽表格必须能横滑;
|
|
9
|
+
2. 公式:<view class="mt-math"> 里的 SVG 文本在 JS 里转成 data URI 后由 <image> 承载;
|
|
10
|
+
3. 附件:需要绑定点击,故独立分支而非走通用 <view>。
|
|
11
|
+
-->
|
|
12
|
+
|
|
13
|
+
<!-- 文本节点 -->
|
|
14
|
+
<text
|
|
15
|
+
wx:if="{{node.type === 'text'}}"
|
|
16
|
+
class="{{node.attrs.class}}"
|
|
17
|
+
style="{{node.attrs.style}}"
|
|
18
|
+
user-select="{{selectable}}"
|
|
19
|
+
>{{node.text}}</text>
|
|
20
|
+
|
|
21
|
+
<!-- 图片:mode=widthFix 是「宽度自适应、高度按比例」的唯一可靠方式 -->
|
|
22
|
+
<image
|
|
23
|
+
wx:elif="{{node.name === 'image'}}"
|
|
24
|
+
class="{{node.attrs.class}}"
|
|
25
|
+
style="{{node.attrs.style}}"
|
|
26
|
+
src="{{node.attrs.src}}"
|
|
27
|
+
mode="widthFix"
|
|
28
|
+
bindtap="onImageTap"
|
|
29
|
+
/>
|
|
30
|
+
|
|
31
|
+
<!-- 视频(rich-text 通道不可表达,必须由原生组件承载) -->
|
|
32
|
+
<video
|
|
33
|
+
wx:elif="{{node.name === 'video'}}"
|
|
34
|
+
class="{{node.attrs.class}}"
|
|
35
|
+
style="{{node.attrs.style}}"
|
|
36
|
+
src="{{node.attrs.src}}"
|
|
37
|
+
poster="{{node.attrs.poster}}"
|
|
38
|
+
/>
|
|
39
|
+
|
|
40
|
+
<!-- 音频 -->
|
|
41
|
+
<audio
|
|
42
|
+
wx:elif="{{node.name === 'audio'}}"
|
|
43
|
+
class="{{node.attrs.class}}"
|
|
44
|
+
style="{{node.attrs.style}}"
|
|
45
|
+
src="{{node.attrs.src}}"
|
|
46
|
+
/>
|
|
47
|
+
|
|
48
|
+
<!-- 公式:SVG 已在 JS 中转为 data URI -->
|
|
49
|
+
<image
|
|
50
|
+
wx:elif="{{node.attrs.class === 'mt-math' && mathSrc}}"
|
|
51
|
+
class="mt-math"
|
|
52
|
+
src="{{mathSrc}}"
|
|
53
|
+
mode="widthFix"
|
|
54
|
+
/>
|
|
55
|
+
|
|
56
|
+
<!-- 表格:外层 <scroll-view scroll-x> 提供横滑 -->
|
|
57
|
+
<scroll-view wx:elif="{{node.attrs.class === 'mt-table'}}" class="mt-table-scroll" scroll-x>
|
|
58
|
+
<view class="{{node.attrs.class}}" style="{{node.attrs.style}}">
|
|
59
|
+
<mt-node
|
|
60
|
+
wx:for="{{node.children}}"
|
|
61
|
+
wx:key="index"
|
|
62
|
+
node="{{item}}"
|
|
63
|
+
selectable="{{selectable}}"
|
|
64
|
+
bind:mttap="onBubble"
|
|
65
|
+
/>
|
|
66
|
+
</view>
|
|
67
|
+
</scroll-view>
|
|
68
|
+
|
|
69
|
+
<!-- 附件卡片:可点击 -->
|
|
70
|
+
<view
|
|
71
|
+
wx:elif="{{node.attrs.class === 'mt-attachment'}}"
|
|
72
|
+
class="mt-attachment"
|
|
73
|
+
style="{{node.attrs.style}}"
|
|
74
|
+
data-src="{{node.attrs['data-src']}}"
|
|
75
|
+
bindtap="onAttachmentTap"
|
|
76
|
+
>
|
|
77
|
+
<mt-node
|
|
78
|
+
wx:for="{{node.children}}"
|
|
79
|
+
wx:key="index"
|
|
80
|
+
node="{{item}}"
|
|
81
|
+
selectable="{{selectable}}"
|
|
82
|
+
bind:mttap="onBubble"
|
|
83
|
+
/>
|
|
84
|
+
</view>
|
|
85
|
+
|
|
86
|
+
<!-- 通用元素节点:<view> + 递归 -->
|
|
87
|
+
<view wx:else class="{{node.attrs.class}}" style="{{node.attrs.style}}">
|
|
88
|
+
<mt-node
|
|
89
|
+
wx:for="{{node.children}}"
|
|
90
|
+
wx:key="index"
|
|
91
|
+
node="{{item}}"
|
|
92
|
+
selectable="{{selectable}}"
|
|
93
|
+
bind:mttap="onBubble"
|
|
94
|
+
/>
|
|
95
|
+
</view>
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* mt-node 的节点样式。
|
|
3
|
+
*
|
|
4
|
+
* ## 与 rich-text 通道的**本质差异**(必须写进对外文档)
|
|
5
|
+
*
|
|
6
|
+
* | 通道 | 样式来源 | 主题切换方式 |
|
|
7
|
+
* |---|---|---|
|
|
8
|
+
* | 组件通道(本文件) | WXSS + CSS 变量 | 宿主覆盖 `page { --mt-*: … }`,或用 `mt-theme.wxss` |
|
|
9
|
+
* | `rich-text` 通道 | 序列化时**内联的字面量** | `render(doc, { theme, themeTokens })` |
|
|
10
|
+
*
|
|
11
|
+
* 原因是硬约束:`rich-text` 内的 `style` 不参与宿主 CSS 变量继承,
|
|
12
|
+
* 而组件通道里的元素是宿主的真实节点,CSS 变量照常生效。
|
|
13
|
+
*
|
|
14
|
+
* ## 每个取值都带 fallback
|
|
15
|
+
*
|
|
16
|
+
* 宿主不引 `mt-theme.wxss` 也要能渲染出可用外观 —— 一个「必须再引一个文件才能看」
|
|
17
|
+
* 的渲染器,第一次用就会被认为是坏的。
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
/* ---- 段落与标题 ---- */
|
|
21
|
+
|
|
22
|
+
.mt-p {
|
|
23
|
+
margin: 0 0 0.75em;
|
|
24
|
+
font-size: var(--mt-font-size, 15px);
|
|
25
|
+
line-height: var(--mt-line-height, 1.7);
|
|
26
|
+
color: var(--mt-color-text, #1f2329);
|
|
27
|
+
overflow-wrap: break-word;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.mt-h1,
|
|
31
|
+
.mt-h2,
|
|
32
|
+
.mt-h3,
|
|
33
|
+
.mt-h4,
|
|
34
|
+
.mt-h5,
|
|
35
|
+
.mt-h6 {
|
|
36
|
+
margin: 1em 0 0.5em;
|
|
37
|
+
font-weight: 600;
|
|
38
|
+
line-height: 1.35;
|
|
39
|
+
color: var(--mt-color-text, #1f2329);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
.mt-h1 {
|
|
43
|
+
font-size: var(--mt-h1-font-size, 28px);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
.mt-h2 {
|
|
47
|
+
font-size: var(--mt-h2-font-size, 24px);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
.mt-h3 {
|
|
51
|
+
font-size: var(--mt-h3-font-size, 20px);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
.mt-h4 {
|
|
55
|
+
font-size: var(--mt-h4-font-size, 18px);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
.mt-h5 {
|
|
59
|
+
font-size: var(--mt-h5-font-size, 16px);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
.mt-h6 {
|
|
63
|
+
font-size: var(--mt-h6-font-size, 15px);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/* ---- 引用(左侧竖线由序列化时内联的 style 提供) ---- */
|
|
67
|
+
|
|
68
|
+
.mt-blockquote {
|
|
69
|
+
margin: 0 0 0.75em;
|
|
70
|
+
color: var(--mt-color-text-secondary, #646a73);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/* ---- 列表:缩进由 WXSS 提供,序号由 <ol> 自绘 ---- */
|
|
74
|
+
|
|
75
|
+
.mt-ul,
|
|
76
|
+
.mt-ol {
|
|
77
|
+
margin: 0 0 0.75em;
|
|
78
|
+
padding-left: 1.6em;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
.mt-li {
|
|
82
|
+
margin: 0.25em 0;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/* 嵌套列表再加一层缩进 */
|
|
86
|
+
.mt-li .mt-ul,
|
|
87
|
+
.mt-li .mt-ol {
|
|
88
|
+
margin-bottom: 0;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/* ---- 任务列表(小程序无 input,序列化时已用 ☐ / ☑ 字符模拟) ---- */
|
|
92
|
+
|
|
93
|
+
.mt-task-list {
|
|
94
|
+
margin: 0 0 0.75em;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
.mt-task-item {
|
|
98
|
+
margin: 0.25em 0;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/* ---- 代码块(背景色与横向滚动由内联 style 提供) ---- */
|
|
102
|
+
|
|
103
|
+
.mt-pre {
|
|
104
|
+
margin: 0 0 0.75em;
|
|
105
|
+
border-radius: var(--mt-radius-md, 6px);
|
|
106
|
+
font-size: var(--mt-font-size-sm, 13px);
|
|
107
|
+
white-space: pre-wrap;
|
|
108
|
+
overflow-wrap: break-word;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/* ---- 分割线(border-top 由内联 style 提供) ---- */
|
|
112
|
+
|
|
113
|
+
.mt-hr {
|
|
114
|
+
height: 0;
|
|
115
|
+
margin: 1.25em 0;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/* ---- 表格:外层 scroll-view 提供横滑 ---- */
|
|
119
|
+
|
|
120
|
+
.mt-table-scroll {
|
|
121
|
+
width: 100%;
|
|
122
|
+
margin: 0 0 0.75em;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
.mt-table {
|
|
126
|
+
border-collapse: collapse;
|
|
127
|
+
font-size: var(--mt-font-size-sm, 13px);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
.mt-th,
|
|
131
|
+
.mt-td {
|
|
132
|
+
padding: 6px 8px;
|
|
133
|
+
border: 1px solid var(--mt-color-border, #dee0e3);
|
|
134
|
+
vertical-align: top;
|
|
135
|
+
color: var(--mt-color-text, #1f2329);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
.mt-th {
|
|
139
|
+
background-color: var(--mt-color-bg-muted, #f5f7fa);
|
|
140
|
+
font-weight: 600;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/* ---- 媒体与其它 ---- */
|
|
144
|
+
|
|
145
|
+
.mt-img {
|
|
146
|
+
display: block;
|
|
147
|
+
max-width: 100%;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
.mt-math {
|
|
151
|
+
display: block;
|
|
152
|
+
max-width: 100%;
|
|
153
|
+
margin: 0.5em 0;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
.mt-attachment {
|
|
157
|
+
display: block;
|
|
158
|
+
margin: 0 0 0.75em;
|
|
159
|
+
padding: 8px 12px;
|
|
160
|
+
border: 1px solid var(--mt-color-border, #dee0e3);
|
|
161
|
+
border-radius: var(--mt-radius-md, 6px);
|
|
162
|
+
background-color: var(--mt-color-bg-muted, #f5f7fa);
|
|
163
|
+
}
|