|
|
@@ -1,7 +1,20 @@
|
|
1
|
1
|
/**
|
|
2
|
|
- * AI 聊天气泡:将 Markdown 转为 mp-html 可渲染的 HTML(默认 uni_modules 包不含 markdown 插件)
|
|
|
2
|
+ * AI 聊天气泡:将 Markdown 转为 mp-html 可渲染的 HTML
|
|
|
3
|
+ * iOS App 的 rich-text 对 rpx、h1~h6(2em) 支持差,统一用 px + p 标签
|
|
3
|
4
|
*/
|
|
4
|
5
|
|
|
|
6
|
+const FONT = {
|
|
|
7
|
+ body: 14,
|
|
|
8
|
+ bodySm: 12,
|
|
|
9
|
+ h1: 15,
|
|
|
10
|
+ h2: 14.5,
|
|
|
11
|
+ h3: 14
|
|
|
12
|
+}
|
|
|
13
|
+
|
|
|
14
|
+function px(n) {
|
|
|
15
|
+ return `${n}px`
|
|
|
16
|
+}
|
|
|
17
|
+
|
|
5
|
18
|
function escapeHtml(s) {
|
|
6
|
19
|
return String(s)
|
|
7
|
20
|
.replace(/&/g, '&')
|
|
|
@@ -15,6 +28,24 @@ function looksLikeHtml(s) {
|
|
15
|
28
|
return /<\/?[a-z][\s\S]*>/i.test(s)
|
|
16
|
29
|
}
|
|
17
|
30
|
|
|
|
31
|
+/** iOS rich-text:h1~h6 改为 p+px,去掉 font/big 等会放大的标签 */
|
|
|
32
|
+function normalizeAiHtml(html) {
|
|
|
33
|
+ let s = String(html)
|
|
|
34
|
+ const headingPx = [FONT.h1, FONT.h2, FONT.h3, FONT.h3, FONT.h3, FONT.h3]
|
|
|
35
|
+ for (let level = 1; level <= 6; level += 1) {
|
|
|
36
|
+ const size = headingPx[level - 1]
|
|
|
37
|
+ const re = new RegExp(`<h${level}(\\s[^>]*)?>([\\s\\S]*?)</h${level}>`, 'gi')
|
|
|
38
|
+ s = s.replace(
|
|
|
39
|
+ re,
|
|
|
40
|
+ (_, _attrs, content) =>
|
|
|
41
|
+ `<p style="margin:8px 0 6px;font-size:${px(size)};font-weight:600;line-height:1.35;word-break:break-word;">${content}</p>`
|
|
|
42
|
+ )
|
|
|
43
|
+ }
|
|
|
44
|
+ s = s.replace(/<font\b[^>]*>/gi, '<span>').replace(/<\/font>/gi, '</span>')
|
|
|
45
|
+ s = s.replace(/<big\b[^>]*>/gi, '<span>').replace(/<\/big>/gi, '</span>')
|
|
|
46
|
+ return s
|
|
|
47
|
+}
|
|
|
48
|
+
|
|
18
|
49
|
/**
|
|
19
|
50
|
* @param {string} text 原始 Markdown 或纯文本
|
|
20
|
51
|
* @returns {string} HTML
|
|
|
@@ -25,7 +56,7 @@ export function markdownToHtml(text) {
|
|
25
|
56
|
}
|
|
26
|
57
|
const raw = String(text)
|
|
27
|
58
|
if (looksLikeHtml(raw)) {
|
|
28
|
|
- return raw
|
|
|
59
|
+ return normalizeAiHtml(raw)
|
|
29
|
60
|
}
|
|
30
|
61
|
|
|
31
|
62
|
const blocks = []
|
|
|
@@ -51,7 +82,14 @@ export function markdownToHtml(text) {
|
|
51
|
82
|
|
|
52
|
83
|
function formatCodeBlock(code) {
|
|
53
|
84
|
const body = escapeHtml(code.replace(/^\n|\n$/g, ''))
|
|
54
|
|
- return `<pre style="margin:8px 0;padding:10px 12px;border-radius:8px;background:#f6f8fa;overflow-x:auto;font-size:24rpx;line-height:1.45"><code>${body}</code></pre>`
|
|
|
85
|
+ return `<pre style="margin:8px 0;padding:10px 12px;border-radius:8px;background:#f6f8fa;overflow-x:auto;font-size:${px(
|
|
|
86
|
+ FONT.bodySm
|
|
|
87
|
+ )};line-height:1.45"><code>${body}</code></pre>`
|
|
|
88
|
+}
|
|
|
89
|
+
|
|
|
90
|
+function headingStyle(level) {
|
|
|
91
|
+ const size = level === 1 ? FONT.h1 : level === 2 ? FONT.h2 : FONT.h3
|
|
|
92
|
+ return `margin:8px 0 6px;font-size:${px(size)};font-weight:600;line-height:1.35;word-break:break-word;`
|
|
55
|
93
|
}
|
|
56
|
94
|
|
|
57
|
95
|
function formatMdBlock(text) {
|
|
|
@@ -71,10 +109,7 @@ function formatMdBlock(text) {
|
|
71
|
109
|
if (/^#{1,3}\s+/.test(trimmed)) {
|
|
72
|
110
|
const level = trimmed.match(/^(#+)/)[1].length
|
|
73
|
111
|
const content = inlineFormat(escapeHtml(trimmed.replace(/^#+\s+/, '')))
|
|
74
|
|
- const size = level === 1 ? '32rpx' : level === 2 ? '30rpx' : '28rpx'
|
|
75
|
|
- out.push(
|
|
76
|
|
- `<div style="margin:12px 0 8px;font-size:${size};font-weight:600;line-height:1.35">${content}</div>`
|
|
77
|
|
- )
|
|
|
112
|
+ out.push(`<p style="${headingStyle(level)}">${content}</p>`)
|
|
78
|
113
|
i += 1
|
|
79
|
114
|
continue
|
|
80
|
115
|
}
|
|
|
@@ -86,8 +121,8 @@ function formatMdBlock(text) {
|
|
86
|
121
|
i += 1
|
|
87
|
122
|
}
|
|
88
|
123
|
out.push(
|
|
89
|
|
- `<ul style="margin:6px 0 12px;padding-left:1.25em">${items
|
|
90
|
|
- .map((li) => `<li style="margin:4px 0;line-height:1.5">${li}</li>`)
|
|
|
124
|
+ `<ul style="margin:6px 0 12px;padding-left:1.25em;font-size:${px(FONT.body)}">${items
|
|
|
125
|
+ .map((li) => `<li style="margin:4px 0;line-height:1.5;font-size:${px(FONT.body)}">${li}</li>`)
|
|
91
|
126
|
.join('')}</ul>`
|
|
92
|
127
|
)
|
|
93
|
128
|
continue
|
|
|
@@ -100,8 +135,8 @@ function formatMdBlock(text) {
|
|
100
|
135
|
i += 1
|
|
101
|
136
|
}
|
|
102
|
137
|
out.push(
|
|
103
|
|
- `<ol style="margin:6px 0 12px;padding-left:1.25em">${items
|
|
104
|
|
- .map((li) => `<li style="margin:4px 0;line-height:1.5">${li}</li>`)
|
|
|
138
|
+ `<ol style="margin:6px 0 12px;padding-left:1.25em;font-size:${px(FONT.body)}">${items
|
|
|
139
|
+ .map((li) => `<li style="margin:4px 0;line-height:1.5;font-size:${px(FONT.body)}">${li}</li>`)
|
|
105
|
140
|
.join('')}</ol>`
|
|
106
|
141
|
)
|
|
107
|
142
|
continue
|
|
|
@@ -115,7 +150,9 @@ function formatMdBlock(text) {
|
|
115
|
150
|
}
|
|
116
|
151
|
const body = quoteLines.map((l) => inlineFormat(escapeHtml(l))).join('<br/>')
|
|
117
|
152
|
out.push(
|
|
118
|
|
- `<blockquote style="margin:8px 0;padding:6px 12px;border-left:4px solid #22c55e;background:rgba(255,255,255,0.55)">${body}</blockquote>`
|
|
|
153
|
+ `<blockquote style="margin:8px 0;padding:6px 12px;border-left:4px solid #22c55e;background:rgba(255,255,255,0.55);font-size:${px(
|
|
|
154
|
+ FONT.body
|
|
|
155
|
+ )}">${body}</blockquote>`
|
|
119
|
156
|
)
|
|
120
|
157
|
continue
|
|
121
|
158
|
}
|
|
|
@@ -126,35 +163,57 @@ function formatMdBlock(text) {
|
|
126
|
163
|
i += 1
|
|
127
|
164
|
}
|
|
128
|
165
|
const body = inlineFormat(escapeHtml(para.join('\n'))).replace(/\n/g, '<br/>')
|
|
129
|
|
- out.push(`<p style="margin:0 0 10px;line-height:1.55;word-break:break-word">${body}</p>`)
|
|
|
166
|
+ out.push(
|
|
|
167
|
+ `<p style="margin:0 0 10px;line-height:1.55;word-break:break-word;font-size:${px(FONT.body)}">${body}</p>`
|
|
|
168
|
+ )
|
|
130
|
169
|
}
|
|
131
|
170
|
|
|
132
|
|
- return out.join('') || `<p style="margin:0;line-height:1.55">${inlineFormat(escapeHtml(text))}</p>`
|
|
|
171
|
+ return (
|
|
|
172
|
+ out.join('') ||
|
|
|
173
|
+ `<p style="margin:0;line-height:1.55;font-size:${px(FONT.body)}">${inlineFormat(escapeHtml(text))}</p>`
|
|
|
174
|
+ )
|
|
133
|
175
|
}
|
|
134
|
176
|
|
|
135
|
177
|
function inlineFormat(s) {
|
|
136
|
178
|
return s
|
|
137
|
|
- .replace(/`([^`]+)`/g, '<code style="padding:2px 6px;border-radius:4px;background:rgba(0,0,0,0.06);font-size:24rpx">$1</code>')
|
|
|
179
|
+ .replace(
|
|
|
180
|
+ /`([^`]+)`/g,
|
|
|
181
|
+ `<code style="padding:2px 6px;border-radius:4px;background:rgba(0,0,0,0.06);font-size:${px(FONT.bodySm)}">$1</code>`
|
|
|
182
|
+ )
|
|
138
|
183
|
.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>')
|
|
139
|
184
|
.replace(/\*([^*]+)\*/g, '<em>$1</em>')
|
|
140
|
185
|
}
|
|
141
|
186
|
|
|
142
|
|
-/** mp-html tag-style 默认样式(对象形式,见插件文档) */
|
|
|
187
|
+const bodyStyle = `font-size:${px(FONT.body)};line-height:1.55;word-break:break-word;`
|
|
|
188
|
+
|
|
|
189
|
+/** mp-html tag-style(统一 px,避免 iOS rich-text 忽略 rpx) */
|
|
143
|
190
|
export const MP_HTML_TAG_STYLE = {
|
|
144
|
|
- p: 'margin:0 0 10px;line-height:1.55;word-break:break-word;',
|
|
145
|
|
- div: 'word-break:break-word;',
|
|
146
|
|
- ul: 'margin:6px 0 12px;padding-left:1.25em;',
|
|
147
|
|
- ol: 'margin:6px 0 12px;padding-left:1.25em;',
|
|
148
|
|
- li: 'margin:4px 0;line-height:1.5;',
|
|
149
|
|
- pre: 'margin:8px 0;padding:10px 12px;border-radius:8px;background:#f6f8fa;overflow-x:auto;',
|
|
150
|
|
- code: 'font-family:monospace;font-size:24rpx;',
|
|
151
|
|
- blockquote:
|
|
152
|
|
- 'margin:8px 0;padding:6px 12px;border-left:4px solid #22c55e;background:rgba(255,255,255,0.55);',
|
|
153
|
|
- a: 'color:#16a34a;text-decoration:underline;',
|
|
154
|
|
- table: 'border-collapse:collapse;margin:8px 0;font-size:24rpx;max-width:100%;',
|
|
155
|
|
- th: 'border:1px solid #ddd4c8;padding:6px 10px;background:#f0ebe3;',
|
|
156
|
|
- td: 'border:1px solid #ddd4c8;padding:6px 10px;'
|
|
|
191
|
+ p: `margin:0 0 10px;${bodyStyle}`,
|
|
|
192
|
+ div: bodyStyle,
|
|
|
193
|
+ h1: `display:block;margin:8px 0 6px;font-size:${px(FONT.h1)};font-weight:600;line-height:1.35;`,
|
|
|
194
|
+ h2: `display:block;margin:8px 0 6px;font-size:${px(FONT.h2)};font-weight:600;line-height:1.35;`,
|
|
|
195
|
+ h3: `display:block;margin:8px 0 6px;font-size:${px(FONT.h3)};font-weight:600;line-height:1.35;`,
|
|
|
196
|
+ h4: `display:block;margin:8px 0 4px;font-size:${px(FONT.h3)};font-weight:600;line-height:1.35;`,
|
|
|
197
|
+ h5: `display:block;margin:6px 0 4px;font-size:${px(FONT.h3)};font-weight:600;line-height:1.35;`,
|
|
|
198
|
+ h6: `display:block;margin:6px 0 4px;font-size:${px(FONT.h3)};font-weight:600;line-height:1.35;`,
|
|
|
199
|
+ ul: `margin:6px 0 12px;padding-left:1.25em;font-size:${px(FONT.body)};`,
|
|
|
200
|
+ ol: `margin:6px 0 12px;padding-left:1.25em;font-size:${px(FONT.body)};`,
|
|
|
201
|
+ li: `margin:4px 0;line-height:1.5;font-size:${px(FONT.body)};`,
|
|
|
202
|
+ pre: `margin:8px 0;padding:10px 12px;border-radius:8px;background:#f6f8fa;overflow-x:auto;font-size:${px(FONT.bodySm)};`,
|
|
|
203
|
+ code: `font-family:monospace;font-size:${px(FONT.bodySm)};`,
|
|
|
204
|
+ blockquote: `margin:8px 0;padding:6px 12px;border-left:4px solid #22c55e;background:rgba(255,255,255,0.55);font-size:${px(
|
|
|
205
|
+ FONT.body
|
|
|
206
|
+ )};`,
|
|
|
207
|
+ a: `color:#16a34a;text-decoration:underline;font-size:${px(FONT.body)};`,
|
|
|
208
|
+ strong: 'font-weight:600;',
|
|
|
209
|
+ em: 'font-style:italic;',
|
|
|
210
|
+ big: `display:inline;font-size:${px(FONT.body)};`,
|
|
|
211
|
+ small: `display:inline;font-size:${px(FONT.bodySm)};`,
|
|
|
212
|
+ table: `border-collapse:collapse;margin:8px 0;font-size:${px(FONT.bodySm)};max-width:100%;`,
|
|
|
213
|
+ th: `border:1px solid #ddd4c8;padding:6px 10px;background:#f0ebe3;font-size:${px(FONT.bodySm)};`,
|
|
|
214
|
+ td: `border:1px solid #ddd4c8;padding:6px 10px;font-size:${px(FONT.bodySm)};`
|
|
157
|
215
|
}
|
|
158
|
216
|
|
|
159
|
|
-export const MP_HTML_CONTAINER_STYLE =
|
|
160
|
|
- 'display:block;width:100%;min-width:0;overflow:visible;line-height:1.55;font-size:28rpx;color:#4a4542;word-break:break-word;'
|
|
|
217
|
+export const MP_HTML_CONTAINER_STYLE = `display:block;width:100%;min-width:0;overflow:visible;line-height:1.55;font-size:${px(
|
|
|
218
|
+ FONT.body
|
|
|
219
|
+)};color:#4a4542;word-break:break-word;-webkit-text-size-adjust:100%;text-size-adjust:100%;`
|