dsh-obsidian-notes 0.0.0-stage → 0.5.0
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 +282 -2
- package/README.zh.md +198 -0
- package/cordis.patch.yml +11 -0
- package/lib/client.js +4316 -0
- package/lib/types/index.d.ts +3 -0
- package/package.json +70 -4
- package/src/client.js +3215 -0
- package/src/index.js +1293 -0
- package/src/markdown.js +1089 -0
- package/src/session-store.js +146 -0
- package/src/vault-store.js +141 -0
package/src/markdown.js
ADDED
|
@@ -0,0 +1,1089 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 轻量 Markdown 渲染器(无第三方依赖,纯 React.createElement),两个出口、
|
|
3
|
+
* 两套块级能力,共同复用行内解析 renderInline(粗斜体/行内代码/链接/图片/双链):
|
|
4
|
+
*
|
|
5
|
+
* 1) renderNumbered —— 笔记面板预览**实际使用**的渲染器(一行源码 = 一行 + 行号)。
|
|
6
|
+
* 块级支持:引用(逐行、嵌套,不再吞行)、callout(类型/别名/标题,正文里的
|
|
7
|
+
* 列表与标题)、代码围栏(```/~~~、info string、未闭合到文末,行号保留)、
|
|
8
|
+
* 列表(无序/有序/嵌套,按实际缩进,tab=4)、任务列表(只读复选框)、
|
|
9
|
+
* 表格(整块真表格、行号在首列、对齐、\| 转义)、标题分级(#~######)、
|
|
10
|
+
* 分隔线、setext 标题(=== / ---)。frontmatter 由服务端读时剥离,不进入预览。
|
|
11
|
+
* 2) renderMarkdown —— 旧的块渲染器,**当前没有产品调用点**;能力比行号路径旧
|
|
12
|
+
* (标题降级、无 setext/任务列表等)。保留但不要再往它上面补面板语法,
|
|
13
|
+
* 面板行为一律以 renderNumbered 为准。
|
|
14
|
+
*
|
|
15
|
+
* 未支持(P2,保持原文显示):数学、mermaid、脚注、删除线/高亮/转义/双反引号、
|
|
16
|
+
* 自动链接、标签、HTML、笔记 transclusion、音频/PDF 嵌入、块引用 ^id、
|
|
17
|
+
* callout 折叠/未知类型/嵌套、aliases 解析。行内强调仍沿用旧解析器(`_` 相邻文本
|
|
18
|
+
* 的边界处理有已知限制),统一留到 P2。
|
|
19
|
+
*
|
|
20
|
+
* 偏移记账(供「选中片段提问」使用):
|
|
21
|
+
* 当 handlers.onText 为函数时,每个文本片段会被包成 <span className="dsh-obs-src"
|
|
22
|
+
* data-obs-src="start,end">,其中 start/end 是该片段在源 markdown 字符串中的绝对
|
|
23
|
+
* 偏移,满足 MD.slice(start,end) === 片段内容。宿主可据此把 DOM 选区反查回源码。
|
|
24
|
+
* 未传 onText 时行为与旧版完全一致(纯 React 文本节点,不产生额外元素)。
|
|
25
|
+
* 依赖工厂闭包内的 `React`(require("react"))。
|
|
26
|
+
*/
|
|
27
|
+
const renderMarkdownModule = (() => {
|
|
28
|
+
const createElement = React.createElement;
|
|
29
|
+
const Fragment = React.Fragment;
|
|
30
|
+
|
|
31
|
+
function escapeHtml(s) {
|
|
32
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function safeUrl(url) {
|
|
36
|
+
const trimmed = url.trim();
|
|
37
|
+
if (/^(https?:|mailto:|#|\/|\.\/|\.\.\/)/i.test(trimmed)) return trimmed;
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// 取扩展名(小写带点,无则返回 "")
|
|
42
|
+
function extOf(p) {
|
|
43
|
+
const m = /\.([a-z0-9]+)$/i.exec(p);
|
|
44
|
+
return m ? `.${m[1].toLowerCase()}` : "";
|
|
45
|
+
}
|
|
46
|
+
// 取路径最后一段(POSIX)
|
|
47
|
+
function baseOf(p) {
|
|
48
|
+
const i = p.lastIndexOf("/");
|
|
49
|
+
return i >= 0 ? p.slice(i + 1) : p;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// 图片扩展名白名单(用于 embed 目标判定)
|
|
53
|
+
const IMAGE_EXT = new Set([".png", ".jpg", ".jpeg", ".gif", ".webp"]);
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Obsidian 的图片尺寸语法:`![[a.png|100]]`(宽)/ `![[a.png|100x200]]`(宽x高)。
|
|
57
|
+
* 非纯数字的别名不是尺寸(Obsidian 里当 alt 文本用),返回 null。
|
|
58
|
+
*/
|
|
59
|
+
function parseImageSize(alias) {
|
|
60
|
+
const m = /^(\d+)(?:x(\d+))?$/.exec(alias);
|
|
61
|
+
if (!m) return null;
|
|
62
|
+
const size = { width: Number(m[1]) };
|
|
63
|
+
if (m[2] !== undefined) size.height = Number(m[2]);
|
|
64
|
+
return size;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* 把「图片 src / embed 目标」映射为可显示的 URL:
|
|
69
|
+
* 外部 URL、mailto / # / data: / 服务端绝对路径 → 直接可用;
|
|
70
|
+
* 其它(Vault 相对路径,如 attachments/x.png)→ 交给 handlers.assetUrl 转成
|
|
71
|
+
* `/obsidian/file?path=...`;无 assetUrl 时返回 null(退化为文本,不渲染成图)。
|
|
72
|
+
*/
|
|
73
|
+
function resolveSrc(src, handlers) {
|
|
74
|
+
if (typeof src !== "string") return null;
|
|
75
|
+
const t = src.trim();
|
|
76
|
+
if (/^(https?:|mailto:|#|\/|data:)/i.test(t)) return t;
|
|
77
|
+
if (handlers && typeof handlers.assetUrl === "function") return handlers.assetUrl(t);
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// ── 行内解析(code > img > embed > link > wikilink > strong > em,取最左匹配)──
|
|
82
|
+
// M7:双链/embed 分开捕获 target(m[1])、章节(m[2])、别名(m[3]),
|
|
83
|
+
// 显示时别名优先、其次「target › 章节」,偏移各自映射到源码切片。
|
|
84
|
+
const INLINE_PATTERNS = [
|
|
85
|
+
["code", /`([^`\n]+)`/],
|
|
86
|
+
["img", /!\[([^\]]*)\]\(([^)\s]+)\)/],
|
|
87
|
+
["embed", /!\[\[([^\]|#\n]*)(?:#([^\]|\n]*))?(?:\|([^\]\n]*))?\]\]/],
|
|
88
|
+
["link", /\[([^\]]+)\]\(([^)\s]+)\)/],
|
|
89
|
+
["wiki", /\[\[([^\]|#\n]*)(?:#([^\]|\n]*))?(?:\|([^\]\n]*))?\]\]/],
|
|
90
|
+
["strong", /\*\*([^*\n]+)\*\*|__([^_\n]+)__/],
|
|
91
|
+
["em", /(^|[^*])\*([^*\n]+)\*(?!\*)|(^|[^_])_([^_\n]+)_(?!_)/],
|
|
92
|
+
];
|
|
93
|
+
const patternIndex = (name) => INLINE_PATTERNS.findIndex((p) => p[0] === name);
|
|
94
|
+
|
|
95
|
+
/** 文本片段 → React 节点;handlers.onText 存在时包成带偏移的 span */
|
|
96
|
+
function textNode(text, start, end, key, handlers) {
|
|
97
|
+
if (text === "") return null;
|
|
98
|
+
if (handlers && typeof handlers.onText === "function") {
|
|
99
|
+
return createElement("span", { key, className: "dsh-obs-src", "data-obs-src": `${start},${end}` }, text);
|
|
100
|
+
}
|
|
101
|
+
return createElement(Fragment, { key }, text);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* 双链 / embed 的源码切分(M7):m 为 INLINE_PATTERNS 的匹配结果,
|
|
106
|
+
* mStart 为 m[0] 在源码里的绝对起点。返回各段的「去掉首尾空白后的文本 + 精确起点」:
|
|
107
|
+
* target(m[1])、section(m[2],`#` 之后)、alias(m[3],`|` 之后)。
|
|
108
|
+
* has* 记录分隔符是否出现(空 section/alias 也保留语义)。
|
|
109
|
+
*/
|
|
110
|
+
function wikilinkParts(m, mStart, embed) {
|
|
111
|
+
const innerStart = mStart + (embed ? 3 : 2); // 跳过 "![[" / "[["
|
|
112
|
+
const targetRaw = m[1];
|
|
113
|
+
const targetLead = targetRaw.length - targetRaw.trimStart().length;
|
|
114
|
+
const target = targetRaw.trim();
|
|
115
|
+
const afterTarget = innerStart + targetRaw.length;
|
|
116
|
+
const secRaw = m[2];
|
|
117
|
+
const section = secRaw === undefined ? "" : secRaw.trim();
|
|
118
|
+
const sectionLead = secRaw === undefined ? 0 : secRaw.length - secRaw.trimStart().length;
|
|
119
|
+
const sectionStart = secRaw === undefined ? -1 : afterTarget + 1 + sectionLead;
|
|
120
|
+
const aliasRaw = m[3];
|
|
121
|
+
const alias = aliasRaw === undefined ? "" : aliasRaw.trim();
|
|
122
|
+
const aliasLead = aliasRaw === undefined ? 0 : aliasRaw.length - aliasRaw.trimStart().length;
|
|
123
|
+
const aliasStart = aliasRaw === undefined
|
|
124
|
+
? -1
|
|
125
|
+
: afterTarget + (secRaw === undefined ? 0 : 1 + secRaw.length) + 1 + aliasLead;
|
|
126
|
+
return {
|
|
127
|
+
target,
|
|
128
|
+
targetStart: innerStart + targetLead,
|
|
129
|
+
section,
|
|
130
|
+
sectionStart,
|
|
131
|
+
hasSection: secRaw !== undefined,
|
|
132
|
+
alias,
|
|
133
|
+
aliasStart,
|
|
134
|
+
hasAlias: aliasRaw !== undefined,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* 双链显示节点(M7,契约 §4.7):[[A]] → A;[[A|别名]] → 别名;
|
|
140
|
+
* [[A#标题]] → A › 标题(两个源码 span,中间 ` › ` 是非 span 装饰文本);[[A#^块]] → A › ^块。
|
|
141
|
+
* 可见文本都落在 span.dsh-obs-src 上,` › ` 只是装饰,不包 data-obs-src。
|
|
142
|
+
*/
|
|
143
|
+
function wikilinkDisplayNodes(parts, m, mStart, keyBase, handlers) {
|
|
144
|
+
const out = [];
|
|
145
|
+
if (parts.hasAlias && parts.alias !== "") {
|
|
146
|
+
const n = textNode(parts.alias, parts.aliasStart, parts.aliasStart + parts.alias.length, `${keyBase}-a`, handlers);
|
|
147
|
+
if (n) out.push(n);
|
|
148
|
+
return out;
|
|
149
|
+
}
|
|
150
|
+
if (parts.hasSection && parts.section !== "") {
|
|
151
|
+
if (parts.target !== "") {
|
|
152
|
+
const tn = textNode(parts.target, parts.targetStart, parts.targetStart + parts.target.length, `${keyBase}-t`, handlers);
|
|
153
|
+
if (tn) out.push(tn);
|
|
154
|
+
out.push(createElement(Fragment, { key: `${keyBase}-sep` }, " › "));
|
|
155
|
+
}
|
|
156
|
+
const sn = textNode(parts.section, parts.sectionStart, parts.sectionStart + parts.section.length, `${keyBase}-s`, handlers);
|
|
157
|
+
if (sn) out.push(sn);
|
|
158
|
+
return out;
|
|
159
|
+
}
|
|
160
|
+
const n = textNode(parts.target, parts.targetStart, parts.targetStart + parts.target.length, `${keyBase}-t`, handlers);
|
|
161
|
+
if (n) out.push(n);
|
|
162
|
+
// 极端输入(如 [[]])退化为源码原文,避免出现空按钮
|
|
163
|
+
if (out.length === 0) {
|
|
164
|
+
const raw = textNode(m[0], mStart, mStart + m[0].length, `${keyBase}-raw`, handlers);
|
|
165
|
+
if (raw) out.push(raw);
|
|
166
|
+
}
|
|
167
|
+
return out;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* 行内解析。text 对应源码区间 [srcStart, srcStart + text.length)。
|
|
172
|
+
* 返回节点数组。
|
|
173
|
+
*/
|
|
174
|
+
function renderInline(text, handlers, keyBase, srcStart) {
|
|
175
|
+
const nodes = [];
|
|
176
|
+
let cur = srcStart;
|
|
177
|
+
let rest = text;
|
|
178
|
+
let keyIndex = 0;
|
|
179
|
+
const pushText = (s) => {
|
|
180
|
+
const n = textNode(s, cur, cur + s.length, `${keyBase}-t${keyIndex++}`, handlers);
|
|
181
|
+
if (n) nodes.push(n);
|
|
182
|
+
cur += s.length;
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
while (rest !== "") {
|
|
186
|
+
const matches = [];
|
|
187
|
+
for (const [name, re] of INLINE_PATTERNS) {
|
|
188
|
+
const m = re.exec(rest);
|
|
189
|
+
if (m) matches.push({ name, m });
|
|
190
|
+
}
|
|
191
|
+
if (matches.length === 0) { pushText(rest); return nodes; }
|
|
192
|
+
matches.sort((a, b) => (a.m.index - b.m.index) || (patternIndex(a.name) - patternIndex(b.name)));
|
|
193
|
+
const { name, m } = matches[0];
|
|
194
|
+
if (m.index > 0) pushText(rest.slice(0, m.index));
|
|
195
|
+
|
|
196
|
+
const contentLen = (() => {
|
|
197
|
+
if (name === "code") return m[1].length;
|
|
198
|
+
if (name === "img") return 0; // alt 不渲染为可选中文本
|
|
199
|
+
if (name === "embed") return m[0].length; // 按钮/图占整个 ![[...]] 源码
|
|
200
|
+
if (name === "link" || name === "wiki") return m[1].length;
|
|
201
|
+
if (name === "strong") return (m[1] ?? m[2]).length;
|
|
202
|
+
return (m[2] ?? m[4]).length; // em(m[0] 含前缀组)
|
|
203
|
+
})();
|
|
204
|
+
const openLen = (() => {
|
|
205
|
+
if (name === "code") return 1; // `
|
|
206
|
+
if (name === "link") return 1; // [
|
|
207
|
+
if (name === "wiki") return 2; // [[
|
|
208
|
+
if (name === "embed") return 0; // 以整段 ![[...]] 作为可选中源码
|
|
209
|
+
if (name === "strong") return 2; // ** 或 __
|
|
210
|
+
if (name === "em") return m[0].length - contentLen - 1; // 前缀 + *
|
|
211
|
+
return 0;
|
|
212
|
+
})();
|
|
213
|
+
const bodyStart = cur + openLen; // cur 已含 m.index
|
|
214
|
+
|
|
215
|
+
if (name === "code") {
|
|
216
|
+
const n = textNode(m[1], bodyStart, bodyStart + m[1].length, `${keyBase}-c${keyIndex++}`, handlers);
|
|
217
|
+
if (n) nodes.push(n);
|
|
218
|
+
} else if (name === "img") {
|
|
219
|
+
const src = resolveSrc(m[2], handlers);
|
|
220
|
+
nodes.push(src === null
|
|
221
|
+
? textNode(m[0], cur, cur + m[0].length, `${keyBase}-i${keyIndex++}`, handlers)
|
|
222
|
+
: createElement("img", { key: `${keyBase}-i${keyIndex++}`, src, alt: m[1], className: "dsh-obs-md-img" }));
|
|
223
|
+
} else if (name === "embed") {
|
|
224
|
+
const parts = wikilinkParts(m, cur, true);
|
|
225
|
+
const target = parts.target;
|
|
226
|
+
const alias = parts.alias;
|
|
227
|
+
// 图片 embed → <img src=/obsidian/file?path=...>;其它 → 退回双链按钮(不做笔记内容嵌入)
|
|
228
|
+
if (IMAGE_EXT.has(extOf(target))) {
|
|
229
|
+
const src = resolveSrc(target, handlers);
|
|
230
|
+
const size = parseImageSize(alias);
|
|
231
|
+
nodes.push(src === null
|
|
232
|
+
? textNode(m[0], cur, cur + m[0].length, `${keyBase}-e${keyIndex++}`, handlers)
|
|
233
|
+
: createElement("img", {
|
|
234
|
+
key: `${keyBase}-e${keyIndex++}`,
|
|
235
|
+
src,
|
|
236
|
+
// Obsidian:`|100` / `|100x200` 是尺寸,其余别名当 alt
|
|
237
|
+
alt: size === null && alias !== "" ? alias : baseOf(target),
|
|
238
|
+
className: "dsh-obs-md-img",
|
|
239
|
+
...(size === null ? {} : size),
|
|
240
|
+
}));
|
|
241
|
+
} else {
|
|
242
|
+
// M7:非图片 embed 显示目标/别名/章节,不再显示 ![[ ]] 原文
|
|
243
|
+
const key = `${keyBase}-e${keyIndex++}`;
|
|
244
|
+
nodes.push(createElement(
|
|
245
|
+
"button",
|
|
246
|
+
{
|
|
247
|
+
key,
|
|
248
|
+
type: "button",
|
|
249
|
+
className: "dsh-obs-wikilink",
|
|
250
|
+
title: `[[${target}]]`,
|
|
251
|
+
onClick: () => {
|
|
252
|
+
if (handlers.onWikilink) handlers.onWikilink(target);
|
|
253
|
+
},
|
|
254
|
+
},
|
|
255
|
+
wikilinkDisplayNodes(parts, m, cur, key, handlers),
|
|
256
|
+
));
|
|
257
|
+
}
|
|
258
|
+
} else if (name === "link") {
|
|
259
|
+
const href = safeUrl(m[2]);
|
|
260
|
+
if (href === null) {
|
|
261
|
+
nodes.push(textNode(m[0], cur, cur + m[0].length, `${keyBase}-l${keyIndex++}`, handlers));
|
|
262
|
+
} else {
|
|
263
|
+
nodes.push(createElement("a", { key: `${keyBase}-l${keyIndex++}`, href, target: "_blank", rel: "noreferrer" },
|
|
264
|
+
renderInline(m[1], handlers, `${keyBase}-l${keyIndex}`, bodyStart)));
|
|
265
|
+
}
|
|
266
|
+
} else if (name === "wiki") {
|
|
267
|
+
// M7:显示 target / 别名 / target › 章节;title 与 onClick 参数仍是 target(不含 # 章节)
|
|
268
|
+
const parts = wikilinkParts(m, cur, false);
|
|
269
|
+
const target = parts.target;
|
|
270
|
+
const key = `${keyBase}-w${keyIndex++}`;
|
|
271
|
+
nodes.push(createElement(
|
|
272
|
+
"button",
|
|
273
|
+
{
|
|
274
|
+
key,
|
|
275
|
+
type: "button",
|
|
276
|
+
className: "dsh-obs-wikilink",
|
|
277
|
+
title: `[[${target}]]`,
|
|
278
|
+
onClick: () => {
|
|
279
|
+
if (handlers.onWikilink) handlers.onWikilink(target);
|
|
280
|
+
},
|
|
281
|
+
},
|
|
282
|
+
wikilinkDisplayNodes(parts, m, cur, key, handlers),
|
|
283
|
+
));
|
|
284
|
+
} else {
|
|
285
|
+
const body = name === "strong" ? (m[1] ?? m[2]) : (m[2] ?? m[4]);
|
|
286
|
+
nodes.push(createElement(
|
|
287
|
+
name === "strong" ? "strong" : "em",
|
|
288
|
+
{ key: `${keyBase}-s${keyIndex++}` },
|
|
289
|
+
renderInline(body, handlers, `${keyBase}-s${keyIndex}`, bodyStart),
|
|
290
|
+
));
|
|
291
|
+
}
|
|
292
|
+
cur += m[0].length;
|
|
293
|
+
rest = rest.slice(m.index + m[0].length);
|
|
294
|
+
}
|
|
295
|
+
return nodes;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* 单元格源码 → 渲染片段:按源码连续切段,`\|` 单独成 {type:"pipe"} 原子
|
|
300
|
+
* (显示 `|`,偏移只覆盖 `|` 那一个字符,不含反斜杠)。
|
|
301
|
+
*/
|
|
302
|
+
function buildCellSegments(text, start) {
|
|
303
|
+
const segs = [];
|
|
304
|
+
let buf = "";
|
|
305
|
+
let bufStart = 0;
|
|
306
|
+
let i = 0;
|
|
307
|
+
while (i < text.length) {
|
|
308
|
+
if (text[i] === "\\" && text[i + 1] === "|") {
|
|
309
|
+
if (buf !== "") segs.push({ type: "text", text: buf, start: start + bufStart });
|
|
310
|
+
segs.push({ type: "pipe", start: start + i + 1 });
|
|
311
|
+
buf = "";
|
|
312
|
+
bufStart = i + 2;
|
|
313
|
+
i += 2;
|
|
314
|
+
continue;
|
|
315
|
+
}
|
|
316
|
+
buf += text[i];
|
|
317
|
+
i += 1;
|
|
318
|
+
}
|
|
319
|
+
if (buf !== "") segs.push({ type: "text", text: buf, start: start + bufStart });
|
|
320
|
+
return segs;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* 表格行 → [{text, start, segments}]:
|
|
325
|
+
* - 修旧 bug:行首/行尾 `|` 只作分隔,不再多推一个空单元格(`a || b` 中间空列保留);
|
|
326
|
+
* - `\|` 不切列,显示 `|`,偏移只记 `|`(segments 交给 renderInlineCell 精确记账);
|
|
327
|
+
* - `[[ ]]` 内的 `|` 不切列;
|
|
328
|
+
* - text/start 保持旧形状,块渲染器继续可用。
|
|
329
|
+
*/
|
|
330
|
+
function splitTableRow(line, lineStart) {
|
|
331
|
+
const parts = [];
|
|
332
|
+
let bufStart = 0;
|
|
333
|
+
let j = 0;
|
|
334
|
+
let depth = 0;
|
|
335
|
+
while (j < line.length) {
|
|
336
|
+
const ch = line[j];
|
|
337
|
+
if (ch === "\\" && line[j + 1] === "|") { j += 2; continue; }
|
|
338
|
+
if (ch === "|" && depth === 0) {
|
|
339
|
+
parts.push({ raw: line.slice(bufStart, j), start: lineStart + bufStart });
|
|
340
|
+
j += 1;
|
|
341
|
+
bufStart = j;
|
|
342
|
+
continue;
|
|
343
|
+
}
|
|
344
|
+
if (ch === "[" && line[j + 1] === "[") { depth = 1; j += 2; continue; }
|
|
345
|
+
if (ch === "]" && line[j + 1] === "]" && depth === 1) { depth = 0; j += 2; continue; }
|
|
346
|
+
j += 1;
|
|
347
|
+
}
|
|
348
|
+
parts.push({ raw: line.slice(bufStart), start: lineStart + bufStart });
|
|
349
|
+
if (parts.length > 1 && parts[parts.length - 1].raw.trim() === "") parts.pop();
|
|
350
|
+
if (parts.length > 1 && parts[0].raw.trim() === "") parts.shift();
|
|
351
|
+
return parts.map((p) => {
|
|
352
|
+
const lead = p.raw.length - p.raw.trimStart().length;
|
|
353
|
+
const text = p.raw.trim();
|
|
354
|
+
const start = p.start + lead;
|
|
355
|
+
return { text, start, segments: buildCellSegments(text, start) };
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/** 单元格内容渲染:无 `\|` 时与 renderInline 完全一致。 */
|
|
360
|
+
function renderInlineCell(cell, handlers, keyBase) {
|
|
361
|
+
const segs = cell.segments;
|
|
362
|
+
if (!segs || segs.length === 0) return [];
|
|
363
|
+
if (segs.length === 1 && segs[0].type === "text") {
|
|
364
|
+
return renderInline(segs[0].text, handlers, keyBase, segs[0].start);
|
|
365
|
+
}
|
|
366
|
+
const out = [];
|
|
367
|
+
let si = 0;
|
|
368
|
+
for (const seg of segs) {
|
|
369
|
+
if (seg.type === "pipe") {
|
|
370
|
+
const n = textNode("|", seg.start, seg.start + 1, `${keyBase}-e${si++}`, handlers);
|
|
371
|
+
if (n) out.push(n);
|
|
372
|
+
} else if (seg.text !== "") {
|
|
373
|
+
out.push(...renderInline(seg.text, handlers, `${keyBase}-e${si++}`, seg.start));
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
return out;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* 表格分隔行 → 每列对齐数组("left" / "center" / "right" / "none");
|
|
381
|
+
* 不是分隔行返回 null。分隔行必须含 `|`,每格只由 `-` 与两侧 `:` 组成。
|
|
382
|
+
*/
|
|
383
|
+
function parseTableAlignLine(line) {
|
|
384
|
+
if (!line.includes("|")) return null;
|
|
385
|
+
if (!/^[|\-: \t]+$/.test(line)) return null;
|
|
386
|
+
const cells = splitTableRow(line, 0);
|
|
387
|
+
if (cells.length === 0) return null;
|
|
388
|
+
const aligns = [];
|
|
389
|
+
for (const cell of cells) {
|
|
390
|
+
const t = cell.text;
|
|
391
|
+
if (!/^:?-+:?$/.test(t)) return null;
|
|
392
|
+
if (t.startsWith(":") && t.endsWith(":")) aligns.push("center");
|
|
393
|
+
else if (t.startsWith(":")) aligns.push("left");
|
|
394
|
+
else if (t.endsWith(":")) aligns.push("right");
|
|
395
|
+
else aligns.push("none");
|
|
396
|
+
}
|
|
397
|
+
return aligns;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// ── Obsidian callout(`> [!type] 标题`)────────────────────────────────────
|
|
401
|
+
// 类型 → 展示名 + 图标(emoji,避免依赖 DSH 主题里未必存在的图标集);
|
|
402
|
+
// 配色不用内联色值,改由 CSS 加 [data-callout] 选择器统一给出。
|
|
403
|
+
const CALLOUT_INFO = {
|
|
404
|
+
note: { label: "笔记", icon: "📝" },
|
|
405
|
+
abstract: { label: "摘要", icon: "📋" },
|
|
406
|
+
info: { label: "信息", icon: "ℹ️" },
|
|
407
|
+
todo: { label: "待办", icon: "✅" },
|
|
408
|
+
tip: { label: "提示", icon: "💡" },
|
|
409
|
+
success: { label: "成功", icon: "✔️" },
|
|
410
|
+
question: { label: "疑问", icon: "❓" },
|
|
411
|
+
warning: { label: "注意", icon: "⚠️" },
|
|
412
|
+
failure: { label: "失败", icon: "❌" },
|
|
413
|
+
danger: { label: "危险", icon: "⚡" },
|
|
414
|
+
bug: { label: "缺陷", icon: "🐛" },
|
|
415
|
+
example: { label: "示例", icon: "🧪" },
|
|
416
|
+
quote: { label: "引用", icon: "❝" },
|
|
417
|
+
};
|
|
418
|
+
// Obsidian 内置别名 → 规范类型
|
|
419
|
+
const CALLOUT_ALIAS = {
|
|
420
|
+
summary: "abstract", tldr: "abstract",
|
|
421
|
+
hint: "tip", important: "tip",
|
|
422
|
+
check: "success", done: "success",
|
|
423
|
+
help: "question", faq: "question",
|
|
424
|
+
caution: "warning", attention: "warning",
|
|
425
|
+
fail: "failure", missing: "failure",
|
|
426
|
+
error: "danger",
|
|
427
|
+
cite: "quote",
|
|
428
|
+
none: "note",
|
|
429
|
+
};
|
|
430
|
+
// 整个 `[!type]` 标记 + 可选折叠符 + 可选标题:不区分大小写
|
|
431
|
+
const CALLOUT_RE = /^\[!\s*([A-Za-z][\w-]*)\s*\][+-]?(?:[ \t]+([\s\S]*))?$/;
|
|
432
|
+
|
|
433
|
+
function calloutCanonical(type) {
|
|
434
|
+
const t = String(type).toLowerCase();
|
|
435
|
+
return CALLOUT_INFO[t] ? t : (CALLOUT_ALIAS[t] ?? "note");
|
|
436
|
+
}
|
|
437
|
+
function calloutSpec(type) {
|
|
438
|
+
const canon = calloutCanonical(type);
|
|
439
|
+
return { canon, ...CALLOUT_INFO[canon] };
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/**
|
|
443
|
+
* 引用块首行 → callout 描述;不是 callout 返回 null。
|
|
444
|
+
* srcStart 为「> 」前缀之后内容的源码起点,用于标题文本的偏移记账。
|
|
445
|
+
* 返回 { spec, title, titleStart }:title 为空串表示没有显式标题;
|
|
446
|
+
* titleStart 是标题在源码里的绝对起点(已按实际前后空白修正,供 renderInline 精确记账)。
|
|
447
|
+
*/
|
|
448
|
+
function parseCalloutHeader(header, srcStart) {
|
|
449
|
+
const m = CALLOUT_RE.exec(header);
|
|
450
|
+
if (!m) return null;
|
|
451
|
+
const spec = calloutSpec(m[1]);
|
|
452
|
+
const restRaw = m[2] ?? "";
|
|
453
|
+
const restStart = header.indexOf(m[0]) + m[0].length - restRaw.length;
|
|
454
|
+
const lead = restRaw.length - restRaw.trimStart().length;
|
|
455
|
+
const title = restRaw.trim();
|
|
456
|
+
return {
|
|
457
|
+
spec,
|
|
458
|
+
title,
|
|
459
|
+
titleStart: title === "" ? null : srcStart + restStart + lead,
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
/** 单行块起始判定(供段落换行识别) */
|
|
464
|
+
function isBlockStart(l) {
|
|
465
|
+
return /^(#{1,6})\s+/.test(l) || /^```/.test(l) || /^>\s?/.test(l) ||
|
|
466
|
+
/^\s*(?:[-*+]\s|\d+[.)]\s+)/.test(l) || /^\s*(?:-{3,}|\*{3,}|_{3,})\s*$/.test(l);
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
/** 解析一个块(block)为 React 节点;blockStart 为该块在源码中的绝对起点。 */
|
|
470
|
+
function renderBlock(block, blockStart, handlers, keyBase) {
|
|
471
|
+
const lines = block.split("\n");
|
|
472
|
+
const lineStarts = [];
|
|
473
|
+
{
|
|
474
|
+
let ps = blockStart;
|
|
475
|
+
for (const l of lines) { lineStarts.push(ps); ps += l.length + 1; }
|
|
476
|
+
}
|
|
477
|
+
const first = lines[0];
|
|
478
|
+
|
|
479
|
+
// 围栏代码块:主体整体一个文本节点(含换行,偏移连续)
|
|
480
|
+
const fence = /^```([\w+-]*)\s*$/.exec(first);
|
|
481
|
+
if (fence) {
|
|
482
|
+
const closing = lines.findIndex((l, i) => i > 0 && /^```\s*$/.test(l));
|
|
483
|
+
const bodyLines = closing > 0 ? lines.slice(1, closing) : lines.slice(1);
|
|
484
|
+
const body = bodyLines.join("\n");
|
|
485
|
+
const codeNode = body === ""
|
|
486
|
+
? createElement("code", null)
|
|
487
|
+
: createElement("code", null, textNode(body, lineStarts[1], lineStarts[1] + body.length, `${keyBase}-pre`, handlers));
|
|
488
|
+
return createElement("pre", { key: keyBase, className: "dsh-obs-code" }, codeNode);
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// 标题
|
|
492
|
+
const heading = /^(#{1,6})\s+(.*)$/.exec(first);
|
|
493
|
+
if (heading) {
|
|
494
|
+
const level = Math.min(heading[1].length + 2, 6);
|
|
495
|
+
const content = heading[2];
|
|
496
|
+
return createElement(`h${level}`, { key: keyBase },
|
|
497
|
+
renderInline(content, handlers, keyBase, lineStarts[0] + (first.length - content.length)));
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
// 分隔线
|
|
501
|
+
if (/^\s*(?:-{3,}|\*{3,}|_{3,})\s*$/.test(first)) {
|
|
502
|
+
return createElement("hr", { key: keyBase });
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// 引用块(连续 > 行;逐行渲染 + <br> 保证文本节点偏移连续)
|
|
506
|
+
if (lines.every((l) => l === "" || /^>\s?/.test(l))) {
|
|
507
|
+
const firstQuote = /^>\s?/.exec(first);
|
|
508
|
+
const headerContent = firstQuote ? first.slice(firstQuote[0].length) : first;
|
|
509
|
+
const headerStart = lineStarts[0] + (firstQuote ? firstQuote[0].length : 0);
|
|
510
|
+
const callout = parseCalloutHeader(headerContent, headerStart);
|
|
511
|
+
if (callout) {
|
|
512
|
+
// callout 主体:去掉每行 `>` 前缀,正文行内语法照常解析
|
|
513
|
+
const bodyLines = [];
|
|
514
|
+
for (let i = 1; i < lines.length; i++) {
|
|
515
|
+
const l = lines[i];
|
|
516
|
+
if (l === "") continue;
|
|
517
|
+
const q = /^>\s?/.exec(l);
|
|
518
|
+
const prefix = q ? q[0].length : 0;
|
|
519
|
+
const text = l.slice(prefix);
|
|
520
|
+
if (text === "") continue;
|
|
521
|
+
bodyLines.push({
|
|
522
|
+
text,
|
|
523
|
+
start: lineStarts[i] + prefix,
|
|
524
|
+
key: `${keyBase}-cb${i}`,
|
|
525
|
+
});
|
|
526
|
+
}
|
|
527
|
+
const body = [];
|
|
528
|
+
for (let i = 0; i < bodyLines.length; i++) {
|
|
529
|
+
const bl = bodyLines[i];
|
|
530
|
+
body.push(...renderInline(bl.text, handlers, bl.key, bl.start));
|
|
531
|
+
if (i < bodyLines.length - 1) body.push(createElement(Fragment, { key: `${bl.key}-br` }, "\n"));
|
|
532
|
+
}
|
|
533
|
+
// 严格口径(契约 §4.6):有显式标题时徽标只留图标、标题文本单独显示;
|
|
534
|
+
// 没有显式标题时才回落到「图标 + 类型名」。
|
|
535
|
+
const headChildren = [
|
|
536
|
+
createElement("span", { key: `${keyBase}-ci`, className: "dsh-obs-callout-icon" }, callout.spec.icon),
|
|
537
|
+
];
|
|
538
|
+
if (callout.title !== "") {
|
|
539
|
+
headChildren.push(createElement("span", { key: `${keyBase}-ct`, className: "dsh-obs-callout-title" },
|
|
540
|
+
renderInline(callout.title, handlers, `${keyBase}-ct`, callout.titleStart)));
|
|
541
|
+
} else {
|
|
542
|
+
headChildren.push(createElement("span", { key: `${keyBase}-ctl`, className: "dsh-obs-callout-title" }, callout.spec.label));
|
|
543
|
+
}
|
|
544
|
+
return createElement(
|
|
545
|
+
"div",
|
|
546
|
+
{
|
|
547
|
+
key: keyBase,
|
|
548
|
+
className: "dsh-obs-callout",
|
|
549
|
+
"data-callout": callout.spec.canon,
|
|
550
|
+
},
|
|
551
|
+
createElement(
|
|
552
|
+
"div",
|
|
553
|
+
{ key: `${keyBase}-ch`, className: "dsh-obs-callout-head" },
|
|
554
|
+
...headChildren,
|
|
555
|
+
),
|
|
556
|
+
createElement("div", { key: `${keyBase}-cbd`, className: "dsh-obs-callout-body" }, body),
|
|
557
|
+
);
|
|
558
|
+
}
|
|
559
|
+
const inner = [];
|
|
560
|
+
let ki = 0;
|
|
561
|
+
for (let i = 0; i < lines.length; i++) {
|
|
562
|
+
const l = lines[i];
|
|
563
|
+
if (l === "") continue;
|
|
564
|
+
const m = /^>\s?/.exec(l);
|
|
565
|
+
const content = l.slice(m ? m[0].length : 0);
|
|
566
|
+
inner.push(...renderInline(content, handlers, `${keyBase}-q${ki}`, lineStarts[i] + (m ? m[0].length : 0)));
|
|
567
|
+
ki++;
|
|
568
|
+
if (i < lines.length - 1 && lines[i + 1] !== "") inner.push(createElement(Fragment, { key: `${keyBase}-qbr${ki}` }, "\n"));
|
|
569
|
+
}
|
|
570
|
+
return createElement("blockquote", { key: keyBase }, inner);
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
// 列表(无序/有序)
|
|
574
|
+
if (lines.every((l) => /^\s*(?:[-*+]\s+|\d+[.)]\s+)/.test(l) || l === "")) {
|
|
575
|
+
const ordered = /^\s*\d+[.)]\s+/.test(lines.find((l) => l !== "") ?? "");
|
|
576
|
+
const ul = createElement(ordered ? "ol" : "ul", { key: keyBase });
|
|
577
|
+
let li = 0;
|
|
578
|
+
for (let i = 0; i < lines.length; i++) {
|
|
579
|
+
const l = lines[i];
|
|
580
|
+
if (l === "") continue;
|
|
581
|
+
const m = /^\s*(?:[-*+]\s+|\d+[.)]\s+)/.exec(l);
|
|
582
|
+
ul.props.children = (ul.props.children ?? []).concat(
|
|
583
|
+
createElement("li", { key: `${keyBase}-li${li++}` },
|
|
584
|
+
renderInline(l.slice(m[0].length), handlers, `${keyBase}-li${li}`, lineStarts[i] + m[0].length)));
|
|
585
|
+
}
|
|
586
|
+
return ul;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
// 表格(首行含 | 且第二行是分隔行)
|
|
590
|
+
if (lines.length >= 2 && lines[0].includes("|") && /^\s*\|?\s*:?-{3,}/.test(lines[1] ?? "")) {
|
|
591
|
+
const table = createElement("table", { key: keyBase });
|
|
592
|
+
const thead = createElement("thead", null);
|
|
593
|
+
const htr = createElement("tr", null);
|
|
594
|
+
htr.props.children = splitTableRow(lines[0], lineStarts[0]).map((cell, j) =>
|
|
595
|
+
createElement("th", { key: `${keyBase}-th${j}` }, renderInlineCell(cell, handlers, `${keyBase}-th${j}`)));
|
|
596
|
+
thead.props.children = htr;
|
|
597
|
+
const tbody = createElement("tbody", null);
|
|
598
|
+
let trIdx = 0;
|
|
599
|
+
for (let i = 2; i < lines.length; i++) {
|
|
600
|
+
if (!lines[i].includes("|")) continue;
|
|
601
|
+
const tr = createElement("tr", { key: `${keyBase}-tr${trIdx}` });
|
|
602
|
+
tr.props.children = splitTableRow(lines[i], lineStarts[i]).map((cell, j) =>
|
|
603
|
+
createElement("td", { key: `${keyBase}-td${trIdx}-${j}` }, renderInlineCell(cell, handlers, `${keyBase}-td${trIdx}-${j}`)));
|
|
604
|
+
tbody.props.children = (tbody.props.children ?? []).concat(tr);
|
|
605
|
+
trIdx++;
|
|
606
|
+
}
|
|
607
|
+
table.props.children = [thead, tbody];
|
|
608
|
+
return table;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
// 段落:逐行渲染(每行一个文本节点 + <br>),保证文本节点偏移连续
|
|
612
|
+
const inner = [];
|
|
613
|
+
let ki = 0;
|
|
614
|
+
const nonEmpty = [];
|
|
615
|
+
for (let i = 0; i < lines.length; i++) if (lines[i] !== "") nonEmpty.push(i);
|
|
616
|
+
for (let idx = 0; idx < nonEmpty.length; idx++) {
|
|
617
|
+
const i = nonEmpty[idx];
|
|
618
|
+
inner.push(...renderInline(lines[i], handlers, `${keyBase}-p${ki}`, lineStarts[i]));
|
|
619
|
+
ki++;
|
|
620
|
+
if (idx < nonEmpty.length - 1) inner.push(createElement(Fragment, { key: `${keyBase}-pbr${ki}` }, "\n"));
|
|
621
|
+
}
|
|
622
|
+
return createElement("p", { key: keyBase }, inner);
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
/** 将 markdown 正文渲染为一组块级 React 节点。 */
|
|
626
|
+
function renderMarkdown(markdown, handlers = {}) {
|
|
627
|
+
const normalized = markdown.replace(/\r\n/g, "\n");
|
|
628
|
+
const blocks = normalized.split(/\n\s*\n/).filter((b) => b.trim() !== "");
|
|
629
|
+
if (blocks.length === 0) {
|
|
630
|
+
return createElement("p", { className: "dsh-obs-empty" }, "(空笔记)");
|
|
631
|
+
}
|
|
632
|
+
const nodes = [];
|
|
633
|
+
let pos = 0;
|
|
634
|
+
for (const block of blocks) {
|
|
635
|
+
const idx = normalized.indexOf(block, pos);
|
|
636
|
+
const blockStart = idx >= 0 ? idx : pos;
|
|
637
|
+
nodes.push(renderBlock(block, blockStart, handlers, `b${nodes.length}`));
|
|
638
|
+
pos = blockStart + block.length;
|
|
639
|
+
}
|
|
640
|
+
return createElement(Fragment, null, nodes);
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
// ── 行号路径(renderNumbered)的块级识别 helper(P0:M1 引用 / M2 围栏 / M3 列表 / M4 表格)──
|
|
644
|
+
|
|
645
|
+
/** 缩进折算成列数:tab 按 4 空格;不写死每级宽度,层级由调用方按实际缩进比较。 */
|
|
646
|
+
function indentCols(s) {
|
|
647
|
+
let n = 0;
|
|
648
|
+
for (let k = 0; k < s.length; k++) n += s[k] === "\t" ? 4 : 1;
|
|
649
|
+
return n;
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
/** 水平线行(列表识别前先排除):`---` / `***` / `___`,两侧允许空白。 */
|
|
653
|
+
function isHrLine(line) {
|
|
654
|
+
return /^\s*(?:-{3,}|\*{3,}|_{3,})\s*$/.test(line);
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
/**
|
|
658
|
+
* 围栏开行:行首 ≤3 空格 + 连续 ≥3 个 ` 或 ~。
|
|
659
|
+
* 返回 { ch, len, lang, langStart }:lang 为 info string 第一个词(无则空串),
|
|
660
|
+
* langStart 为语言词在该行内的偏移;不是开围栏返回 null。
|
|
661
|
+
*/
|
|
662
|
+
function matchFenceOpen(line) {
|
|
663
|
+
const m = /^( {0,3})(`{3,}|~{3,})([\s\S]*)$/.exec(line);
|
|
664
|
+
if (!m) return null;
|
|
665
|
+
const marks = m[2];
|
|
666
|
+
const info = m[3];
|
|
667
|
+
// 反引号围栏的 info string 里不能再出现反引号(CommonMark),否则当普通行
|
|
668
|
+
if (marks[0] === "`" && info.includes("`")) return null;
|
|
669
|
+
const infoTrim = info.length - info.trimStart().length;
|
|
670
|
+
const rest = info.slice(infoTrim);
|
|
671
|
+
const lang = rest === "" ? "" : rest.split(/[ \t]+/)[0];
|
|
672
|
+
return { ch: marks[0], len: marks.length, lang, langStart: m[1].length + marks.length + infoTrim };
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
/** 围栏闭行:同字符、长度不短于开围栏、行内只有空白。 */
|
|
676
|
+
function isFenceClose(line, ch, minLen) {
|
|
677
|
+
const m = /^( {0,3})(`+|~+)[ \t]*$/.exec(line);
|
|
678
|
+
return !!m && m[2][0] === ch && m[2].length >= minLen;
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
/**
|
|
682
|
+
* 引用行前缀:行首 ≤3 空格 + 连续 `>` 标记(标记之间允许一个空格/tab,兼容 `> >`)。
|
|
683
|
+
* 返回 { prefixLen, depth }(depth = 引用标记个数,1 起);不是引用行返回 null。
|
|
684
|
+
*/
|
|
685
|
+
function quotePrefixInfo(line) {
|
|
686
|
+
const m = /^( {0,3}(?:>[ \t]?)+)/.exec(line);
|
|
687
|
+
if (!m) return null;
|
|
688
|
+
const prefix = m[1];
|
|
689
|
+
let depth = 0;
|
|
690
|
+
for (let k = 0; k < prefix.length; k++) if (prefix[k] === ">") depth += 1;
|
|
691
|
+
return { prefixLen: prefix.length, depth };
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
// 列表行:缩进 + 无序/有序标记 + 至少一个空白 + 正文(任务标记在正文里再拆)
|
|
695
|
+
const LIST_RE = /^([ \t]*)([-*+]|\d{1,9}[.)])([ \t]+)([\s\S]*)$/;
|
|
696
|
+
const TASK_RE = /^\[([ xX])\](?:[ \t]+|$)/;
|
|
697
|
+
// 层级 → 无序符号轮换(0/1/2 起再循环)
|
|
698
|
+
const UL_MARKERS = ["•", "◦", "▪"];
|
|
699
|
+
|
|
700
|
+
/**
|
|
701
|
+
* 列表行识别(单行):返回 { indent, ordered, num, body, bodyStart, task };
|
|
702
|
+
* 不是列表行返回 null。先排除水平线;任务标记 `[ ]`/`[x]` 单独拆出,
|
|
703
|
+
* bodyStart 是行内偏移(相对行首),任务从 `[x] ` 之后算正文。
|
|
704
|
+
*/
|
|
705
|
+
function parseListLine(line) {
|
|
706
|
+
if (line.trim() === "" || isHrLine(line)) return null;
|
|
707
|
+
const m = LIST_RE.exec(line);
|
|
708
|
+
if (!m) return null;
|
|
709
|
+
const ordered = /\d/.test(m[2].charAt(0));
|
|
710
|
+
const prefixLen = m[1].length + m[2].length + m[3].length;
|
|
711
|
+
const rawBody = m[4];
|
|
712
|
+
// 任务标记不区分有序/无序(方案 M3:「内容以 ^\[([ xX])\]\s+ 开头时」)
|
|
713
|
+
const tm = TASK_RE.exec(rawBody);
|
|
714
|
+
const task = tm === null
|
|
715
|
+
? null
|
|
716
|
+
: {
|
|
717
|
+
checked: tm[1].toLowerCase() === "x",
|
|
718
|
+
body: rawBody.slice(tm[0].length),
|
|
719
|
+
bodyStart: prefixLen + tm[0].length,
|
|
720
|
+
};
|
|
721
|
+
return {
|
|
722
|
+
indent: indentCols(m[1]),
|
|
723
|
+
ordered,
|
|
724
|
+
num: ordered ? parseInt(m[2], 10) : null,
|
|
725
|
+
body: task ? task.body : rawBody,
|
|
726
|
+
bodyStart: task ? task.bodyStart : prefixLen,
|
|
727
|
+
task,
|
|
728
|
+
};
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
/**
|
|
732
|
+
* M5 标题行:`#{1,6}` + 至少一个空白 + 内容。
|
|
733
|
+
* 返回 { level, content, contentOffset }(contentOffset 相对行首,按实际匹配长度算,
|
|
734
|
+
* `# 多空格` 也对);不是标题返回 null。行号路径与 callout 正文共用。
|
|
735
|
+
*/
|
|
736
|
+
function splitHeadingLine(line) {
|
|
737
|
+
const m = /^(#{1,6})[ \t]+([\s\S]*)$/.exec(line);
|
|
738
|
+
if (!m) return null;
|
|
739
|
+
const content = m[2];
|
|
740
|
+
return { level: m[1].length, content, contentOffset: m[0].length - content.length };
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
/**
|
|
744
|
+
* M5 setext 内容行的排除条件:当前行本身已是别的块起始(标题/列表/引用/围栏/hr)时,
|
|
745
|
+
* 下一行的 `===` / `---` 不能把它变成 setext 标题(先 setext 后 hr 的前提)。
|
|
746
|
+
*/
|
|
747
|
+
function isBlockStartLine(line) {
|
|
748
|
+
return splitHeadingLine(line) !== null ||
|
|
749
|
+
/^ {0,3}(?:`{3,}|~{3,})/.test(line) ||
|
|
750
|
+
/^ {0,3}>/.test(line) ||
|
|
751
|
+
parseListLine(line) !== null ||
|
|
752
|
+
isHrLine(line);
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
/**
|
|
756
|
+
* 行号预览模式(预览态「每行一个行标号」):
|
|
757
|
+
* 每个源码行渲染为一行 <div class="dsh-obs-md-row">,左侧行号 .dsh-obs-md-ln、
|
|
758
|
+
* 内容 .dsh-obs-md-lc(行内 markdown 保留,且允许自然折行)。行号与源码行一一对应,
|
|
759
|
+
* 不依赖固定宽度/不折行,长行折行也不影响后续行号对齐。选中映射(.dsh-obs-src 偏移)
|
|
760
|
+
* 仍由 renderInline 的 onText 记账保留。
|
|
761
|
+
* 块级语法(P0):引用逐行(M1)、围栏代码逐行(M2)、列表/任务逐行(M3)、表格整块(M4)。
|
|
762
|
+
* 表格是唯一不按行拆的块:行号放在每个 tr 的首列(.dsh-obs-md-tln),保证 1..N 连续。
|
|
763
|
+
*/
|
|
764
|
+
function renderNumbered(markdown, handlers = {}) {
|
|
765
|
+
const normalized = markdown.replace(/\r\n/g, "\n");
|
|
766
|
+
const rows = [];
|
|
767
|
+
const lines = normalized.split("\n");
|
|
768
|
+
let pos = 0;
|
|
769
|
+
let lineNo = 0;
|
|
770
|
+
let i = 0;
|
|
771
|
+
let keyIdx = 0;
|
|
772
|
+
|
|
773
|
+
const nextLine = () => {
|
|
774
|
+
const text = lines[i];
|
|
775
|
+
const start = pos;
|
|
776
|
+
lineNo += 1;
|
|
777
|
+
pos += text.length + 1;
|
|
778
|
+
i += 1;
|
|
779
|
+
return { text, start, num: lineNo };
|
|
780
|
+
};
|
|
781
|
+
|
|
782
|
+
// 同一个 callout 的连续行共用一个 groupId → 首尾行加样式类(圆角收尾)
|
|
783
|
+
let groupId = -1;
|
|
784
|
+
let inCallout = false;
|
|
785
|
+
let calloutFirst = true;
|
|
786
|
+
|
|
787
|
+
const makeRow = (num, rowClass, lcInner, lcClass, extraProps) =>
|
|
788
|
+
createElement("div", {
|
|
789
|
+
key: `n${keyIdx++}`,
|
|
790
|
+
className: `dsh-obs-md-row${rowClass ? ` ${rowClass}` : ""}`,
|
|
791
|
+
...(extraProps ?? {}),
|
|
792
|
+
},
|
|
793
|
+
createElement("span", { className: "dsh-obs-md-ln" }, String(num)),
|
|
794
|
+
createElement("span", { className: `dsh-obs-md-lc${lcClass ? ` ${lcClass}` : ""}` }, lcInner),
|
|
795
|
+
);
|
|
796
|
+
// "code code-head" → "dsh-obs-md-code dsh-obs-md-code-head"(块级样式类统一前缀)
|
|
797
|
+
const rowClassOf = (cls) =>
|
|
798
|
+
cls ? cls.split(/\s+/).filter(Boolean).map((c) => `dsh-obs-md-${c}`).join(" ") : "";
|
|
799
|
+
const closeCallout = () => {
|
|
800
|
+
if (!inCallout) return;
|
|
801
|
+
const last = rows[rows.length - 1];
|
|
802
|
+
if (last) last.props.className += " dsh-obs-md-callout-end";
|
|
803
|
+
inCallout = false;
|
|
804
|
+
};
|
|
805
|
+
/**
|
|
806
|
+
* gid === null → 普通行;否则为 callout 的某一行(同一 callout 共用一个 gid)。
|
|
807
|
+
* extraProps 承载契约属性:data-quote-depth / data-code-group / data-list-* 等。
|
|
808
|
+
*/
|
|
809
|
+
const pushRow = (num, cls, inner, gid, extraProps) => {
|
|
810
|
+
const rowClass = rowClassOf(cls);
|
|
811
|
+
if (gid === null) {
|
|
812
|
+
closeCallout();
|
|
813
|
+
rows.push(makeRow(num, rowClass, inner, null, extraProps));
|
|
814
|
+
return;
|
|
815
|
+
}
|
|
816
|
+
if (!inCallout || gid !== groupId) { closeCallout(); groupId = gid; inCallout = true; calloutFirst = true; }
|
|
817
|
+
const lcClass = calloutFirst ? "dsh-obs-md-callout-head" : "dsh-obs-md-callout-body";
|
|
818
|
+
calloutFirst = false;
|
|
819
|
+
rows.push(makeRow(num, rowClass, inner, lcClass, { "data-callout-group": String(gid), ...(extraProps ?? {}) }));
|
|
820
|
+
};
|
|
821
|
+
// 块级节点(表格)直接进 rows 序列:先收尾上一个 callout,避免末行样式打错行
|
|
822
|
+
const pushBlock = (node) => { closeCallout(); rows.push(node); };
|
|
823
|
+
|
|
824
|
+
// 列表层级栈:按实际缩进比较(tab=4 列),不写死每级宽度;有序编号按层计数
|
|
825
|
+
let listIndentStack = [];
|
|
826
|
+
let listSeq = [];
|
|
827
|
+
let listKindAtDepth = [];
|
|
828
|
+
const resetListState = () => { listIndentStack = []; listSeq = []; listKindAtDepth = []; };
|
|
829
|
+
const listDepthFor = (indent) => {
|
|
830
|
+
if (listIndentStack.length === 0) { listIndentStack.push(indent); return 0; }
|
|
831
|
+
let top = listIndentStack.length - 1;
|
|
832
|
+
while (top > 0 && indent < listIndentStack[top]) top -= 1;
|
|
833
|
+
if (indent > listIndentStack[top]) {
|
|
834
|
+
listIndentStack.length = top + 1;
|
|
835
|
+
listIndentStack.push(indent);
|
|
836
|
+
const depth = top + 1;
|
|
837
|
+
listKindAtDepth[depth] = undefined;
|
|
838
|
+
listSeq[depth] = undefined;
|
|
839
|
+
return depth;
|
|
840
|
+
}
|
|
841
|
+
listIndentStack.length = top + 1;
|
|
842
|
+
if (indent < listIndentStack[top]) listIndentStack[top] = indent;
|
|
843
|
+
return top;
|
|
844
|
+
};
|
|
845
|
+
const listMarkerOf = (info, depth) => {
|
|
846
|
+
if (!info.ordered) {
|
|
847
|
+
listKindAtDepth[depth] = "ul";
|
|
848
|
+
return UL_MARKERS[depth % UL_MARKERS.length];
|
|
849
|
+
}
|
|
850
|
+
if (listKindAtDepth[depth] === "ol" && typeof listSeq[depth] === "number") listSeq[depth] += 1;
|
|
851
|
+
else listSeq[depth] = info.num;
|
|
852
|
+
listKindAtDepth[depth] = "ol";
|
|
853
|
+
return `${listSeq[depth]}.`;
|
|
854
|
+
};
|
|
855
|
+
/**
|
|
856
|
+
* 列表行 → lc 子节点 + data-list-* 属性(普通行与 callout 正文共用,M6 复用 M3)。
|
|
857
|
+
* srcStart 为该行「正文内容」的源码起点(任务行已跳过 [x] 标记)。
|
|
858
|
+
*/
|
|
859
|
+
const listRowContent = (info, keyBase, srcStart) => {
|
|
860
|
+
const depth = listDepthFor(info.indent);
|
|
861
|
+
const inner = [createElement("span", { key: `${keyBase}-mk`, className: "dsh-obs-md-list-marker" }, listMarkerOf(info, depth))];
|
|
862
|
+
if (info.task) {
|
|
863
|
+
inner.push(createElement("input", {
|
|
864
|
+
key: `${keyBase}-cb`,
|
|
865
|
+
type: "checkbox",
|
|
866
|
+
className: "dsh-obs-md-task-check",
|
|
867
|
+
disabled: true,
|
|
868
|
+
checked: info.task.checked,
|
|
869
|
+
}));
|
|
870
|
+
}
|
|
871
|
+
inner.push(createElement("span", { key: `${keyBase}-bd`, className: "dsh-obs-md-list-body" },
|
|
872
|
+
renderInline(info.body, handlers, `${keyBase}-bd`, srcStart + info.bodyStart)));
|
|
873
|
+
const props = {
|
|
874
|
+
"data-list-depth": String(depth),
|
|
875
|
+
"data-list-kind": info.task ? "task" : info.ordered ? "ol" : "ul",
|
|
876
|
+
};
|
|
877
|
+
if (info.task) props["data-checked"] = info.task.checked ? "1" : "0";
|
|
878
|
+
return { inner, props };
|
|
879
|
+
};
|
|
880
|
+
|
|
881
|
+
while (i < lines.length) {
|
|
882
|
+
const cur = nextLine();
|
|
883
|
+
|
|
884
|
+
// ── M2 围栏代码块(``` 与 ~~~ 等价):整块每行占号,正文不解析行内 ──
|
|
885
|
+
const fence = matchFenceOpen(cur.text);
|
|
886
|
+
if (fence) {
|
|
887
|
+
resetListState();
|
|
888
|
+
const group = `f${cur.start}`;
|
|
889
|
+
const lang = fence.lang;
|
|
890
|
+
const langNode = lang === ""
|
|
891
|
+
? null
|
|
892
|
+
: textNode(lang, cur.start + fence.langStart, cur.start + fence.langStart + lang.length, `n${keyIdx}lang`, handlers);
|
|
893
|
+
pushRow(cur.num, "code code-head",
|
|
894
|
+
createElement("span", { key: `n${keyIdx}lang`, className: "dsh-obs-md-code-lang" }, langNode),
|
|
895
|
+
null, { "data-code-group": group });
|
|
896
|
+
let closed = false;
|
|
897
|
+
while (i < lines.length) {
|
|
898
|
+
if (isFenceClose(lines[i], fence.ch, fence.len)) {
|
|
899
|
+
const close = nextLine();
|
|
900
|
+
pushRow(close.num, "code code-end", null, null, { "data-code-group": group });
|
|
901
|
+
closed = true;
|
|
902
|
+
break;
|
|
903
|
+
}
|
|
904
|
+
const body = nextLine();
|
|
905
|
+
pushRow(body.num, "code",
|
|
906
|
+
body.text === "" ? null : textNode(body.text, body.start, body.start + body.text.length, `n${keyIdx}body`, handlers),
|
|
907
|
+
null, { "data-code-group": group });
|
|
908
|
+
}
|
|
909
|
+
if (!closed) {
|
|
910
|
+
// 未闭合吃到文末:最后一行加 code-end(只有开围栏行时它同时是 head 和 end)
|
|
911
|
+
const last = rows[rows.length - 1];
|
|
912
|
+
if (last) last.props.className += " dsh-obs-md-code-end";
|
|
913
|
+
}
|
|
914
|
+
continue;
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
// ── M1 引用:连续 `>` 行逐行成像(旧实现会吞掉后续行);首行是 callout 才走分组 ──
|
|
918
|
+
const quote = quotePrefixInfo(cur.text);
|
|
919
|
+
if (quote) {
|
|
920
|
+
const block = [cur];
|
|
921
|
+
while (i < lines.length && quotePrefixInfo(lines[i])) block.push(nextLine());
|
|
922
|
+
resetListState();
|
|
923
|
+
const callout = parseCalloutHeader(
|
|
924
|
+
block[0].text.slice(quote.prefixLen),
|
|
925
|
+
block[0].start + quote.prefixLen,
|
|
926
|
+
);
|
|
927
|
+
if (callout) {
|
|
928
|
+
const gid = `c${block[0].start}`;
|
|
929
|
+
const spec = callout.spec;
|
|
930
|
+
// 严格口径(契约 §4.6,Lead 裁决):有显式标题时徽标只显示图标、标题文本单独显示;
|
|
931
|
+
// 没有显式标题时才显示「图标 + 类型名」。
|
|
932
|
+
const badgeChildren = [
|
|
933
|
+
createElement("span", { key: "i", className: "dsh-obs-md-callout-ico" }, spec.icon),
|
|
934
|
+
];
|
|
935
|
+
if (callout.title === "") {
|
|
936
|
+
badgeChildren.push(createElement("span", { key: "l", className: "dsh-obs-md-callout-label" }, spec.label));
|
|
937
|
+
}
|
|
938
|
+
const head = [createElement("span", { key: "badge", className: "dsh-obs-md-callout-badge" }, badgeChildren)];
|
|
939
|
+
if (callout.title !== "") {
|
|
940
|
+
head.push(createElement("span", { key: "t", className: "dsh-obs-md-callout-title" },
|
|
941
|
+
renderInline(callout.title, handlers, `n${keyIdx}ct`, callout.titleStart)));
|
|
942
|
+
}
|
|
943
|
+
pushRow(block[0].num, "quote", head, gid, { "data-callout": spec.canon });
|
|
944
|
+
// 正文块(契约 §4.6):去掉 `>` 前缀后分类为 列表/任务、标题、空行、段落。
|
|
945
|
+
for (let k = 1; k < block.length; k++) {
|
|
946
|
+
const b = block[k];
|
|
947
|
+
const qi = quotePrefixInfo(b.text);
|
|
948
|
+
const prefixLen = qi ? qi.prefixLen : 0;
|
|
949
|
+
const bodyText = b.text.slice(prefixLen);
|
|
950
|
+
const bodyStart = b.start + prefixLen;
|
|
951
|
+
const keyBase = `n${keyIdx}c${k}`;
|
|
952
|
+
const baseProps = { "data-callout": spec.canon };
|
|
953
|
+
if (bodyText === "") {
|
|
954
|
+
// `>` 空行:行号与分组样式保留,内容为空
|
|
955
|
+
pushRow(b.num, "quote", null, gid, baseProps);
|
|
956
|
+
continue;
|
|
957
|
+
}
|
|
958
|
+
const bodyList = parseListLine(bodyText);
|
|
959
|
+
if (bodyList) {
|
|
960
|
+
const { inner, props } = listRowContent(bodyList, keyBase, bodyStart);
|
|
961
|
+
pushRow(b.num, "quote list", inner, gid, { ...baseProps, ...props });
|
|
962
|
+
continue;
|
|
963
|
+
}
|
|
964
|
+
const bodyHeading = splitHeadingLine(bodyText);
|
|
965
|
+
if (bodyHeading) {
|
|
966
|
+
pushRow(b.num, `quote heading h${bodyHeading.level}`,
|
|
967
|
+
renderInline(bodyHeading.content, handlers, keyBase, bodyStart + bodyHeading.contentOffset), gid,
|
|
968
|
+
{ ...baseProps, "data-heading-level": String(bodyHeading.level) });
|
|
969
|
+
continue;
|
|
970
|
+
}
|
|
971
|
+
pushRow(b.num, "quote", renderInline(bodyText, handlers, keyBase, bodyStart), gid, baseProps);
|
|
972
|
+
}
|
|
973
|
+
resetListState();
|
|
974
|
+
continue;
|
|
975
|
+
}
|
|
976
|
+
for (let k = 0; k < block.length; k++) {
|
|
977
|
+
const qi = quotePrefixInfo(block[k].text);
|
|
978
|
+
pushRow(block[k].num, "quote",
|
|
979
|
+
renderInline(block[k].text.slice(qi.prefixLen), handlers, `n${keyIdx}q${k}`, block[k].start + qi.prefixLen),
|
|
980
|
+
null, { "data-quote-depth": String(qi.depth) });
|
|
981
|
+
}
|
|
982
|
+
continue;
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
// ── M4 表格:整块 <table>,行号放每行首列(表头/分隔/正文行都占号)──
|
|
986
|
+
const headerCells = cur.text.includes("|") ? splitTableRow(cur.text, cur.start) : null;
|
|
987
|
+
const aligns = headerCells && headerCells.length > 0 && i < lines.length
|
|
988
|
+
? parseTableAlignLine(lines[i])
|
|
989
|
+
: null;
|
|
990
|
+
if (aligns) {
|
|
991
|
+
resetListState();
|
|
992
|
+
const sep = nextLine();
|
|
993
|
+
const bodyLines = [];
|
|
994
|
+
while (i < lines.length && lines[i].includes("|")) bodyLines.push(nextLine());
|
|
995
|
+
const gid = `t${cur.start}`;
|
|
996
|
+
const colCount = headerCells.length;
|
|
997
|
+
const alignOf = (j) => aligns[j] ?? "none";
|
|
998
|
+
// 表头/正文列数不齐时按表头补齐(多出的截掉)
|
|
999
|
+
const padCells = (cells, line) => {
|
|
1000
|
+
const out = cells.slice(0, colCount);
|
|
1001
|
+
while (out.length < colCount) out.push({ text: "", start: line.start + line.text.length, segments: [] });
|
|
1002
|
+
return out;
|
|
1003
|
+
};
|
|
1004
|
+
const thead = createElement("thead", null,
|
|
1005
|
+
createElement("tr", null,
|
|
1006
|
+
createElement("th", { key: `${gid}-th-ln`, className: "dsh-obs-md-tln" }, String(cur.num)),
|
|
1007
|
+
...headerCells.map((cell, j) => createElement(
|
|
1008
|
+
"th",
|
|
1009
|
+
{ key: `${gid}-th${j}`, "data-align": alignOf(j) },
|
|
1010
|
+
renderInlineCell(cell, handlers, `${gid}-th${j}`),
|
|
1011
|
+
)),
|
|
1012
|
+
));
|
|
1013
|
+
const tbodyChildren = [
|
|
1014
|
+
createElement("tr", { key: `${gid}-sep`, className: "dsh-obs-md-tsep" },
|
|
1015
|
+
createElement("td", { key: `${gid}-sep-ln`, className: "dsh-obs-md-tln" }, String(sep.num)),
|
|
1016
|
+
createElement("td", { key: `${gid}-sep-cell`, className: "dsh-obs-md-tsep-cell", colspan: colCount }, null),
|
|
1017
|
+
),
|
|
1018
|
+
];
|
|
1019
|
+
for (const bodyLine of bodyLines) {
|
|
1020
|
+
const cells = padCells(splitTableRow(bodyLine.text, bodyLine.start), bodyLine);
|
|
1021
|
+
tbodyChildren.push(createElement("tr", { key: `${gid}-row${bodyLine.num}`, className: "dsh-obs-md-trow" },
|
|
1022
|
+
createElement("td", { key: `${gid}-row${bodyLine.num}-ln`, className: "dsh-obs-md-tln" }, String(bodyLine.num)),
|
|
1023
|
+
...cells.map((cell, j) => createElement(
|
|
1024
|
+
"td",
|
|
1025
|
+
{ key: `${gid}-row${bodyLine.num}-c${j}`, "data-align": alignOf(j) },
|
|
1026
|
+
renderInlineCell(cell, handlers, `${gid}-row${bodyLine.num}-c${j}`),
|
|
1027
|
+
)),
|
|
1028
|
+
));
|
|
1029
|
+
}
|
|
1030
|
+
const tbody = createElement("tbody", null, ...tbodyChildren);
|
|
1031
|
+
pushBlock(createElement("div", {
|
|
1032
|
+
key: `n${keyIdx++}`,
|
|
1033
|
+
className: "dsh-obs-md-tablewrap",
|
|
1034
|
+
"data-table-group": gid,
|
|
1035
|
+
}, createElement("table", { className: "dsh-obs-md-table" }, thead, tbody)));
|
|
1036
|
+
continue;
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
// ── M3 列表(无序/有序/嵌套)+ 任务列表(只读复选框)──
|
|
1040
|
+
const listInfo = parseListLine(cur.text);
|
|
1041
|
+
if (listInfo) {
|
|
1042
|
+
const { inner, props } = listRowContent(listInfo, `n${keyIdx}`, cur.start);
|
|
1043
|
+
pushRow(cur.num, "list", inner, null, props);
|
|
1044
|
+
continue;
|
|
1045
|
+
}
|
|
1046
|
+
resetListState();
|
|
1047
|
+
|
|
1048
|
+
// ── M5 setext:当前行非块起始、下一行是 `=+` / `-{3,}` 下划线 → 当前行 h1/h2;
|
|
1049
|
+
// 下划线行 dsh-obs-md-setext-underline 空内容占号(不显示 === / ---);先 setext 后 hr。──
|
|
1050
|
+
const setextUnderline = i < lines.length ? lines[i] : null;
|
|
1051
|
+
if (cur.text.trim() !== "" && setextUnderline !== null &&
|
|
1052
|
+
/^(?:=+|-{3,})\s*$/.test(setextUnderline) && !isBlockStartLine(cur.text)) {
|
|
1053
|
+
const level = setextUnderline.trimStart().startsWith("=") ? 1 : 2;
|
|
1054
|
+
pushRow(cur.num, `heading h${level}`,
|
|
1055
|
+
renderInline(cur.text, handlers, `n${keyIdx}`, cur.start), null,
|
|
1056
|
+
{ "data-heading-level": String(level) });
|
|
1057
|
+
const underline = nextLine();
|
|
1058
|
+
pushRow(underline.num, "setext-underline", null, null);
|
|
1059
|
+
continue;
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
// ── M5 标题分级(# ~ ######):内容偏移按实际匹配长度(# 后多空格也对)──
|
|
1063
|
+
const headingMatch = splitHeadingLine(cur.text);
|
|
1064
|
+
if (headingMatch) {
|
|
1065
|
+
pushRow(cur.num, `heading h${headingMatch.level}`,
|
|
1066
|
+
renderInline(headingMatch.content, handlers, `n${keyIdx}`, cur.start + headingMatch.contentOffset), null,
|
|
1067
|
+
{ "data-heading-level": String(headingMatch.level) });
|
|
1068
|
+
continue;
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
// ── M5 分隔线:行 class dsh-obs-md-hr,lc 里放 hr.dsh-obs-md-hr-line,行号保留 ──
|
|
1072
|
+
if (isHrLine(cur.text)) {
|
|
1073
|
+
pushRow(cur.num, "hr",
|
|
1074
|
+
[createElement("hr", { key: `n${keyIdx}hr`, className: "dsh-obs-md-hr-line" })], null);
|
|
1075
|
+
continue;
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
// 其余按普通段落行渲染
|
|
1079
|
+
pushRow(cur.num, null, renderInline(cur.text, handlers, `n${keyIdx}`, cur.start), null);
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
closeCallout();
|
|
1083
|
+
return createElement(Fragment, null, rows);
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
return { renderMarkdown, renderNumbered };
|
|
1087
|
+
})();
|
|
1088
|
+
|
|
1089
|
+
const { renderMarkdown, renderNumbered } = renderMarkdownModule;
|