@ant-design/x-markdown-mini 0.1.0-beta.0 → 0.1.0-beta.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.
Files changed (48) hide show
  1. package/dist/components/Markdown/index.acss +65 -7
  2. package/dist/components/MiniNodeRenderer/index.acss +19 -4
  3. package/dist/components/MiniNodeRenderer/index.axml +109 -29
  4. package/dist/components/MiniNodeRenderer/index.js +12 -0
  5. package/dist/components/MiniNodeRenderer/index.sjs +48 -0
  6. package/dist/es/Markdown/index.acss +65 -7
  7. package/dist/es/MiniNodeRenderer/index.acss +19 -4
  8. package/dist/es/MiniNodeRenderer/index.axml +109 -29
  9. package/dist/es/MiniNodeRenderer/index.js +12 -0
  10. package/dist/es/MiniNodeRenderer/index.sjs +48 -0
  11. package/dist/index.d.mts +39 -1
  12. package/dist/index.d.ts +39 -1
  13. package/dist/index.js +134 -56
  14. package/dist/index.mjs +134 -56
  15. package/dist/miniprogram_dist/components/Markdown/index.json +1 -1
  16. package/dist/miniprogram_dist/components/Markdown/index.wxss +65 -7
  17. package/dist/miniprogram_dist/components/MiniNodeRenderer/index.js +12 -0
  18. package/dist/miniprogram_dist/components/MiniNodeRenderer/index.json +1 -1
  19. package/dist/miniprogram_dist/components/MiniNodeRenderer/index.wxml +98 -21
  20. package/dist/miniprogram_dist/components/MiniNodeRenderer/index.wxs +41 -0
  21. package/dist/miniprogram_dist/components/MiniNodeRenderer/index.wxss +13 -2
  22. package/dist/miniprogram_dist/es/Markdown/index.json +1 -1
  23. package/dist/miniprogram_dist/es/Markdown/index.wxss +65 -7
  24. package/dist/miniprogram_dist/es/MiniNodeRenderer/index.js +12 -0
  25. package/dist/miniprogram_dist/es/MiniNodeRenderer/index.json +1 -1
  26. package/dist/miniprogram_dist/es/MiniNodeRenderer/index.wxml +98 -21
  27. package/dist/miniprogram_dist/es/MiniNodeRenderer/index.wxs +41 -0
  28. package/dist/miniprogram_dist/es/MiniNodeRenderer/index.wxss +13 -2
  29. package/dist/miniprogram_dist/index.js +134 -56
  30. package/dist/miniprogram_dist/plugins/CodeHighlight/index.js +13 -5
  31. package/dist/miniprogram_dist/plugins/CodeHighlight/style.wxss +41 -31
  32. package/dist/miniprogram_dist/plugins/Latex/index.js +44 -23
  33. package/dist/miniprogram_dist/plugins/Latex/style.wxss +11 -11
  34. package/dist/miniprogram_dist/shared/flattenInline.js +33 -5
  35. package/dist/plugins/CodeHighlight/index.d.mts +1 -1
  36. package/dist/plugins/CodeHighlight/index.d.ts +1 -1
  37. package/dist/plugins/CodeHighlight/index.js +13 -5
  38. package/dist/plugins/CodeHighlight/index.mjs +13 -5
  39. package/dist/plugins/CodeHighlight/style.acss +41 -31
  40. package/dist/plugins/Latex/index.d.mts +1 -1
  41. package/dist/plugins/Latex/index.d.ts +1 -1
  42. package/dist/plugins/Latex/index.js +44 -23
  43. package/dist/plugins/Latex/index.mjs +47 -23
  44. package/dist/plugins/Latex/style.acss +11 -6
  45. package/dist/shared/flattenInline.js +33 -5
  46. package/dist/{types-CegkonfJ.d.mts → types-BcxGtbQZ.d.mts} +25 -0
  47. package/dist/{types-CegkonfJ.d.ts → types-BcxGtbQZ.d.ts} +25 -0
  48. package/package.json +3 -2
@@ -1,9 +1,21 @@
1
1
  <wxs src="./index.wxs" module="u" />
2
2
 
3
3
  <block wx:for="{{nodes}}" wx:for-item="node" wx:for-index="i" wx:key="i">
4
- <!-- text leaf -->
4
+ <!-- text leaf: 流式时逐字淡入(每字一个 <text>,按下标 key 复用,只有新字触发动画);
5
+ 非流式时整段渲染为单个 <text>。 -->
5
6
  <text
6
- wx:if="{{u.isText(node.name)}}"
7
+ wx:if="{{u.isText(node.name) && animation}}"
8
+ class="{{u.classOf(node)}}"
9
+ selectable="{{selectable}}"
10
+ ><text
11
+ wx:for="{{u.charsOf(node)}}"
12
+ wx:for-item="ch"
13
+ wx:for-index="ci"
14
+ wx:key="ci"
15
+ class="md-anim-char"
16
+ >{{ch}}</text></text>
17
+ <text
18
+ wx:elif="{{u.isText(node.name)}}"
7
19
  class="{{u.classOf(node)}}"
8
20
  selectable="{{selectable}}"
9
21
  >{{u.valueOf(node)}}</text>
@@ -25,26 +37,91 @@
25
37
  catch:tap="_tap"
26
38
  />
27
39
 
28
- <!-- pre: scrollable code container -->
29
- <scroll-view
40
+ <!-- pre: code block with language label + copy toolbar, scrollable body -->
41
+ <view
30
42
  wx:elif="{{u.isPre(node.name)}}"
31
- scroll-x
32
- class="{{u.classOf(node)}}"
33
- style="{{u.styleOf(node)}}"
34
- data-data="{{node}}"
35
- catch:tap="_tap"
43
+ class="md-codeblock {{node.animate ? 'md-animate-block' : ''}}"
36
44
  >
37
- <mini-node-renderer
38
- wx:if="{{node.children}}"
39
- generic:custom-slot="custom-slot"
40
- nodes="{{node.children}}"
41
- selectable="{{selectable}}"
42
- animation="{{false}}"
43
- slotComponents="{{slotComponents}}"
44
- bind:tap="_tap"
45
- bind:appear="_appear"
46
- />
47
- </scroll-view>
45
+ <view wx:if="{{node.header}}" class="md-codeblock-bar">
46
+ <block wx:for="{{node.header}}" wx:for-item="h" wx:for-index="hi" wx:key="hi">
47
+ <text wx:if="{{u.isText(h.name)}}" class="{{u.classOf(h)}}">{{u.valueOf(h)}}</text>
48
+ <image
49
+ wx:elif="{{u.isCopy(h.name)}}"
50
+ class="{{u.classOf(h) || 'md-copy-icon'}}"
51
+ src="https://mdn.alipayobjects.com/huamei_y8xg5f/afts/img/JXzqSoHDzm4AAAAAHpAAAAgADuJhAQFr/original"
52
+ data-copy="{{u.copyOf(h)}}"
53
+ catch:tap="_copy"
54
+ />
55
+ <view wx:else class="{{u.classOf(h)}}" style="{{u.styleOf(h)}}">
56
+ <mini-node-renderer
57
+ wx:if="{{h.children}}"
58
+ generic:custom-slot="custom-slot"
59
+ nodes="{{h.children}}"
60
+ selectable="{{selectable}}"
61
+ animation="{{false}}"
62
+ slotComponents="{{slotComponents}}"
63
+ bind:tap="_tap"
64
+ />
65
+ </view>
66
+ </block>
67
+ </view>
68
+ <scroll-view scroll-x class="md-code-block">
69
+ <mini-node-renderer
70
+ wx:if="{{node.children}}"
71
+ generic:custom-slot="custom-slot"
72
+ nodes="{{node.children}}"
73
+ selectable="{{selectable}}"
74
+ animation="{{false}}"
75
+ slotComponents="{{slotComponents}}"
76
+ bind:tap="_tap"
77
+ bind:appear="_appear"
78
+ />
79
+ </scroll-view>
80
+ </view>
81
+
82
+ <!-- table: caption + copy toolbar card, horizontal-scroll CSS table (columns kept aligned) -->
83
+ <view
84
+ wx:elif="{{u.isTable(node.name)}}"
85
+ class="md-tableblock {{node.animate ? 'md-animate-block' : ''}}"
86
+ >
87
+ <view wx:if="{{node.header}}" class="md-tableblock-bar">
88
+ <block wx:for="{{node.header}}" wx:for-item="h" wx:for-index="hi" wx:key="hi">
89
+ <text wx:if="{{u.isText(h.name)}}" class="{{u.classOf(h)}}">{{u.valueOf(h)}}</text>
90
+ <image
91
+ wx:elif="{{u.isCopy(h.name)}}"
92
+ class="{{u.classOf(h) || 'md-copy-icon'}}"
93
+ src="https://mdn.alipayobjects.com/huamei_y8xg5f/afts/img/JXzqSoHDzm4AAAAAHpAAAAgADuJhAQFr/original"
94
+ data-copy="{{u.copyOf(h)}}"
95
+ catch:tap="_copy"
96
+ />
97
+ <view wx:else class="{{u.classOf(h)}}" style="{{u.styleOf(h)}}">
98
+ <mini-node-renderer
99
+ wx:if="{{h.children}}"
100
+ generic:custom-slot="custom-slot"
101
+ nodes="{{h.children}}"
102
+ selectable="{{selectable}}"
103
+ animation="{{false}}"
104
+ slotComponents="{{slotComponents}}"
105
+ bind:tap="_tap"
106
+ />
107
+ </view>
108
+ </block>
109
+ </view>
110
+ <scroll-view scroll-x class="md-table-scroll">
111
+ <view class="md-table">
112
+ <mini-node-renderer
113
+ wx:if="{{node.children}}"
114
+ generic:custom-slot="custom-slot"
115
+ nodes="{{node.children}}"
116
+ selectable="{{selectable}}"
117
+ animation="{{false}}"
118
+ slotComponents="{{slotComponents}}"
119
+ bind:tap="_tap"
120
+ bind:appear="_appear"
121
+ />
122
+ </view>
123
+ </scroll-view>
124
+ </view>
48
125
 
49
126
  <!-- anchor: kept interactive, children are already flattened text runs -->
50
127
  <text
@@ -66,7 +143,7 @@
66
143
 
67
144
  <!-- inline tag rendered as <text>: children are flat text runs -->
68
145
  <text
69
- wx:elif="{{u.isInline(node.name)}}"
146
+ wx:elif="{{u.isInline(node.name) && !u.isRich(node)}}"
70
147
  class="{{u.classOf(node)}}"
71
148
  style="{{u.styleOf(node)}}"
72
149
  selectable="{{selectable}}"
@@ -6,6 +6,20 @@ function isBr(name) { return name === 'br'; }
6
6
  function isImg(name) { return name === 'img'; }
7
7
  function isHr(name) { return name === 'hr'; }
8
8
  function isPre(name) { return name === 'pre'; }
9
+ function isTable(name) { return name === 'table'; }
10
+ function isCopy(name) { return name === 'copy-button'; }
11
+ function copyOf(node) { return (node.attrs || {})['data-copy'] || ''; }
12
+ // 富内联节点(如 KaTeX 公式容器)含有元素子节点,必须走递归 <view> 路径
13
+ // 渲染,保留嵌套结构与 style;普通已扁平化的内联只有 text/br 子节点。
14
+ function isRich(node) {
15
+ var ch = node.children;
16
+ if (!ch) return false;
17
+ for (var i = 0; i < ch.length; i++) {
18
+ var nm = ch[i].name;
19
+ if (nm !== 'text' && nm !== 'br') return true;
20
+ }
21
+ return false;
22
+ }
9
23
  function isSlot(name, slotComponents) {
10
24
  return !!slotComponents && slotComponents.indexOf(name) > -1;
11
25
  }
@@ -19,8 +33,29 @@ function classOf(node) {
19
33
  return cls;
20
34
  }
21
35
 
36
+ // 把文本拆成「按 code point 安全」的字符数组(不劈坏 emoji/组合 surrogate pair),
37
+ // 供流式逐字淡入:每个字符渲染成独立 <text>,按下标 key 复用,只有新字符会触发淡入动画。
38
+ function charsOf(node) {
39
+ var v = (node.attrs || {}).value || '';
40
+ var out = [];
41
+ var i = 0;
42
+ var n = v.length;
43
+ while (i < n) {
44
+ var code = v.charCodeAt(i);
45
+ if (code >= 55296 && code <= 56319 && i + 1 < n) {
46
+ out.push(v.charAt(i) + v.charAt(i + 1));
47
+ i += 2;
48
+ } else {
49
+ out.push(v.charAt(i));
50
+ i += 1;
51
+ }
52
+ }
53
+ return out;
54
+ }
55
+
22
56
  function styleOf(node) { return (node.attrs || {}).style || ''; }
23
57
  function srcOf(node) { return (node.attrs || {}).src || ''; }
58
+ function langOf(node) { return (node.attrs || {}).lang || ''; }
24
59
  function altOf(node) { return (node.attrs || {}).alt || ''; }
25
60
  function valueOf(node) { return (node.attrs || {}).value || ''; }
26
61
 
@@ -31,10 +66,16 @@ module.exports = {
31
66
  isImg: isImg,
32
67
  isHr: isHr,
33
68
  isPre: isPre,
69
+ isTable: isTable,
70
+ isCopy: isCopy,
71
+ copyOf: copyOf,
72
+ isRich: isRich,
34
73
  isSlot: isSlot,
35
74
  classOf: classOf,
75
+ charsOf: charsOf,
36
76
  styleOf: styleOf,
37
77
  srcOf: srcOf,
78
+ langOf: langOf,
38
79
  altOf: altOf,
39
80
  valueOf: valueOf,
40
81
  };
@@ -5,7 +5,18 @@
5
5
  animation-duration: var(--md-animation-duration, 300ms);
6
6
  }
7
7
 
8
+ /* 纯透明度淡入:不做 translateY 位移,避免流式逐字重渲染时块体上下抖动。 */
8
9
  @keyframes md-block-appear {
9
- 0% { opacity: 0; transform: translateY(4rpx); }
10
- 100% { opacity: 1; transform: translateY(0); }
10
+ from { opacity: 0.3; }
11
+ to { opacity: 1; }
12
+ }
13
+
14
+ /* 流式逐字淡入:每个新字符从透明渐显,已显示的字符(按下标复用)不会重播。 */
15
+ .md-anim-char {
16
+ opacity: 0;
17
+ animation: md-char-in 360ms ease-out forwards;
18
+ }
19
+ @keyframes md-char-in {
20
+ from { opacity: 0; }
21
+ to { opacity: 1; }
11
22
  }
@@ -5,6 +5,6 @@
5
5
  "mini-node-renderer": "../MiniNodeRenderer/index"
6
6
  },
7
7
  "componentGenerics": {
8
- "custom-slot": { "default": "view" }
8
+ "custom-slot": { "default": "../MiniNodeRenderer/index" }
9
9
  }
10
10
  }
@@ -45,11 +45,47 @@
45
45
 
46
46
  .md-code-block {
47
47
  display: block;
48
- margin: 12rpx 0;
49
48
  padding: 16rpx;
50
49
  background: #f6f8fa;
51
- border-radius: 8rpx;
52
50
  overflow-x: auto;
51
+ font-family: Menlo, Monaco, Consolas, "Courier New", monospace;
52
+ font-size: 26rpx;
53
+ line-height: 1.6;
54
+ white-space: pre;
55
+ }
56
+
57
+ /* code & table cards share a captioned header bar with an icon copy button */
58
+ .md-codeblock,
59
+ .md-tableblock {
60
+ margin: 16rpx 0;
61
+ border: 2rpx solid #eceef1;
62
+ border-radius: 16rpx;
63
+ overflow: hidden;
64
+ }
65
+ .md-codeblock-bar,
66
+ .md-tableblock-bar {
67
+ display: flex;
68
+ align-items: center;
69
+ justify-content: space-between;
70
+ height: 64rpx;
71
+ padding: 0 24rpx;
72
+ background: #eceef2;
73
+ font-size: 24rpx;
74
+ color: #4e5969;
75
+ }
76
+ .md-codeblock-lang {
77
+ text-transform: uppercase;
78
+ letter-spacing: 1rpx;
79
+ font-weight: 600;
80
+ color: #1d2129;
81
+ }
82
+ .md-tableblock-title {
83
+ font-weight: 600;
84
+ color: #1d2129;
85
+ }
86
+ .md-copy-icon {
87
+ width: 32rpx;
88
+ height: 32rpx;
53
89
  }
54
90
 
55
91
  .md-code {
@@ -78,6 +114,8 @@
78
114
  .md-img {
79
115
  display: block;
80
116
  max-width: 100%;
117
+ margin: 16rpx 0;
118
+ border-radius: 12rpx;
81
119
  }
82
120
 
83
121
  .md-hr {
@@ -87,17 +125,37 @@
87
125
  margin: 16rpx 0;
88
126
  }
89
127
 
90
- .md-table {
128
+ /* table body scrolls horizontally inside the .md-tableblock card */
129
+ .md-table-scroll {
91
130
  display: block;
92
131
  width: 100%;
93
- margin: 12rpx 0;
94
- border-collapse: collapse;
132
+ overflow: hidden;
133
+ white-space: nowrap;
95
134
  }
135
+ .md-table {
136
+ display: table;
137
+ table-layout: auto;
138
+ width: max-content;
139
+ min-width: 100%;
140
+ border-collapse: separate;
141
+ border-spacing: 0;
142
+ font-size: 26rpx;
143
+ }
144
+ .md-tr { display: table-row; }
96
145
  .md-th, .md-td {
97
146
  display: table-cell;
98
- padding: 8rpx 12rpx;
99
- border: 1rpx solid #e5e5e5;
147
+ padding: 12rpx 20rpx;
148
+ max-width: 480rpx;
149
+ border-right: 2rpx solid #e5e5e5;
150
+ border-bottom: 2rpx solid #e5e5e5;
151
+ text-align: left;
152
+ vertical-align: top;
153
+ word-break: break-word;
154
+ white-space: normal;
100
155
  }
101
156
  .md-th { background: #fafafa; font-weight: 600; }
157
+ .md-tr .md-th:last-child,
158
+ .md-tr .md-td:last-child { border-right: none; }
159
+ .md-table .md-tr:last-child .md-td { border-bottom: none; }
102
160
 
103
161
  .md-html { display: block; }
@@ -16,6 +16,14 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
16
16
  // src/components/wechat/MiniNodeRenderer/index.ts
17
17
  var MiniNodeRenderer_exports = {};
18
18
  module.exports = __toCommonJS(MiniNodeRenderer_exports);
19
+ function copyToClipboard(text) {
20
+ if (!text) return;
21
+ wx.setClipboardData({
22
+ data: text,
23
+ success: () => wx.showToast({ title: "\u5DF2\u590D\u5236", icon: "none", duration: 1200 }),
24
+ fail: () => wx.showToast({ title: "\u590D\u5236\u5931\u8D25", icon: "none" })
25
+ });
26
+ }
19
27
  Component({
20
28
  options: {
21
29
  multipleSlots: true,
@@ -34,6 +42,10 @@ Component({
34
42
  },
35
43
  _appear(e) {
36
44
  this.triggerEvent("appear", e, { bubbles: true, composed: true });
45
+ },
46
+ _copy(e) {
47
+ const ds = e && e.currentTarget && e.currentTarget.dataset;
48
+ copyToClipboard(ds && ds.copy || "");
37
49
  }
38
50
  }
39
51
  });
@@ -5,6 +5,6 @@
5
5
  "mini-node-renderer": "./index"
6
6
  },
7
7
  "componentGenerics": {
8
- "custom-slot": { "default": "view" }
8
+ "custom-slot": { "default": "./index" }
9
9
  }
10
10
  }
@@ -1,9 +1,21 @@
1
1
  <wxs src="./index.wxs" module="u" />
2
2
 
3
3
  <block wx:for="{{nodes}}" wx:for-item="node" wx:for-index="i" wx:key="i">
4
- <!-- text leaf -->
4
+ <!-- text leaf: 流式时逐字淡入(每字一个 <text>,按下标 key 复用,只有新字触发动画);
5
+ 非流式时整段渲染为单个 <text>。 -->
5
6
  <text
6
- wx:if="{{u.isText(node.name)}}"
7
+ wx:if="{{u.isText(node.name) && animation}}"
8
+ class="{{u.classOf(node)}}"
9
+ selectable="{{selectable}}"
10
+ ><text
11
+ wx:for="{{u.charsOf(node)}}"
12
+ wx:for-item="ch"
13
+ wx:for-index="ci"
14
+ wx:key="ci"
15
+ class="md-anim-char"
16
+ >{{ch}}</text></text>
17
+ <text
18
+ wx:elif="{{u.isText(node.name)}}"
7
19
  class="{{u.classOf(node)}}"
8
20
  selectable="{{selectable}}"
9
21
  >{{u.valueOf(node)}}</text>
@@ -25,26 +37,91 @@
25
37
  catch:tap="_tap"
26
38
  />
27
39
 
28
- <!-- pre: scrollable code container -->
29
- <scroll-view
40
+ <!-- pre: code block with language label + copy toolbar, scrollable body -->
41
+ <view
30
42
  wx:elif="{{u.isPre(node.name)}}"
31
- scroll-x
32
- class="{{u.classOf(node)}}"
33
- style="{{u.styleOf(node)}}"
34
- data-data="{{node}}"
35
- catch:tap="_tap"
43
+ class="md-codeblock {{node.animate ? 'md-animate-block' : ''}}"
36
44
  >
37
- <mini-node-renderer
38
- wx:if="{{node.children}}"
39
- generic:custom-slot="custom-slot"
40
- nodes="{{node.children}}"
41
- selectable="{{selectable}}"
42
- animation="{{false}}"
43
- slotComponents="{{slotComponents}}"
44
- bind:tap="_tap"
45
- bind:appear="_appear"
46
- />
47
- </scroll-view>
45
+ <view wx:if="{{node.header}}" class="md-codeblock-bar">
46
+ <block wx:for="{{node.header}}" wx:for-item="h" wx:for-index="hi" wx:key="hi">
47
+ <text wx:if="{{u.isText(h.name)}}" class="{{u.classOf(h)}}">{{u.valueOf(h)}}</text>
48
+ <image
49
+ wx:elif="{{u.isCopy(h.name)}}"
50
+ class="{{u.classOf(h) || 'md-copy-icon'}}"
51
+ src="https://mdn.alipayobjects.com/huamei_y8xg5f/afts/img/JXzqSoHDzm4AAAAAHpAAAAgADuJhAQFr/original"
52
+ data-copy="{{u.copyOf(h)}}"
53
+ catch:tap="_copy"
54
+ />
55
+ <view wx:else class="{{u.classOf(h)}}" style="{{u.styleOf(h)}}">
56
+ <mini-node-renderer
57
+ wx:if="{{h.children}}"
58
+ generic:custom-slot="custom-slot"
59
+ nodes="{{h.children}}"
60
+ selectable="{{selectable}}"
61
+ animation="{{false}}"
62
+ slotComponents="{{slotComponents}}"
63
+ bind:tap="_tap"
64
+ />
65
+ </view>
66
+ </block>
67
+ </view>
68
+ <scroll-view scroll-x class="md-code-block">
69
+ <mini-node-renderer
70
+ wx:if="{{node.children}}"
71
+ generic:custom-slot="custom-slot"
72
+ nodes="{{node.children}}"
73
+ selectable="{{selectable}}"
74
+ animation="{{false}}"
75
+ slotComponents="{{slotComponents}}"
76
+ bind:tap="_tap"
77
+ bind:appear="_appear"
78
+ />
79
+ </scroll-view>
80
+ </view>
81
+
82
+ <!-- table: caption + copy toolbar card, horizontal-scroll CSS table (columns kept aligned) -->
83
+ <view
84
+ wx:elif="{{u.isTable(node.name)}}"
85
+ class="md-tableblock {{node.animate ? 'md-animate-block' : ''}}"
86
+ >
87
+ <view wx:if="{{node.header}}" class="md-tableblock-bar">
88
+ <block wx:for="{{node.header}}" wx:for-item="h" wx:for-index="hi" wx:key="hi">
89
+ <text wx:if="{{u.isText(h.name)}}" class="{{u.classOf(h)}}">{{u.valueOf(h)}}</text>
90
+ <image
91
+ wx:elif="{{u.isCopy(h.name)}}"
92
+ class="{{u.classOf(h) || 'md-copy-icon'}}"
93
+ src="https://mdn.alipayobjects.com/huamei_y8xg5f/afts/img/JXzqSoHDzm4AAAAAHpAAAAgADuJhAQFr/original"
94
+ data-copy="{{u.copyOf(h)}}"
95
+ catch:tap="_copy"
96
+ />
97
+ <view wx:else class="{{u.classOf(h)}}" style="{{u.styleOf(h)}}">
98
+ <mini-node-renderer
99
+ wx:if="{{h.children}}"
100
+ generic:custom-slot="custom-slot"
101
+ nodes="{{h.children}}"
102
+ selectable="{{selectable}}"
103
+ animation="{{false}}"
104
+ slotComponents="{{slotComponents}}"
105
+ bind:tap="_tap"
106
+ />
107
+ </view>
108
+ </block>
109
+ </view>
110
+ <scroll-view scroll-x class="md-table-scroll">
111
+ <view class="md-table">
112
+ <mini-node-renderer
113
+ wx:if="{{node.children}}"
114
+ generic:custom-slot="custom-slot"
115
+ nodes="{{node.children}}"
116
+ selectable="{{selectable}}"
117
+ animation="{{false}}"
118
+ slotComponents="{{slotComponents}}"
119
+ bind:tap="_tap"
120
+ bind:appear="_appear"
121
+ />
122
+ </view>
123
+ </scroll-view>
124
+ </view>
48
125
 
49
126
  <!-- anchor: kept interactive, children are already flattened text runs -->
50
127
  <text
@@ -66,7 +143,7 @@
66
143
 
67
144
  <!-- inline tag rendered as <text>: children are flat text runs -->
68
145
  <text
69
- wx:elif="{{u.isInline(node.name)}}"
146
+ wx:elif="{{u.isInline(node.name) && !u.isRich(node)}}"
70
147
  class="{{u.classOf(node)}}"
71
148
  style="{{u.styleOf(node)}}"
72
149
  selectable="{{selectable}}"
@@ -6,6 +6,20 @@ function isBr(name) { return name === 'br'; }
6
6
  function isImg(name) { return name === 'img'; }
7
7
  function isHr(name) { return name === 'hr'; }
8
8
  function isPre(name) { return name === 'pre'; }
9
+ function isTable(name) { return name === 'table'; }
10
+ function isCopy(name) { return name === 'copy-button'; }
11
+ function copyOf(node) { return (node.attrs || {})['data-copy'] || ''; }
12
+ // 富内联节点(如 KaTeX 公式容器)含有元素子节点,必须走递归 <view> 路径
13
+ // 渲染,保留嵌套结构与 style;普通已扁平化的内联只有 text/br 子节点。
14
+ function isRich(node) {
15
+ var ch = node.children;
16
+ if (!ch) return false;
17
+ for (var i = 0; i < ch.length; i++) {
18
+ var nm = ch[i].name;
19
+ if (nm !== 'text' && nm !== 'br') return true;
20
+ }
21
+ return false;
22
+ }
9
23
  function isSlot(name, slotComponents) {
10
24
  return !!slotComponents && slotComponents.indexOf(name) > -1;
11
25
  }
@@ -19,8 +33,29 @@ function classOf(node) {
19
33
  return cls;
20
34
  }
21
35
 
36
+ // 把文本拆成「按 code point 安全」的字符数组(不劈坏 emoji/组合 surrogate pair),
37
+ // 供流式逐字淡入:每个字符渲染成独立 <text>,按下标 key 复用,只有新字符会触发淡入动画。
38
+ function charsOf(node) {
39
+ var v = (node.attrs || {}).value || '';
40
+ var out = [];
41
+ var i = 0;
42
+ var n = v.length;
43
+ while (i < n) {
44
+ var code = v.charCodeAt(i);
45
+ if (code >= 55296 && code <= 56319 && i + 1 < n) {
46
+ out.push(v.charAt(i) + v.charAt(i + 1));
47
+ i += 2;
48
+ } else {
49
+ out.push(v.charAt(i));
50
+ i += 1;
51
+ }
52
+ }
53
+ return out;
54
+ }
55
+
22
56
  function styleOf(node) { return (node.attrs || {}).style || ''; }
23
57
  function srcOf(node) { return (node.attrs || {}).src || ''; }
58
+ function langOf(node) { return (node.attrs || {}).lang || ''; }
24
59
  function altOf(node) { return (node.attrs || {}).alt || ''; }
25
60
  function valueOf(node) { return (node.attrs || {}).value || ''; }
26
61
 
@@ -31,10 +66,16 @@ module.exports = {
31
66
  isImg: isImg,
32
67
  isHr: isHr,
33
68
  isPre: isPre,
69
+ isTable: isTable,
70
+ isCopy: isCopy,
71
+ copyOf: copyOf,
72
+ isRich: isRich,
34
73
  isSlot: isSlot,
35
74
  classOf: classOf,
75
+ charsOf: charsOf,
36
76
  styleOf: styleOf,
37
77
  srcOf: srcOf,
78
+ langOf: langOf,
38
79
  altOf: altOf,
39
80
  valueOf: valueOf,
40
81
  };
@@ -5,7 +5,18 @@
5
5
  animation-duration: var(--md-animation-duration, 300ms);
6
6
  }
7
7
 
8
+ /* 纯透明度淡入:不做 translateY 位移,避免流式逐字重渲染时块体上下抖动。 */
8
9
  @keyframes md-block-appear {
9
- 0% { opacity: 0; transform: translateY(4rpx); }
10
- 100% { opacity: 1; transform: translateY(0); }
10
+ from { opacity: 0.3; }
11
+ to { opacity: 1; }
12
+ }
13
+
14
+ /* 流式逐字淡入:每个新字符从透明渐显,已显示的字符(按下标复用)不会重播。 */
15
+ .md-anim-char {
16
+ opacity: 0;
17
+ animation: md-char-in 360ms ease-out forwards;
18
+ }
19
+ @keyframes md-char-in {
20
+ from { opacity: 0; }
21
+ to { opacity: 1; }
11
22
  }