@moraya/core 0.1.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/CHANGELOG.md +344 -0
- package/LICENSE +85 -0
- package/README.md +82 -0
- package/dist/adapters/browser-media-resolver.d.ts +21 -0
- package/dist/adapters/browser-media-resolver.js +24 -0
- package/dist/adapters/browser-media-resolver.js.map +1 -0
- package/dist/commands.d.ts +35 -0
- package/dist/commands.js +976 -0
- package/dist/commands.js.map +1 -0
- package/dist/doc-cache.d.ts +29 -0
- package/dist/doc-cache.js +50 -0
- package/dist/doc-cache.js.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +4534 -0
- package/dist/index.js.map +1 -0
- package/dist/markdown.d.ts +46 -0
- package/dist/markdown.js +1553 -0
- package/dist/markdown.js.map +1 -0
- package/dist/plugins/code-block-view.d.ts +52 -0
- package/dist/plugins/code-block-view.js +686 -0
- package/dist/plugins/code-block-view.js.map +1 -0
- package/dist/plugins/cursor-syntax.d.ts +27 -0
- package/dist/plugins/cursor-syntax.js +122 -0
- package/dist/plugins/cursor-syntax.js.map +1 -0
- package/dist/plugins/definition-list.d.ts +23 -0
- package/dist/plugins/definition-list.js +12 -0
- package/dist/plugins/definition-list.js.map +1 -0
- package/dist/plugins/editor-props-plugin.d.ts +36 -0
- package/dist/plugins/editor-props-plugin.js +1963 -0
- package/dist/plugins/editor-props-plugin.js.map +1 -0
- package/dist/plugins/emoji.d.ts +21 -0
- package/dist/plugins/emoji.js +42 -0
- package/dist/plugins/emoji.js.map +1 -0
- package/dist/plugins/enter-handler.d.ts +26 -0
- package/dist/plugins/enter-handler.js +193 -0
- package/dist/plugins/enter-handler.js.map +1 -0
- package/dist/plugins/highlight.d.ts +39 -0
- package/dist/plugins/highlight.js +283 -0
- package/dist/plugins/highlight.js.map +1 -0
- package/dist/plugins/inline-code-convert.d.ts +32 -0
- package/dist/plugins/inline-code-convert.js +173 -0
- package/dist/plugins/inline-code-convert.js.map +1 -0
- package/dist/plugins/link-text-plugin.d.ts +22 -0
- package/dist/plugins/link-text-plugin.js +194 -0
- package/dist/plugins/link-text-plugin.js.map +1 -0
- package/dist/plugins/mermaid-renderer.d.ts +24 -0
- package/dist/plugins/mermaid-renderer.js +80 -0
- package/dist/plugins/mermaid-renderer.js.map +1 -0
- package/dist/schema.d.ts +48 -0
- package/dist/schema.js +847 -0
- package/dist/schema.js.map +1 -0
- package/dist/setup.d.ts +104 -0
- package/dist/setup.js +4393 -0
- package/dist/setup.js.map +1 -0
- package/dist/types.d.ts +107 -0
- package/dist/types.js +10 -0
- package/dist/types.js.map +1 -0
- package/package.json +121 -0
|
@@ -0,0 +1,1963 @@
|
|
|
1
|
+
// src/plugins/editor-props-plugin.ts
|
|
2
|
+
import { Fragment as Fragment2, Slice } from "prosemirror-model";
|
|
3
|
+
import { AllSelection, Plugin, PluginKey, TextSelection } from "prosemirror-state";
|
|
4
|
+
import { Decoration, DecorationSet } from "prosemirror-view";
|
|
5
|
+
|
|
6
|
+
// src/markdown.ts
|
|
7
|
+
import MarkdownIt from "markdown-it";
|
|
8
|
+
import deflistPlugin from "markdown-it-deflist";
|
|
9
|
+
import texmathPlugin from "markdown-it-texmath";
|
|
10
|
+
import { MarkdownParser, MarkdownSerializer } from "prosemirror-markdown";
|
|
11
|
+
|
|
12
|
+
// src/schema.ts
|
|
13
|
+
import { Schema, Fragment } from "prosemirror-model";
|
|
14
|
+
import katex from "katex";
|
|
15
|
+
|
|
16
|
+
// src/types.ts
|
|
17
|
+
var NULL_MEDIA_RESOLVER_SENTINEL = /* @__PURE__ */ Symbol("@moraya/core:null-media-resolver");
|
|
18
|
+
|
|
19
|
+
// src/schema.ts
|
|
20
|
+
function extractHtmlAttr(html, name) {
|
|
21
|
+
const re = new RegExp(`${name}\\s*=\\s*(?:"([^"]*)"|'([^']*)'|([^\\s>]+))`, "i");
|
|
22
|
+
const m = html.match(re);
|
|
23
|
+
return m ? m[1] ?? m[2] ?? m[3] ?? null : null;
|
|
24
|
+
}
|
|
25
|
+
function extractAllHtmlAttrs(html) {
|
|
26
|
+
const attrs = {};
|
|
27
|
+
const re = /([a-zA-Z_][\w:.-]*)\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s>]+))/gi;
|
|
28
|
+
let m;
|
|
29
|
+
while ((m = re.exec(html)) !== null) {
|
|
30
|
+
const name = m[1];
|
|
31
|
+
if (!name) continue;
|
|
32
|
+
attrs[name.toLowerCase()] = m[2] ?? m[3] ?? m[4] ?? "";
|
|
33
|
+
}
|
|
34
|
+
return attrs;
|
|
35
|
+
}
|
|
36
|
+
function showBrokenImage(container, sourceText) {
|
|
37
|
+
container.textContent = "";
|
|
38
|
+
container.className = (container.className.replace(/\bhtml-img-wrapper\b|\bimage-node\b/, "").trim() + " broken-image").trim();
|
|
39
|
+
const icon = document.createElement("span");
|
|
40
|
+
icon.className = "broken-image-icon";
|
|
41
|
+
container.appendChild(icon);
|
|
42
|
+
const code2 = document.createElement("code");
|
|
43
|
+
code2.className = "broken-image-src";
|
|
44
|
+
code2.textContent = sourceText;
|
|
45
|
+
container.appendChild(code2);
|
|
46
|
+
}
|
|
47
|
+
function htmlTagToStyle(openTag) {
|
|
48
|
+
const tagMatch = openTag.match(/^<([a-zA-Z][a-zA-Z0-9]*)/);
|
|
49
|
+
if (!tagMatch || !tagMatch[1]) return "";
|
|
50
|
+
const tagName = tagMatch[1].toLowerCase();
|
|
51
|
+
switch (tagName) {
|
|
52
|
+
case "font": {
|
|
53
|
+
const parts = [];
|
|
54
|
+
const color = extractHtmlAttr(openTag, "color");
|
|
55
|
+
if (color) parts.push(`color: ${color}`);
|
|
56
|
+
const size = extractHtmlAttr(openTag, "size");
|
|
57
|
+
if (size) {
|
|
58
|
+
const sizeMap = {
|
|
59
|
+
"1": "0.63em",
|
|
60
|
+
"2": "0.82em",
|
|
61
|
+
"3": "1em",
|
|
62
|
+
"4": "1.13em",
|
|
63
|
+
"5": "1.5em",
|
|
64
|
+
"6": "2em",
|
|
65
|
+
"7": "3em"
|
|
66
|
+
};
|
|
67
|
+
parts.push(`font-size: ${sizeMap[size] || size}`);
|
|
68
|
+
}
|
|
69
|
+
const face = extractHtmlAttr(openTag, "face");
|
|
70
|
+
if (face) parts.push(`font-family: ${face}`);
|
|
71
|
+
return parts.join("; ");
|
|
72
|
+
}
|
|
73
|
+
case "span":
|
|
74
|
+
case "div":
|
|
75
|
+
return extractHtmlAttr(openTag, "style") || "";
|
|
76
|
+
default:
|
|
77
|
+
return "";
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
var documentBaseDir = "";
|
|
81
|
+
function isAbsoluteFilePath(src) {
|
|
82
|
+
if (!src) return false;
|
|
83
|
+
if (src.startsWith("/") && !src.startsWith("//")) return true;
|
|
84
|
+
if (/^[A-Z]:[\\/]/i.test(src)) return true;
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
function isRelativePath(src) {
|
|
88
|
+
if (!src) return false;
|
|
89
|
+
if (/^(https?:|data:|blob:|javascript:|vbscript:|tauri:|\/\/)/i.test(src)) return false;
|
|
90
|
+
if (src.startsWith("/") || /^[A-Z]:[\\/]/i.test(src)) return false;
|
|
91
|
+
return true;
|
|
92
|
+
}
|
|
93
|
+
function resolveRelativePath(src) {
|
|
94
|
+
if (!documentBaseDir) return src;
|
|
95
|
+
let rel = src.replace(/^\.\//, "");
|
|
96
|
+
const sep = documentBaseDir.includes("\\") ? "\\" : "/";
|
|
97
|
+
let base = documentBaseDir.endsWith(sep) ? documentBaseDir.slice(0, -1) : documentBaseDir;
|
|
98
|
+
while (rel.startsWith("../") || rel.startsWith("..\\")) {
|
|
99
|
+
rel = rel.slice(3);
|
|
100
|
+
const lastSep = base.lastIndexOf(sep);
|
|
101
|
+
if (lastSep > 0) base = base.slice(0, lastSep);
|
|
102
|
+
}
|
|
103
|
+
return `${base}${sep}${rel}`;
|
|
104
|
+
}
|
|
105
|
+
function loadLocalImageSrc(img, src, mediaResolver) {
|
|
106
|
+
let path;
|
|
107
|
+
try {
|
|
108
|
+
path = decodeURIComponent(src);
|
|
109
|
+
} catch {
|
|
110
|
+
path = src;
|
|
111
|
+
}
|
|
112
|
+
mediaResolver.loadLocalImage(path).then((url) => {
|
|
113
|
+
if (url) img.src = url;
|
|
114
|
+
else img.dispatchEvent(new Event("error"));
|
|
115
|
+
}).catch(() => {
|
|
116
|
+
img.dispatchEvent(new Event("error"));
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
function setMediaSrc(el, src, mediaResolver) {
|
|
120
|
+
if (isAbsoluteFilePath(src)) {
|
|
121
|
+
mediaResolver.loadLocalMedia(src).then((url) => {
|
|
122
|
+
if (!url) return;
|
|
123
|
+
el.src = url;
|
|
124
|
+
if (el instanceof HTMLMediaElement) el.load();
|
|
125
|
+
}).catch(() => {
|
|
126
|
+
});
|
|
127
|
+
} else if (isRelativePath(src)) {
|
|
128
|
+
mediaResolver.loadLocalMedia(resolveRelativePath(src)).then((url) => {
|
|
129
|
+
if (!url) return;
|
|
130
|
+
el.src = url;
|
|
131
|
+
if (el instanceof HTMLMediaElement) el.load();
|
|
132
|
+
}).catch(() => {
|
|
133
|
+
});
|
|
134
|
+
} else if (/^https?:\/\//i.test(src)) {
|
|
135
|
+
if (el instanceof HTMLVideoElement) {
|
|
136
|
+
el.src = src;
|
|
137
|
+
el.load();
|
|
138
|
+
} else {
|
|
139
|
+
mediaResolver.loadRemoteMedia(src).then((url) => {
|
|
140
|
+
if (!url) return;
|
|
141
|
+
el.src = url;
|
|
142
|
+
if (el instanceof HTMLMediaElement) el.load();
|
|
143
|
+
}).catch(() => {
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
} else {
|
|
147
|
+
el.src = src;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
function createMediaElement(tagName, value, mediaResolver) {
|
|
151
|
+
const wrapper = document.createElement("span");
|
|
152
|
+
wrapper.dataset.type = "html-inline";
|
|
153
|
+
wrapper.dataset.value = value;
|
|
154
|
+
wrapper.className = "html-media-wrapper";
|
|
155
|
+
wrapper.contentEditable = "false";
|
|
156
|
+
const el = document.createElement(tagName);
|
|
157
|
+
const stopForControls = (ev) => ev.stopPropagation();
|
|
158
|
+
el.addEventListener("mousedown", stopForControls);
|
|
159
|
+
el.addEventListener("click", stopForControls);
|
|
160
|
+
el.addEventListener("pointerdown", stopForControls);
|
|
161
|
+
const openTagMatch = value.match(new RegExp(`^<${tagName}\\b[^>]*>`, "i"));
|
|
162
|
+
const openTag = openTagMatch ? openTagMatch[0] : "";
|
|
163
|
+
const attrs = extractAllHtmlAttrs(openTag);
|
|
164
|
+
for (const [key, val] of Object.entries(attrs)) {
|
|
165
|
+
if (key === "src") continue;
|
|
166
|
+
if (key.startsWith("on")) continue;
|
|
167
|
+
el.setAttribute(key, val);
|
|
168
|
+
}
|
|
169
|
+
const strippedTag = openTag.replace(/=\s*(?:"[^"]*"|'[^']*'|[^\s>]+)/g, "");
|
|
170
|
+
const boolAttrs = ["controls", "autoplay", "loop", "muted", "playsinline"];
|
|
171
|
+
for (const attr of boolAttrs) {
|
|
172
|
+
if (!(attr in attrs) && new RegExp(`\\b${attr}\\b`, "i").test(strippedTag)) {
|
|
173
|
+
el.setAttribute(attr, "");
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
if (tagName === "audio" && !attrs.preload) {
|
|
177
|
+
el.setAttribute("preload", "auto");
|
|
178
|
+
}
|
|
179
|
+
const sourceRe = /<source\b[^>]*\/?>/gi;
|
|
180
|
+
let srcMatch;
|
|
181
|
+
while ((srcMatch = sourceRe.exec(value)) !== null) {
|
|
182
|
+
const srcAttrs = extractAllHtmlAttrs(srcMatch[0]);
|
|
183
|
+
if (!srcAttrs.src) continue;
|
|
184
|
+
const source = document.createElement("source");
|
|
185
|
+
if (srcAttrs.type) source.type = srcAttrs.type;
|
|
186
|
+
setMediaSrc(source, srcAttrs.src, mediaResolver);
|
|
187
|
+
el.appendChild(source);
|
|
188
|
+
}
|
|
189
|
+
if (attrs.src) {
|
|
190
|
+
setMediaSrc(el, attrs.src, mediaResolver);
|
|
191
|
+
}
|
|
192
|
+
wrapper.appendChild(el);
|
|
193
|
+
return wrapper;
|
|
194
|
+
}
|
|
195
|
+
var doc = {
|
|
196
|
+
content: "block+"
|
|
197
|
+
};
|
|
198
|
+
var text = { group: "inline" };
|
|
199
|
+
var paragraph = {
|
|
200
|
+
content: "inline*",
|
|
201
|
+
group: "block",
|
|
202
|
+
parseDOM: [{ tag: "p" }],
|
|
203
|
+
toDOM() {
|
|
204
|
+
return ["p", 0];
|
|
205
|
+
}
|
|
206
|
+
};
|
|
207
|
+
var heading = {
|
|
208
|
+
attrs: {
|
|
209
|
+
id: { default: "" },
|
|
210
|
+
level: { default: 1 }
|
|
211
|
+
},
|
|
212
|
+
content: "inline*",
|
|
213
|
+
group: "block",
|
|
214
|
+
defining: true,
|
|
215
|
+
parseDOM: [1, 2, 3, 4, 5, 6].map((level) => ({
|
|
216
|
+
tag: `h${level}`,
|
|
217
|
+
getAttrs(dom) {
|
|
218
|
+
return { level, id: dom.getAttribute("id") || "" };
|
|
219
|
+
}
|
|
220
|
+
})),
|
|
221
|
+
toDOM(node) {
|
|
222
|
+
const attrs = {};
|
|
223
|
+
if (node.attrs.id) attrs.id = node.attrs.id;
|
|
224
|
+
return [`h${node.attrs.level}`, attrs, 0];
|
|
225
|
+
}
|
|
226
|
+
};
|
|
227
|
+
var blockquote = {
|
|
228
|
+
content: "block+",
|
|
229
|
+
group: "block",
|
|
230
|
+
defining: true,
|
|
231
|
+
parseDOM: [{ tag: "blockquote" }],
|
|
232
|
+
toDOM() {
|
|
233
|
+
return ["blockquote", 0];
|
|
234
|
+
}
|
|
235
|
+
};
|
|
236
|
+
var code_block = {
|
|
237
|
+
content: "text*",
|
|
238
|
+
group: "block",
|
|
239
|
+
marks: "",
|
|
240
|
+
defining: true,
|
|
241
|
+
code: true,
|
|
242
|
+
attrs: {
|
|
243
|
+
language: { default: "text" }
|
|
244
|
+
},
|
|
245
|
+
parseDOM: [{
|
|
246
|
+
tag: "pre",
|
|
247
|
+
preserveWhitespace: "full",
|
|
248
|
+
getAttrs(dom) {
|
|
249
|
+
return { language: dom.dataset.language || "text" };
|
|
250
|
+
}
|
|
251
|
+
}],
|
|
252
|
+
toDOM(node) {
|
|
253
|
+
return ["pre", { "data-language": node.attrs.language || void 0 }, ["code", 0]];
|
|
254
|
+
}
|
|
255
|
+
};
|
|
256
|
+
var horizontal_rule = {
|
|
257
|
+
group: "block",
|
|
258
|
+
parseDOM: [{ tag: "hr" }],
|
|
259
|
+
toDOM() {
|
|
260
|
+
return ["hr"];
|
|
261
|
+
}
|
|
262
|
+
};
|
|
263
|
+
var bullet_list = {
|
|
264
|
+
content: "list_item+",
|
|
265
|
+
group: "block",
|
|
266
|
+
parseDOM: [{ tag: "ul" }],
|
|
267
|
+
toDOM() {
|
|
268
|
+
return ["ul", 0];
|
|
269
|
+
}
|
|
270
|
+
};
|
|
271
|
+
var ordered_list = {
|
|
272
|
+
content: "list_item+",
|
|
273
|
+
group: "block",
|
|
274
|
+
attrs: {
|
|
275
|
+
order: { default: 1 }
|
|
276
|
+
},
|
|
277
|
+
parseDOM: [{
|
|
278
|
+
tag: "ol",
|
|
279
|
+
getAttrs(dom) {
|
|
280
|
+
return { order: dom.hasAttribute("start") ? +(dom.getAttribute("start") || 1) : 1 };
|
|
281
|
+
}
|
|
282
|
+
}],
|
|
283
|
+
toDOM(node) {
|
|
284
|
+
return node.attrs.order === 1 ? ["ol", 0] : ["ol", { start: node.attrs.order }, 0];
|
|
285
|
+
}
|
|
286
|
+
};
|
|
287
|
+
var list_item = {
|
|
288
|
+
content: "paragraph block*",
|
|
289
|
+
group: "listItem",
|
|
290
|
+
defining: true,
|
|
291
|
+
attrs: {
|
|
292
|
+
label: { default: "\u2022" },
|
|
293
|
+
listType: { default: "bullet" },
|
|
294
|
+
spread: { default: "true" },
|
|
295
|
+
checked: { default: null }
|
|
296
|
+
},
|
|
297
|
+
parseDOM: [
|
|
298
|
+
{
|
|
299
|
+
tag: 'li[data-item-type="task"]',
|
|
300
|
+
getAttrs(dom) {
|
|
301
|
+
return {
|
|
302
|
+
label: dom.dataset.label,
|
|
303
|
+
listType: dom.dataset.listType,
|
|
304
|
+
spread: dom.dataset.spread,
|
|
305
|
+
checked: dom.dataset.checked ? dom.dataset.checked === "true" : null
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
},
|
|
309
|
+
{
|
|
310
|
+
tag: "li",
|
|
311
|
+
getAttrs(dom) {
|
|
312
|
+
return {
|
|
313
|
+
label: dom.dataset.label || "\u2022",
|
|
314
|
+
listType: dom.dataset.listType || "bullet",
|
|
315
|
+
spread: dom.dataset.spread || "true"
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
],
|
|
320
|
+
toDOM(node) {
|
|
321
|
+
if (node.attrs.checked != null) {
|
|
322
|
+
return ["li", {
|
|
323
|
+
"data-item-type": "task",
|
|
324
|
+
"data-label": node.attrs.label,
|
|
325
|
+
"data-list-type": node.attrs.listType,
|
|
326
|
+
"data-spread": node.attrs.spread,
|
|
327
|
+
"data-checked": String(node.attrs.checked)
|
|
328
|
+
}, 0];
|
|
329
|
+
}
|
|
330
|
+
return ["li", {
|
|
331
|
+
"data-label": node.attrs.label,
|
|
332
|
+
"data-list-type": node.attrs.listType,
|
|
333
|
+
"data-spread": node.attrs.spread
|
|
334
|
+
}, 0];
|
|
335
|
+
}
|
|
336
|
+
};
|
|
337
|
+
var hardbreak = {
|
|
338
|
+
inline: true,
|
|
339
|
+
group: "inline",
|
|
340
|
+
selectable: false,
|
|
341
|
+
attrs: {
|
|
342
|
+
isInline: { default: false }
|
|
343
|
+
},
|
|
344
|
+
parseDOM: [
|
|
345
|
+
{ tag: "br" },
|
|
346
|
+
{
|
|
347
|
+
tag: 'span[data-type="hardbreak"]',
|
|
348
|
+
getAttrs() {
|
|
349
|
+
return { isInline: true };
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
],
|
|
353
|
+
toDOM() {
|
|
354
|
+
return ["span", { "data-type": "hardbreak", "class": "hardbreak-marker" }, "\n"];
|
|
355
|
+
},
|
|
356
|
+
leafText() {
|
|
357
|
+
return "\n";
|
|
358
|
+
}
|
|
359
|
+
};
|
|
360
|
+
var html_block = {
|
|
361
|
+
content: "text*",
|
|
362
|
+
group: "block",
|
|
363
|
+
marks: "",
|
|
364
|
+
code: true,
|
|
365
|
+
defining: true,
|
|
366
|
+
parseDOM: [{
|
|
367
|
+
tag: 'div[data-type="html"]',
|
|
368
|
+
preserveWhitespace: "full"
|
|
369
|
+
}],
|
|
370
|
+
toDOM() {
|
|
371
|
+
return ["div", { "data-type": "html" }, ["pre", 0]];
|
|
372
|
+
}
|
|
373
|
+
};
|
|
374
|
+
var table = {
|
|
375
|
+
content: "table_header_row table_row+",
|
|
376
|
+
group: "block",
|
|
377
|
+
tableRole: "table",
|
|
378
|
+
isolating: true,
|
|
379
|
+
parseDOM: [{ tag: "table" }],
|
|
380
|
+
toDOM() {
|
|
381
|
+
return ["table", ["tbody", 0]];
|
|
382
|
+
}
|
|
383
|
+
};
|
|
384
|
+
var table_header_row = {
|
|
385
|
+
content: "(table_header)*",
|
|
386
|
+
tableRole: "row",
|
|
387
|
+
parseDOM: [
|
|
388
|
+
{ tag: "tr[data-is-header]" },
|
|
389
|
+
{
|
|
390
|
+
tag: "tr",
|
|
391
|
+
getAttrs(dom) {
|
|
392
|
+
const hasHeader = dom.querySelector("th");
|
|
393
|
+
return hasHeader ? {} : false;
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
],
|
|
397
|
+
toDOM() {
|
|
398
|
+
return ["tr", { "data-is-header": "true" }, 0];
|
|
399
|
+
}
|
|
400
|
+
};
|
|
401
|
+
var table_row = {
|
|
402
|
+
content: "(table_cell)*",
|
|
403
|
+
tableRole: "row",
|
|
404
|
+
parseDOM: [{ tag: "tr" }],
|
|
405
|
+
toDOM() {
|
|
406
|
+
return ["tr", 0];
|
|
407
|
+
}
|
|
408
|
+
};
|
|
409
|
+
var table_header = {
|
|
410
|
+
content: "paragraph+",
|
|
411
|
+
tableRole: "header_cell",
|
|
412
|
+
attrs: {
|
|
413
|
+
alignment: { default: "left" },
|
|
414
|
+
colspan: { default: 1 },
|
|
415
|
+
rowspan: { default: 1 },
|
|
416
|
+
colwidth: { default: null }
|
|
417
|
+
},
|
|
418
|
+
isolating: true,
|
|
419
|
+
parseDOM: [{
|
|
420
|
+
tag: "th",
|
|
421
|
+
getAttrs(dom) {
|
|
422
|
+
return {
|
|
423
|
+
alignment: dom.style.textAlign || "left",
|
|
424
|
+
colspan: Number(dom.getAttribute("colspan") || 1),
|
|
425
|
+
rowspan: Number(dom.getAttribute("rowspan") || 1),
|
|
426
|
+
colwidth: null
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
}],
|
|
430
|
+
toDOM(node) {
|
|
431
|
+
return ["th", { style: `text-align: ${node.attrs.alignment || "left"}` }, 0];
|
|
432
|
+
}
|
|
433
|
+
};
|
|
434
|
+
var table_cell = {
|
|
435
|
+
content: "paragraph+",
|
|
436
|
+
tableRole: "cell",
|
|
437
|
+
attrs: {
|
|
438
|
+
alignment: { default: "left" },
|
|
439
|
+
colspan: { default: 1 },
|
|
440
|
+
rowspan: { default: 1 },
|
|
441
|
+
colwidth: { default: null }
|
|
442
|
+
},
|
|
443
|
+
isolating: true,
|
|
444
|
+
parseDOM: [{
|
|
445
|
+
tag: "td",
|
|
446
|
+
getAttrs(dom) {
|
|
447
|
+
return {
|
|
448
|
+
alignment: dom.style.textAlign || "left",
|
|
449
|
+
colspan: Number(dom.getAttribute("colspan") || 1),
|
|
450
|
+
rowspan: Number(dom.getAttribute("rowspan") || 1),
|
|
451
|
+
colwidth: null
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
}],
|
|
455
|
+
toDOM(node) {
|
|
456
|
+
return ["td", { style: `text-align: ${node.attrs.alignment || "left"}` }, 0];
|
|
457
|
+
}
|
|
458
|
+
};
|
|
459
|
+
var math_inline = {
|
|
460
|
+
group: "inline",
|
|
461
|
+
content: "text*",
|
|
462
|
+
inline: true,
|
|
463
|
+
atom: true,
|
|
464
|
+
parseDOM: [{
|
|
465
|
+
tag: 'span[data-type="math_inline"]',
|
|
466
|
+
getContent(dom, schema) {
|
|
467
|
+
if (!(dom instanceof HTMLElement)) return Fragment.empty;
|
|
468
|
+
const value = dom.dataset.value ?? "";
|
|
469
|
+
if (!value) return Fragment.empty;
|
|
470
|
+
return Fragment.from(schema.text(value));
|
|
471
|
+
}
|
|
472
|
+
}],
|
|
473
|
+
toDOM(node) {
|
|
474
|
+
const code2 = node.textContent;
|
|
475
|
+
const dom = document.createElement("span");
|
|
476
|
+
dom.dataset.type = "math_inline";
|
|
477
|
+
dom.dataset.value = code2;
|
|
478
|
+
try {
|
|
479
|
+
katex.render(code2, dom);
|
|
480
|
+
} catch {
|
|
481
|
+
dom.textContent = code2;
|
|
482
|
+
dom.classList.add("math-error");
|
|
483
|
+
dom.setAttribute("data-math-type", "inline");
|
|
484
|
+
}
|
|
485
|
+
return dom;
|
|
486
|
+
}
|
|
487
|
+
};
|
|
488
|
+
var math_block = {
|
|
489
|
+
content: "text*",
|
|
490
|
+
group: "block",
|
|
491
|
+
marks: "",
|
|
492
|
+
defining: true,
|
|
493
|
+
atom: true,
|
|
494
|
+
isolating: true,
|
|
495
|
+
attrs: {
|
|
496
|
+
value: { default: "" }
|
|
497
|
+
},
|
|
498
|
+
parseDOM: [{
|
|
499
|
+
tag: 'div[data-type="math_block"]',
|
|
500
|
+
preserveWhitespace: "full",
|
|
501
|
+
getAttrs(dom) {
|
|
502
|
+
return { value: dom.dataset.value ?? "" };
|
|
503
|
+
}
|
|
504
|
+
}],
|
|
505
|
+
toDOM(node) {
|
|
506
|
+
const code2 = node.attrs.value;
|
|
507
|
+
const dom = document.createElement("div");
|
|
508
|
+
dom.dataset.type = "math_block";
|
|
509
|
+
dom.dataset.value = code2;
|
|
510
|
+
try {
|
|
511
|
+
katex.render(code2, dom, { displayMode: true });
|
|
512
|
+
} catch {
|
|
513
|
+
dom.textContent = code2;
|
|
514
|
+
dom.classList.add("math-error");
|
|
515
|
+
dom.setAttribute("data-math-type", "block");
|
|
516
|
+
}
|
|
517
|
+
return dom;
|
|
518
|
+
}
|
|
519
|
+
};
|
|
520
|
+
var defList = {
|
|
521
|
+
content: "(defListTerm | defListDescription)+",
|
|
522
|
+
group: "block",
|
|
523
|
+
defining: true,
|
|
524
|
+
parseDOM: [{ tag: "dl" }],
|
|
525
|
+
toDOM() {
|
|
526
|
+
return ["dl", { class: "definition-list" }, 0];
|
|
527
|
+
}
|
|
528
|
+
};
|
|
529
|
+
var defListTerm = {
|
|
530
|
+
content: "inline*",
|
|
531
|
+
group: "block",
|
|
532
|
+
defining: true,
|
|
533
|
+
parseDOM: [{ tag: "dt" }],
|
|
534
|
+
toDOM() {
|
|
535
|
+
return ["dt", 0];
|
|
536
|
+
}
|
|
537
|
+
};
|
|
538
|
+
var defListDescription = {
|
|
539
|
+
content: "block+",
|
|
540
|
+
group: "block",
|
|
541
|
+
defining: true,
|
|
542
|
+
parseDOM: [{ tag: "dd" }],
|
|
543
|
+
toDOM() {
|
|
544
|
+
return ["dd", 0];
|
|
545
|
+
}
|
|
546
|
+
};
|
|
547
|
+
var strong = {
|
|
548
|
+
parseDOM: [
|
|
549
|
+
{
|
|
550
|
+
tag: "b",
|
|
551
|
+
getAttrs(dom) {
|
|
552
|
+
return dom.style.fontWeight !== "normal" && null;
|
|
553
|
+
}
|
|
554
|
+
},
|
|
555
|
+
{ tag: "strong" },
|
|
556
|
+
{
|
|
557
|
+
style: "font-weight",
|
|
558
|
+
getAttrs(value) {
|
|
559
|
+
return /^(bold(er)?|[5-9]\d{2,})$/.test(value) && null;
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
],
|
|
563
|
+
toDOM() {
|
|
564
|
+
return ["strong", 0];
|
|
565
|
+
}
|
|
566
|
+
};
|
|
567
|
+
var em = {
|
|
568
|
+
parseDOM: [
|
|
569
|
+
{ tag: "i" },
|
|
570
|
+
{ tag: "em" },
|
|
571
|
+
{
|
|
572
|
+
style: "font-style",
|
|
573
|
+
getAttrs(value) {
|
|
574
|
+
return value === "italic" && null;
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
],
|
|
578
|
+
toDOM() {
|
|
579
|
+
return ["em", 0];
|
|
580
|
+
}
|
|
581
|
+
};
|
|
582
|
+
var code = {
|
|
583
|
+
priority: 100,
|
|
584
|
+
code: true,
|
|
585
|
+
inclusive: false,
|
|
586
|
+
parseDOM: [{ tag: "code" }],
|
|
587
|
+
toDOM() {
|
|
588
|
+
return ["code", 0];
|
|
589
|
+
}
|
|
590
|
+
};
|
|
591
|
+
var link = {
|
|
592
|
+
attrs: {
|
|
593
|
+
href: {},
|
|
594
|
+
title: { default: null }
|
|
595
|
+
},
|
|
596
|
+
inclusive: false,
|
|
597
|
+
parseDOM: [{
|
|
598
|
+
tag: "a[href]",
|
|
599
|
+
getAttrs(dom) {
|
|
600
|
+
return {
|
|
601
|
+
href: dom.getAttribute("href"),
|
|
602
|
+
title: dom.getAttribute("title")
|
|
603
|
+
};
|
|
604
|
+
}
|
|
605
|
+
}],
|
|
606
|
+
toDOM(mark) {
|
|
607
|
+
const attrs = { href: mark.attrs.href };
|
|
608
|
+
if (mark.attrs.title) attrs.title = mark.attrs.title;
|
|
609
|
+
return ["a", attrs, 0];
|
|
610
|
+
}
|
|
611
|
+
};
|
|
612
|
+
var strike_through = {
|
|
613
|
+
parseDOM: [
|
|
614
|
+
{ tag: "del" },
|
|
615
|
+
{ tag: "s" },
|
|
616
|
+
{
|
|
617
|
+
style: "text-decoration",
|
|
618
|
+
getAttrs(value) {
|
|
619
|
+
return value === "line-through" && null;
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
],
|
|
623
|
+
toDOM() {
|
|
624
|
+
return ["del", 0];
|
|
625
|
+
}
|
|
626
|
+
};
|
|
627
|
+
var html_mark = {
|
|
628
|
+
attrs: {
|
|
629
|
+
openTag: { default: "" },
|
|
630
|
+
closeTag: { default: "" }
|
|
631
|
+
},
|
|
632
|
+
excludes: "",
|
|
633
|
+
// Allow nesting multiple html_marks (e.g., <font><u>text</u></font>)
|
|
634
|
+
parseDOM: [{
|
|
635
|
+
tag: '[data-type="html-mark"]',
|
|
636
|
+
getAttrs(dom) {
|
|
637
|
+
return {
|
|
638
|
+
openTag: dom.dataset.openTag ?? "",
|
|
639
|
+
closeTag: dom.dataset.closeTag ?? ""
|
|
640
|
+
};
|
|
641
|
+
}
|
|
642
|
+
}],
|
|
643
|
+
toDOM(mark) {
|
|
644
|
+
const openTag = mark.attrs.openTag;
|
|
645
|
+
const tagMatch = openTag.match(/^<([a-zA-Z][a-zA-Z0-9]*)/);
|
|
646
|
+
const tagName = tagMatch && tagMatch[1] ? tagMatch[1].toLowerCase() : "span";
|
|
647
|
+
const attrs = {
|
|
648
|
+
"data-type": "html-mark",
|
|
649
|
+
"data-open-tag": openTag,
|
|
650
|
+
"data-close-tag": mark.attrs.closeTag
|
|
651
|
+
};
|
|
652
|
+
const semanticTags = ["sub", "sup", "u", "ins", "mark", "small", "big", "kbd", "abbr"];
|
|
653
|
+
if (semanticTags.includes(tagName)) {
|
|
654
|
+
return [tagName, attrs, 0];
|
|
655
|
+
}
|
|
656
|
+
const style = htmlTagToStyle(openTag);
|
|
657
|
+
if (style) attrs.style = style;
|
|
658
|
+
return ["span", attrs, 0];
|
|
659
|
+
}
|
|
660
|
+
};
|
|
661
|
+
function buildImageNodeSpec(mediaResolver) {
|
|
662
|
+
return {
|
|
663
|
+
inline: true,
|
|
664
|
+
group: "inline",
|
|
665
|
+
selectable: true,
|
|
666
|
+
draggable: true,
|
|
667
|
+
marks: "",
|
|
668
|
+
atom: true,
|
|
669
|
+
defining: true,
|
|
670
|
+
isolating: true,
|
|
671
|
+
attrs: {
|
|
672
|
+
src: { default: "" },
|
|
673
|
+
alt: { default: "" },
|
|
674
|
+
title: { default: "" }
|
|
675
|
+
},
|
|
676
|
+
parseDOM: [{
|
|
677
|
+
tag: "img[src]",
|
|
678
|
+
getAttrs(dom) {
|
|
679
|
+
return {
|
|
680
|
+
src: dom.getAttribute("src") || "",
|
|
681
|
+
alt: dom.getAttribute("alt") || "",
|
|
682
|
+
title: dom.getAttribute("title") || dom.getAttribute("alt") || ""
|
|
683
|
+
};
|
|
684
|
+
}
|
|
685
|
+
}],
|
|
686
|
+
toDOM(node) {
|
|
687
|
+
const container = document.createElement("span");
|
|
688
|
+
container.className = "image-node";
|
|
689
|
+
const img = document.createElement("img");
|
|
690
|
+
if (node.attrs.alt) img.alt = node.attrs.alt;
|
|
691
|
+
if (node.attrs.title) img.title = node.attrs.title;
|
|
692
|
+
const titleStr = node.attrs.title || "";
|
|
693
|
+
const widthMatch = titleStr.match(/^width=(\d+%?)$/);
|
|
694
|
+
const widthVal = widthMatch?.[1];
|
|
695
|
+
if (widthVal) {
|
|
696
|
+
img.style.width = widthVal.includes("%") ? widthVal : `${widthVal}px`;
|
|
697
|
+
img.style.maxWidth = "none";
|
|
698
|
+
}
|
|
699
|
+
img.onerror = () => {
|
|
700
|
+
const alt = node.attrs.alt ? `![${node.attrs.alt}]` : "![]";
|
|
701
|
+
const title = node.attrs.title ? ` "${node.attrs.title}"` : "";
|
|
702
|
+
showBrokenImage(container, `${alt}(${node.attrs.src}${title})`);
|
|
703
|
+
};
|
|
704
|
+
const src = node.attrs.src;
|
|
705
|
+
if (isAbsoluteFilePath(src)) {
|
|
706
|
+
loadLocalImageSrc(img, src, mediaResolver);
|
|
707
|
+
} else if (isRelativePath(src)) {
|
|
708
|
+
loadLocalImageSrc(img, resolveRelativePath(src), mediaResolver);
|
|
709
|
+
} else {
|
|
710
|
+
img.src = src;
|
|
711
|
+
}
|
|
712
|
+
container.appendChild(img);
|
|
713
|
+
return container;
|
|
714
|
+
}
|
|
715
|
+
};
|
|
716
|
+
}
|
|
717
|
+
function buildHtmlInlineNodeSpec(mediaResolver) {
|
|
718
|
+
return {
|
|
719
|
+
group: "inline",
|
|
720
|
+
inline: true,
|
|
721
|
+
atom: true,
|
|
722
|
+
attrs: {
|
|
723
|
+
value: { default: "" }
|
|
724
|
+
},
|
|
725
|
+
parseDOM: [{
|
|
726
|
+
tag: 'span[data-type="html-inline"]',
|
|
727
|
+
getAttrs(dom) {
|
|
728
|
+
return { value: dom.dataset.value ?? "" };
|
|
729
|
+
}
|
|
730
|
+
}],
|
|
731
|
+
toDOM(node) {
|
|
732
|
+
const value = node.attrs.value;
|
|
733
|
+
if (/^<img\s/i.test(value)) {
|
|
734
|
+
const wrapper = document.createElement("span");
|
|
735
|
+
wrapper.dataset.type = "html-inline";
|
|
736
|
+
wrapper.dataset.value = value;
|
|
737
|
+
wrapper.className = "html-img-wrapper";
|
|
738
|
+
const attrs = extractAllHtmlAttrs(value);
|
|
739
|
+
const src = attrs.src || "";
|
|
740
|
+
if (src) {
|
|
741
|
+
const img = document.createElement("img");
|
|
742
|
+
for (const [key, val] of Object.entries(attrs)) {
|
|
743
|
+
if (key === "src") continue;
|
|
744
|
+
if (key === "onerror" || key === "onload" || key.startsWith("on")) continue;
|
|
745
|
+
img.setAttribute(key, val);
|
|
746
|
+
}
|
|
747
|
+
img.onerror = () => {
|
|
748
|
+
showBrokenImage(wrapper, value);
|
|
749
|
+
};
|
|
750
|
+
if (isAbsoluteFilePath(src)) {
|
|
751
|
+
loadLocalImageSrc(img, src, mediaResolver);
|
|
752
|
+
} else if (isRelativePath(src)) {
|
|
753
|
+
loadLocalImageSrc(img, resolveRelativePath(src), mediaResolver);
|
|
754
|
+
} else {
|
|
755
|
+
img.src = src;
|
|
756
|
+
}
|
|
757
|
+
wrapper.appendChild(img);
|
|
758
|
+
} else {
|
|
759
|
+
showBrokenImage(wrapper, value);
|
|
760
|
+
}
|
|
761
|
+
return wrapper;
|
|
762
|
+
}
|
|
763
|
+
if (/^<video\b/i.test(value)) return createMediaElement("video", value, mediaResolver);
|
|
764
|
+
if (/^<audio\b/i.test(value)) return createMediaElement("audio", value, mediaResolver);
|
|
765
|
+
return ["span", { "data-type": "html-inline", "data-value": value }];
|
|
766
|
+
}
|
|
767
|
+
};
|
|
768
|
+
}
|
|
769
|
+
function buildNodes(mediaResolver) {
|
|
770
|
+
return {
|
|
771
|
+
doc,
|
|
772
|
+
text,
|
|
773
|
+
paragraph,
|
|
774
|
+
heading,
|
|
775
|
+
blockquote,
|
|
776
|
+
code_block,
|
|
777
|
+
horizontal_rule,
|
|
778
|
+
bullet_list,
|
|
779
|
+
ordered_list,
|
|
780
|
+
list_item,
|
|
781
|
+
image: buildImageNodeSpec(mediaResolver),
|
|
782
|
+
hardbreak,
|
|
783
|
+
html_block,
|
|
784
|
+
html_inline: buildHtmlInlineNodeSpec(mediaResolver),
|
|
785
|
+
table,
|
|
786
|
+
table_header_row,
|
|
787
|
+
table_row,
|
|
788
|
+
table_header,
|
|
789
|
+
table_cell,
|
|
790
|
+
math_inline,
|
|
791
|
+
math_block,
|
|
792
|
+
defList,
|
|
793
|
+
defListTerm,
|
|
794
|
+
defListDescription
|
|
795
|
+
};
|
|
796
|
+
}
|
|
797
|
+
var marks = {
|
|
798
|
+
html_mark,
|
|
799
|
+
strong,
|
|
800
|
+
em,
|
|
801
|
+
code,
|
|
802
|
+
link,
|
|
803
|
+
strike_through
|
|
804
|
+
};
|
|
805
|
+
var nullMediaResolver = {
|
|
806
|
+
[NULL_MEDIA_RESOLVER_SENTINEL]: true,
|
|
807
|
+
async loadLocalImage() {
|
|
808
|
+
return "";
|
|
809
|
+
},
|
|
810
|
+
async loadLocalMedia() {
|
|
811
|
+
return "";
|
|
812
|
+
},
|
|
813
|
+
async loadRemoteMedia(url) {
|
|
814
|
+
return url;
|
|
815
|
+
}
|
|
816
|
+
};
|
|
817
|
+
var defaultSchema = new Schema({
|
|
818
|
+
nodes: buildNodes(nullMediaResolver),
|
|
819
|
+
marks
|
|
820
|
+
});
|
|
821
|
+
|
|
822
|
+
// src/markdown.ts
|
|
823
|
+
var md = new MarkdownIt({
|
|
824
|
+
html: true,
|
|
825
|
+
linkify: false,
|
|
826
|
+
typographer: false
|
|
827
|
+
}).enable(["table", "strikethrough"]).use(deflistPlugin).use(texmathPlugin);
|
|
828
|
+
function tagPairedHtmlInline(tokens) {
|
|
829
|
+
const VOID_RE = /^<(?:br|hr|img|input|wbr|area|base|col|embed|link|meta|param|source|track)[\s/>]/i;
|
|
830
|
+
for (const token of tokens) {
|
|
831
|
+
if (token.type !== "inline" || !token.children) continue;
|
|
832
|
+
const children = token.children;
|
|
833
|
+
const stack = [];
|
|
834
|
+
for (let i = 0; i < children.length; i++) {
|
|
835
|
+
const child = children[i];
|
|
836
|
+
if (!child || child.type !== "html_inline") continue;
|
|
837
|
+
const content = child.content;
|
|
838
|
+
if (VOID_RE.test(content) || /\/>$/.test(content) || /^<!--/.test(content)) continue;
|
|
839
|
+
const closeMatch = content.match(/^<\/([a-zA-Z][a-zA-Z0-9]*)\s*>$/);
|
|
840
|
+
if (closeMatch && closeMatch[1]) {
|
|
841
|
+
const tagName = closeMatch[1].toLowerCase();
|
|
842
|
+
for (let j = stack.length - 1; j >= 0; j--) {
|
|
843
|
+
const entry = stack[j];
|
|
844
|
+
if (!entry) continue;
|
|
845
|
+
if (entry.tagName === tagName) {
|
|
846
|
+
const opener = children[entry.index];
|
|
847
|
+
if (opener) {
|
|
848
|
+
opener.meta = { ...opener.meta || {}, htmlPaired: true };
|
|
849
|
+
}
|
|
850
|
+
child.meta = { ...child.meta || {}, htmlPaired: true };
|
|
851
|
+
stack.splice(j, 1);
|
|
852
|
+
break;
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
continue;
|
|
856
|
+
}
|
|
857
|
+
const openMatch = content.match(/^<([a-zA-Z][a-zA-Z0-9]*)\b[^>]*>$/);
|
|
858
|
+
if (openMatch && openMatch[1]) {
|
|
859
|
+
stack.push({ tagName: openMatch[1].toLowerCase(), index: i });
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
function preserveBlankLines(tokens) {
|
|
865
|
+
function mkToken(type, tag, nesting, extra) {
|
|
866
|
+
return {
|
|
867
|
+
type,
|
|
868
|
+
tag,
|
|
869
|
+
nesting,
|
|
870
|
+
content: "",
|
|
871
|
+
children: null,
|
|
872
|
+
attrs: null,
|
|
873
|
+
info: "",
|
|
874
|
+
meta: null,
|
|
875
|
+
map: null,
|
|
876
|
+
block: true,
|
|
877
|
+
hidden: false,
|
|
878
|
+
level: 0,
|
|
879
|
+
markup: "",
|
|
880
|
+
...extra
|
|
881
|
+
};
|
|
882
|
+
}
|
|
883
|
+
const result = [];
|
|
884
|
+
let lastTopBlockEndLine = 0;
|
|
885
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
886
|
+
const tok = tokens[i];
|
|
887
|
+
if (!tok) continue;
|
|
888
|
+
if (tok.map && tok.level === 0 && (tok.nesting === 1 || tok.nesting === 0)) {
|
|
889
|
+
const startLine = tok.map[0];
|
|
890
|
+
const gap = startLine - lastTopBlockEndLine;
|
|
891
|
+
if (gap > 1 && lastTopBlockEndLine > 0) {
|
|
892
|
+
const extra = gap - 1;
|
|
893
|
+
for (let j = 0; j < extra; j++) {
|
|
894
|
+
result.push(
|
|
895
|
+
mkToken("paragraph_open", "p", 1),
|
|
896
|
+
mkToken("inline", "", 0, { level: 1, block: false, children: [] }),
|
|
897
|
+
mkToken("paragraph_close", "p", -1)
|
|
898
|
+
);
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
lastTopBlockEndLine = tok.map[1];
|
|
902
|
+
}
|
|
903
|
+
result.push(tok);
|
|
904
|
+
}
|
|
905
|
+
return result;
|
|
906
|
+
}
|
|
907
|
+
var _origMdParse = md.parse.bind(md);
|
|
908
|
+
md.parse = function(src, env) {
|
|
909
|
+
let tokens = _origMdParse(src, env);
|
|
910
|
+
tagPairedHtmlInline(tokens);
|
|
911
|
+
tokens = preserveBlankLines(tokens);
|
|
912
|
+
return tokens;
|
|
913
|
+
};
|
|
914
|
+
var parserTokens = {
|
|
915
|
+
// ── Block tokens ──
|
|
916
|
+
paragraph: { block: "paragraph" },
|
|
917
|
+
blockquote: { block: "blockquote" },
|
|
918
|
+
heading: {
|
|
919
|
+
block: "heading",
|
|
920
|
+
getAttrs(token) {
|
|
921
|
+
return { level: Number(token.tag.slice(1)) };
|
|
922
|
+
}
|
|
923
|
+
},
|
|
924
|
+
hr: { node: "horizontal_rule" },
|
|
925
|
+
bullet_list: { block: "bullet_list" },
|
|
926
|
+
ordered_list: {
|
|
927
|
+
block: "ordered_list",
|
|
928
|
+
getAttrs(token) {
|
|
929
|
+
return { order: Number(token.attrGet("start") || 1) };
|
|
930
|
+
}
|
|
931
|
+
},
|
|
932
|
+
list_item: {
|
|
933
|
+
block: "list_item",
|
|
934
|
+
getAttrs(_token, tokens, index) {
|
|
935
|
+
let checked = null;
|
|
936
|
+
for (let i = index + 1; i < tokens.length; i++) {
|
|
937
|
+
const t = tokens[i];
|
|
938
|
+
if (!t) continue;
|
|
939
|
+
if (t.type === "inline" && t.content) {
|
|
940
|
+
const match = t.content.match(/^\[( |x|X)\]\s?/);
|
|
941
|
+
if (match) {
|
|
942
|
+
checked = match[1] !== " ";
|
|
943
|
+
t.content = t.content.slice(match[0].length);
|
|
944
|
+
const children = t.children;
|
|
945
|
+
if (children && children.length > 0) {
|
|
946
|
+
const firstChild = children[0];
|
|
947
|
+
if (firstChild.type === "text") {
|
|
948
|
+
firstChild.content = firstChild.content.slice(match[0].length);
|
|
949
|
+
if (!firstChild.content) {
|
|
950
|
+
children.shift();
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
break;
|
|
956
|
+
}
|
|
957
|
+
if (t.type === "list_item_close") break;
|
|
958
|
+
}
|
|
959
|
+
return { checked };
|
|
960
|
+
}
|
|
961
|
+
},
|
|
962
|
+
code_block: {
|
|
963
|
+
block: "code_block",
|
|
964
|
+
getAttrs() {
|
|
965
|
+
return { language: "text" };
|
|
966
|
+
},
|
|
967
|
+
noCloseToken: true
|
|
968
|
+
},
|
|
969
|
+
fence: {
|
|
970
|
+
block: "code_block",
|
|
971
|
+
getAttrs(token) {
|
|
972
|
+
return { language: token.info.trim() || "text" };
|
|
973
|
+
},
|
|
974
|
+
noCloseToken: true
|
|
975
|
+
},
|
|
976
|
+
html_block: {
|
|
977
|
+
block: "html_block",
|
|
978
|
+
noCloseToken: true
|
|
979
|
+
},
|
|
980
|
+
html_inline: {
|
|
981
|
+
// markdown-it emits this token for inline HTML like <br>, <span>, <sup>,
|
|
982
|
+
// and HTML comments <!-- ... -->. Store raw HTML in the `value` attr.
|
|
983
|
+
node: "html_inline",
|
|
984
|
+
noCloseToken: true,
|
|
985
|
+
getAttrs(token) {
|
|
986
|
+
return { value: token.content };
|
|
987
|
+
}
|
|
988
|
+
},
|
|
989
|
+
// ── Table tokens ──
|
|
990
|
+
// NOTE: tr/th/td are NOT listed here — they are handled by custom tokenHandler
|
|
991
|
+
// overrides in MorayaMarkdownParser below. The `block:` spec alone can't
|
|
992
|
+
// handle (a) thead-row → table_header_row vs table_row dispatch, or
|
|
993
|
+
// (b) wrapping inline content in the required paragraph child of each cell.
|
|
994
|
+
table: { block: "table" },
|
|
995
|
+
thead: { ignore: true },
|
|
996
|
+
tbody: { ignore: true },
|
|
997
|
+
// ── Definition list tokens ──
|
|
998
|
+
dl: { block: "defList" },
|
|
999
|
+
dt: { block: "defListTerm" },
|
|
1000
|
+
dd: { block: "defListDescription" },
|
|
1001
|
+
// ── Math tokens (from markdown-it-texmath) ──
|
|
1002
|
+
// Use block: spec (not node:) so token.content is added as text children,
|
|
1003
|
+
// correctly filling math_inline's `content: 'text*'`.
|
|
1004
|
+
math_inline: {
|
|
1005
|
+
block: "math_inline",
|
|
1006
|
+
noCloseToken: true
|
|
1007
|
+
},
|
|
1008
|
+
// markdown-it-texmath emits math_inline_double for $$...$$ in inline context.
|
|
1009
|
+
// Map to math_inline to prevent "Token type not supported" crash.
|
|
1010
|
+
math_inline_double: {
|
|
1011
|
+
block: "math_inline",
|
|
1012
|
+
noCloseToken: true
|
|
1013
|
+
},
|
|
1014
|
+
math_block: {
|
|
1015
|
+
node: "math_block",
|
|
1016
|
+
noCloseToken: true,
|
|
1017
|
+
getAttrs(token) {
|
|
1018
|
+
return { value: token.content.trim() };
|
|
1019
|
+
}
|
|
1020
|
+
},
|
|
1021
|
+
// ── Inline tokens ──
|
|
1022
|
+
image: {
|
|
1023
|
+
node: "image",
|
|
1024
|
+
getAttrs(token) {
|
|
1025
|
+
let src = token.attrGet("src") || "";
|
|
1026
|
+
try {
|
|
1027
|
+
src = decodeURIComponent(src);
|
|
1028
|
+
} catch {
|
|
1029
|
+
}
|
|
1030
|
+
return {
|
|
1031
|
+
src,
|
|
1032
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1033
|
+
alt: (token.children || []).map((c) => c.content).join("") || "",
|
|
1034
|
+
title: token.attrGet("title") || ""
|
|
1035
|
+
};
|
|
1036
|
+
}
|
|
1037
|
+
},
|
|
1038
|
+
hardbreak: { node: "hardbreak" },
|
|
1039
|
+
softbreak: { node: "hardbreak", attrs: { isInline: true } },
|
|
1040
|
+
// ── Mark tokens ──
|
|
1041
|
+
em: { mark: "em" },
|
|
1042
|
+
strong: { mark: "strong" },
|
|
1043
|
+
s: { mark: "strike_through" },
|
|
1044
|
+
code_inline: { mark: "code", noCloseToken: true },
|
|
1045
|
+
link: {
|
|
1046
|
+
mark: "link",
|
|
1047
|
+
getAttrs(token) {
|
|
1048
|
+
let href = token.attrGet("href") || "";
|
|
1049
|
+
href = href.replace(
|
|
1050
|
+
/%[C-F][0-9A-F](?:%[89AB][0-9A-F])+/gi,
|
|
1051
|
+
(m) => {
|
|
1052
|
+
try {
|
|
1053
|
+
return decodeURIComponent(m);
|
|
1054
|
+
} catch {
|
|
1055
|
+
return m;
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
);
|
|
1059
|
+
return {
|
|
1060
|
+
href,
|
|
1061
|
+
title: token.attrGet("title") || null
|
|
1062
|
+
};
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
};
|
|
1066
|
+
var MorayaMarkdownParser = class extends MarkdownParser {
|
|
1067
|
+
/**
|
|
1068
|
+
* The schema this parser instance is bound to. Captured for use in
|
|
1069
|
+
* tokenHandler overrides (tr_open / th_open / etc.) so they reference the
|
|
1070
|
+
* caller-provided schema rather than the module-level defaultSchema.
|
|
1071
|
+
*/
|
|
1072
|
+
schema;
|
|
1073
|
+
constructor(schemaArg = defaultSchema) {
|
|
1074
|
+
super(schemaArg, md, parserTokens);
|
|
1075
|
+
this.schema = schemaArg;
|
|
1076
|
+
const h = (
|
|
1077
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1078
|
+
this.tokenHandlers
|
|
1079
|
+
);
|
|
1080
|
+
function cellAlignment(tok) {
|
|
1081
|
+
const style = tok.attrGet("style") || "";
|
|
1082
|
+
const m = style.match(/text-align:\s*(\w+)/);
|
|
1083
|
+
return m && m[1] ? m[1] : "left";
|
|
1084
|
+
}
|
|
1085
|
+
h["tr_open"] = (state, _tok, tokens, i) => {
|
|
1086
|
+
let inThead = false;
|
|
1087
|
+
for (let j = i - 1; j >= 0; j--) {
|
|
1088
|
+
if (tokens[j].type === "thead_open") {
|
|
1089
|
+
inThead = true;
|
|
1090
|
+
break;
|
|
1091
|
+
}
|
|
1092
|
+
if (tokens[j].type === "thead_close" || tokens[j].type === "tbody_open") break;
|
|
1093
|
+
}
|
|
1094
|
+
state.openNode(inThead ? schemaArg.nodes.table_header_row : schemaArg.nodes.table_row, null);
|
|
1095
|
+
};
|
|
1096
|
+
h["tr_close"] = (state) => state.closeNode();
|
|
1097
|
+
h["th_open"] = (state, tok) => {
|
|
1098
|
+
state.openNode(schemaArg.nodes.table_header, { alignment: cellAlignment(tok) });
|
|
1099
|
+
state.openNode(schemaArg.nodes.paragraph, null);
|
|
1100
|
+
};
|
|
1101
|
+
h["th_close"] = (state) => {
|
|
1102
|
+
state.closeNode();
|
|
1103
|
+
state.closeNode();
|
|
1104
|
+
};
|
|
1105
|
+
h["td_open"] = (state, tok) => {
|
|
1106
|
+
state.openNode(schemaArg.nodes.table_cell, { alignment: cellAlignment(tok) });
|
|
1107
|
+
state.openNode(schemaArg.nodes.paragraph, null);
|
|
1108
|
+
};
|
|
1109
|
+
h["td_close"] = (state) => {
|
|
1110
|
+
state.closeNode();
|
|
1111
|
+
state.closeNode();
|
|
1112
|
+
};
|
|
1113
|
+
const defaultLinkOpen = h["link_open"];
|
|
1114
|
+
const defaultLinkClose = h["link_close"];
|
|
1115
|
+
h["link_open"] = (state, tok, tokens, i) => {
|
|
1116
|
+
let hasContent = false;
|
|
1117
|
+
for (let j = i + 1; j < tokens.length; j++) {
|
|
1118
|
+
if (tokens[j].type === "link_close") break;
|
|
1119
|
+
if (tokens[j].type === "text" && tokens[j].content) {
|
|
1120
|
+
hasContent = true;
|
|
1121
|
+
break;
|
|
1122
|
+
}
|
|
1123
|
+
if (["image", "code_inline", "softbreak", "hardbreak", "html_inline"].includes(tokens[j].type)) {
|
|
1124
|
+
hasContent = true;
|
|
1125
|
+
break;
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
if (!hasContent) {
|
|
1129
|
+
let href = tok.attrGet("href") || "";
|
|
1130
|
+
href = href.replace(
|
|
1131
|
+
/%[C-F][0-9A-F](?:%[89AB][0-9A-F])+/gi,
|
|
1132
|
+
(m) => {
|
|
1133
|
+
try {
|
|
1134
|
+
return decodeURIComponent(m);
|
|
1135
|
+
} catch {
|
|
1136
|
+
return m;
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
);
|
|
1140
|
+
const title = tok.attrGet("title");
|
|
1141
|
+
let literal = `[](${href}`;
|
|
1142
|
+
if (title) literal += ` "${title}"`;
|
|
1143
|
+
literal += ")";
|
|
1144
|
+
state.addText(literal);
|
|
1145
|
+
for (let j = i + 1; j < tokens.length; j++) {
|
|
1146
|
+
if (tokens[j].type === "link_close") {
|
|
1147
|
+
tokens[j].meta = { ...tokens[j].meta || {}, skipClose: true };
|
|
1148
|
+
break;
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
return;
|
|
1152
|
+
}
|
|
1153
|
+
defaultLinkOpen(state, tok, tokens, i);
|
|
1154
|
+
};
|
|
1155
|
+
h["link_close"] = (state, tok, tokens, i) => {
|
|
1156
|
+
if (tok.meta?.skipClose) return;
|
|
1157
|
+
defaultLinkClose(state, tok, tokens, i);
|
|
1158
|
+
};
|
|
1159
|
+
const defaultTextHandler = h["text"];
|
|
1160
|
+
h["text"] = (state, tok, toks, ii) => {
|
|
1161
|
+
if (tok.meta?.mediaSkip) return;
|
|
1162
|
+
defaultTextHandler(state, tok, toks, ii);
|
|
1163
|
+
};
|
|
1164
|
+
h["html_inline"] = (state, tok, tokens, i) => {
|
|
1165
|
+
if (tok.meta?.mediaSkip) return;
|
|
1166
|
+
const content = tok.content;
|
|
1167
|
+
const mediaMatch = content.match(/^<(audio|video)\b/i);
|
|
1168
|
+
if (mediaMatch && mediaMatch[1]) {
|
|
1169
|
+
const tagName = mediaMatch[1].toLowerCase();
|
|
1170
|
+
const closeRe = new RegExp(`^</${tagName}\\s*>$`, "i");
|
|
1171
|
+
let fullHtml = content;
|
|
1172
|
+
for (let j = i + 1; j < tokens.length; j++) {
|
|
1173
|
+
const t = tokens[j];
|
|
1174
|
+
if (t.type === "html_inline" && closeRe.test(t.content.trim())) {
|
|
1175
|
+
fullHtml += t.content;
|
|
1176
|
+
t.meta = { ...t.meta || {}, mediaSkip: true };
|
|
1177
|
+
break;
|
|
1178
|
+
}
|
|
1179
|
+
if (t.content) fullHtml += t.content;
|
|
1180
|
+
t.meta = { ...t.meta || {}, mediaSkip: true };
|
|
1181
|
+
}
|
|
1182
|
+
state.addNode(schemaArg.nodes.html_inline, { value: fullHtml });
|
|
1183
|
+
return;
|
|
1184
|
+
}
|
|
1185
|
+
if (tok.meta?.htmlPaired) {
|
|
1186
|
+
const htmlMark = schemaArg.marks.html_mark;
|
|
1187
|
+
if (!htmlMark) {
|
|
1188
|
+
state.addNode(schemaArg.nodes.html_inline, { value: content });
|
|
1189
|
+
return;
|
|
1190
|
+
}
|
|
1191
|
+
if (!content.startsWith("</")) {
|
|
1192
|
+
const tagMatch = content.match(/^<([a-zA-Z][a-zA-Z0-9]*)/);
|
|
1193
|
+
const tagName = tagMatch && tagMatch[1] ? tagMatch[1].toLowerCase() : "";
|
|
1194
|
+
state.openMark(htmlMark.create({
|
|
1195
|
+
openTag: content,
|
|
1196
|
+
closeTag: `</${tagName}>`
|
|
1197
|
+
}));
|
|
1198
|
+
} else {
|
|
1199
|
+
state.closeMark(htmlMark);
|
|
1200
|
+
}
|
|
1201
|
+
return;
|
|
1202
|
+
}
|
|
1203
|
+
state.addNode(schemaArg.nodes.html_inline, { value: content });
|
|
1204
|
+
};
|
|
1205
|
+
const defaultHtmlBlock = h["html_block"];
|
|
1206
|
+
h["html_block"] = (state, tok, tokens, i) => {
|
|
1207
|
+
const content = tok.content.trim();
|
|
1208
|
+
if (/^<img\s/i.test(content)) {
|
|
1209
|
+
const imgPattern = /<img\s[^>]*\/?>/gi;
|
|
1210
|
+
const imgs = content.match(imgPattern);
|
|
1211
|
+
state.openNode(schemaArg.nodes.paragraph, null);
|
|
1212
|
+
if (imgs && imgs.length > 0) {
|
|
1213
|
+
for (let j = 0; j < imgs.length; j++) {
|
|
1214
|
+
if (j > 0) {
|
|
1215
|
+
state.addNode(schemaArg.nodes.hardbreak, { isInline: true });
|
|
1216
|
+
}
|
|
1217
|
+
state.addNode(schemaArg.nodes.html_inline, { value: imgs[j] });
|
|
1218
|
+
}
|
|
1219
|
+
} else {
|
|
1220
|
+
state.addNode(schemaArg.nodes.html_inline, { value: content });
|
|
1221
|
+
}
|
|
1222
|
+
state.closeNode();
|
|
1223
|
+
} else if (/^<(video|audio)\b/i.test(content)) {
|
|
1224
|
+
state.openNode(schemaArg.nodes.paragraph, null);
|
|
1225
|
+
state.addNode(schemaArg.nodes.html_inline, { value: content });
|
|
1226
|
+
state.closeNode();
|
|
1227
|
+
} else {
|
|
1228
|
+
defaultHtmlBlock(state, tok, tokens, i);
|
|
1229
|
+
}
|
|
1230
|
+
};
|
|
1231
|
+
}
|
|
1232
|
+
};
|
|
1233
|
+
var defaultParser = new MorayaMarkdownParser(defaultSchema);
|
|
1234
|
+
var parserCache = /* @__PURE__ */ new WeakMap();
|
|
1235
|
+
parserCache.set(defaultSchema, defaultParser);
|
|
1236
|
+
function getParserFor(schema) {
|
|
1237
|
+
if (!schema || schema === defaultSchema) return defaultParser;
|
|
1238
|
+
let p = parserCache.get(schema);
|
|
1239
|
+
if (!p) {
|
|
1240
|
+
p = new MorayaMarkdownParser(schema);
|
|
1241
|
+
parserCache.set(schema, p);
|
|
1242
|
+
}
|
|
1243
|
+
return p;
|
|
1244
|
+
}
|
|
1245
|
+
var serializer = new MarkdownSerializer(
|
|
1246
|
+
{
|
|
1247
|
+
// ── Block nodes ──
|
|
1248
|
+
doc(state, node) {
|
|
1249
|
+
state.renderContent(node);
|
|
1250
|
+
},
|
|
1251
|
+
paragraph(state, node) {
|
|
1252
|
+
if (node.content.size === 0) {
|
|
1253
|
+
state.write("");
|
|
1254
|
+
} else {
|
|
1255
|
+
state.renderInline(node);
|
|
1256
|
+
}
|
|
1257
|
+
state.closeBlock(node);
|
|
1258
|
+
},
|
|
1259
|
+
heading(state, node) {
|
|
1260
|
+
state.write(`${"#".repeat(node.attrs.level)} `);
|
|
1261
|
+
state.renderInline(node, false);
|
|
1262
|
+
state.closeBlock(node);
|
|
1263
|
+
},
|
|
1264
|
+
blockquote(state, node) {
|
|
1265
|
+
state.wrapBlock("> ", null, node, () => state.renderContent(node));
|
|
1266
|
+
},
|
|
1267
|
+
code_block(state, node) {
|
|
1268
|
+
const lang = node.attrs.language || "";
|
|
1269
|
+
const fenceLang = lang === "text" ? "" : lang;
|
|
1270
|
+
state.write(`\`\`\`${fenceLang}
|
|
1271
|
+
`);
|
|
1272
|
+
state.text(node.textContent, false);
|
|
1273
|
+
state.ensureNewLine();
|
|
1274
|
+
state.write("```");
|
|
1275
|
+
state.closeBlock(node);
|
|
1276
|
+
},
|
|
1277
|
+
horizontal_rule(state, node) {
|
|
1278
|
+
state.write("---");
|
|
1279
|
+
state.closeBlock(node);
|
|
1280
|
+
},
|
|
1281
|
+
bullet_list(state, node) {
|
|
1282
|
+
state.renderList(node, " ", () => "- ");
|
|
1283
|
+
},
|
|
1284
|
+
ordered_list(state, node) {
|
|
1285
|
+
const start = node.attrs.order || 1;
|
|
1286
|
+
state.renderList(node, " ", (i) => `${start + i}. `);
|
|
1287
|
+
},
|
|
1288
|
+
list_item(state, node) {
|
|
1289
|
+
if (node.attrs.checked != null) {
|
|
1290
|
+
const checkbox = node.attrs.checked ? "[x] " : "[ ] ";
|
|
1291
|
+
state.write(checkbox);
|
|
1292
|
+
}
|
|
1293
|
+
state.renderContent(node);
|
|
1294
|
+
},
|
|
1295
|
+
image(state, node) {
|
|
1296
|
+
const alt = state.esc(node.attrs.alt || "", false);
|
|
1297
|
+
const src = node.attrs.src || "";
|
|
1298
|
+
const title = node.attrs.title;
|
|
1299
|
+
if (title) {
|
|
1300
|
+
state.write(`}")`);
|
|
1301
|
+
} else {
|
|
1302
|
+
state.write(``);
|
|
1303
|
+
}
|
|
1304
|
+
},
|
|
1305
|
+
hardbreak(state) {
|
|
1306
|
+
state.write(" \n");
|
|
1307
|
+
},
|
|
1308
|
+
html_block(state, node) {
|
|
1309
|
+
state.text(node.textContent, false);
|
|
1310
|
+
state.closeBlock(node);
|
|
1311
|
+
},
|
|
1312
|
+
html_inline(state, node) {
|
|
1313
|
+
state.text(node.attrs.value, false);
|
|
1314
|
+
},
|
|
1315
|
+
// ── Table nodes ──
|
|
1316
|
+
table(state, node) {
|
|
1317
|
+
const alignments = [];
|
|
1318
|
+
const headerRow = node.child(0);
|
|
1319
|
+
headerRow.forEach((cell) => {
|
|
1320
|
+
alignments.push(cell.attrs.alignment || "left");
|
|
1321
|
+
});
|
|
1322
|
+
renderTableRow(state, headerRow);
|
|
1323
|
+
const sep = alignments.map((a) => {
|
|
1324
|
+
switch (a) {
|
|
1325
|
+
case "center":
|
|
1326
|
+
return ":---:";
|
|
1327
|
+
case "right":
|
|
1328
|
+
return "---:";
|
|
1329
|
+
default:
|
|
1330
|
+
return "---";
|
|
1331
|
+
}
|
|
1332
|
+
});
|
|
1333
|
+
state.write(`| ${sep.join(" | ")} |`);
|
|
1334
|
+
state.ensureNewLine();
|
|
1335
|
+
for (let i = 1; i < node.childCount; i++) {
|
|
1336
|
+
renderTableRow(state, node.child(i));
|
|
1337
|
+
}
|
|
1338
|
+
state.closeBlock(node);
|
|
1339
|
+
},
|
|
1340
|
+
table_header_row() {
|
|
1341
|
+
},
|
|
1342
|
+
table_row() {
|
|
1343
|
+
},
|
|
1344
|
+
table_header(state, node) {
|
|
1345
|
+
state.renderInline(node.firstChild);
|
|
1346
|
+
},
|
|
1347
|
+
table_cell(state, node) {
|
|
1348
|
+
state.renderInline(node.firstChild);
|
|
1349
|
+
},
|
|
1350
|
+
// ── Math nodes ──
|
|
1351
|
+
math_inline(state, node) {
|
|
1352
|
+
state.write(`$${node.textContent}$`);
|
|
1353
|
+
},
|
|
1354
|
+
math_block(state, node) {
|
|
1355
|
+
state.write("$$\n");
|
|
1356
|
+
state.text(node.attrs.value || node.textContent, false);
|
|
1357
|
+
state.ensureNewLine();
|
|
1358
|
+
state.write("$$");
|
|
1359
|
+
state.closeBlock(node);
|
|
1360
|
+
},
|
|
1361
|
+
// ── Definition list nodes ──
|
|
1362
|
+
defList(state, node) {
|
|
1363
|
+
state.renderContent(node);
|
|
1364
|
+
},
|
|
1365
|
+
defListTerm(state, node) {
|
|
1366
|
+
state.renderInline(node);
|
|
1367
|
+
state.closeBlock(node);
|
|
1368
|
+
},
|
|
1369
|
+
defListDescription(state, node) {
|
|
1370
|
+
state.write(": ");
|
|
1371
|
+
state.renderContent(node);
|
|
1372
|
+
},
|
|
1373
|
+
// ── Fallback for text node (shouldn't be needed but safe) ──
|
|
1374
|
+
text(state, node) {
|
|
1375
|
+
state.text(node.text || "");
|
|
1376
|
+
}
|
|
1377
|
+
},
|
|
1378
|
+
{
|
|
1379
|
+
// ── Mark serializers ──
|
|
1380
|
+
strong: {
|
|
1381
|
+
open: "**",
|
|
1382
|
+
close: "**",
|
|
1383
|
+
mixable: true,
|
|
1384
|
+
expelEnclosingWhitespace: true
|
|
1385
|
+
},
|
|
1386
|
+
em: {
|
|
1387
|
+
open: "*",
|
|
1388
|
+
close: "*",
|
|
1389
|
+
mixable: true,
|
|
1390
|
+
expelEnclosingWhitespace: true
|
|
1391
|
+
},
|
|
1392
|
+
code: {
|
|
1393
|
+
open(_state, mark, parent, index) {
|
|
1394
|
+
return isPlainURL(mark, parent, index, 1) ? "" : "`";
|
|
1395
|
+
},
|
|
1396
|
+
close(_state, mark, parent, index) {
|
|
1397
|
+
return isPlainURL(mark, parent, index, -1) ? "" : "`";
|
|
1398
|
+
},
|
|
1399
|
+
escape: false
|
|
1400
|
+
},
|
|
1401
|
+
link: {
|
|
1402
|
+
open(_state, mark, parent, index) {
|
|
1403
|
+
return isPlainURL(mark, parent, index, 1) ? "<" : "[";
|
|
1404
|
+
},
|
|
1405
|
+
close(state, mark, parent, index) {
|
|
1406
|
+
const href = mark.attrs.href;
|
|
1407
|
+
const title = mark.attrs.title;
|
|
1408
|
+
if (isPlainURL(mark, parent, index, -1)) {
|
|
1409
|
+
return ">";
|
|
1410
|
+
}
|
|
1411
|
+
return title ? `](${href} "${state.esc(title, false)}")` : `](${href})`;
|
|
1412
|
+
},
|
|
1413
|
+
mixable: false
|
|
1414
|
+
},
|
|
1415
|
+
strike_through: {
|
|
1416
|
+
open: "~~",
|
|
1417
|
+
close: "~~",
|
|
1418
|
+
mixable: true,
|
|
1419
|
+
expelEnclosingWhitespace: true
|
|
1420
|
+
},
|
|
1421
|
+
html_mark: {
|
|
1422
|
+
open(_state, mark) {
|
|
1423
|
+
return mark.attrs.openTag;
|
|
1424
|
+
},
|
|
1425
|
+
close(_state, mark) {
|
|
1426
|
+
return mark.attrs.closeTag;
|
|
1427
|
+
}
|
|
1428
|
+
}
|
|
1429
|
+
},
|
|
1430
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1431
|
+
{
|
|
1432
|
+
hardBreakNodeName: "hardbreak",
|
|
1433
|
+
strict: false
|
|
1434
|
+
}
|
|
1435
|
+
);
|
|
1436
|
+
function renderTableRow(state, row) {
|
|
1437
|
+
const cells = [];
|
|
1438
|
+
const s = state;
|
|
1439
|
+
row.forEach((cell) => {
|
|
1440
|
+
const parts = [];
|
|
1441
|
+
cell.forEach((para) => {
|
|
1442
|
+
if (para.type.name !== "paragraph") return;
|
|
1443
|
+
const savedOut = s.out;
|
|
1444
|
+
const savedClosed = s.closed;
|
|
1445
|
+
s.out = "";
|
|
1446
|
+
s.closed = null;
|
|
1447
|
+
state.renderInline(para);
|
|
1448
|
+
const piece = s.out.replace(/\n/g, " ").trim();
|
|
1449
|
+
s.out = savedOut;
|
|
1450
|
+
s.closed = savedClosed;
|
|
1451
|
+
parts.push(piece);
|
|
1452
|
+
});
|
|
1453
|
+
cells.push(parts.join(" "));
|
|
1454
|
+
});
|
|
1455
|
+
state.write(`| ${cells.join(" | ")} |`);
|
|
1456
|
+
state.ensureNewLine();
|
|
1457
|
+
}
|
|
1458
|
+
function isPlainURL(mark, parent, index, side) {
|
|
1459
|
+
if (mark.attrs.title || !/^\w+:/.test(mark.attrs.href)) return false;
|
|
1460
|
+
const content = parent.child(index + (side < 0 ? -1 : 0));
|
|
1461
|
+
if (!content.isText || content.text !== mark.attrs.href || content.marks[content.marks.length - 1] !== mark) {
|
|
1462
|
+
return false;
|
|
1463
|
+
}
|
|
1464
|
+
if (index === (side < 0 ? 1 : parent.childCount - 1)) return true;
|
|
1465
|
+
const next = parent.child(index + (side < 0 ? -2 : 1));
|
|
1466
|
+
return !mark.isInSet(next.marks);
|
|
1467
|
+
}
|
|
1468
|
+
function normalizeMathBlocks(text2) {
|
|
1469
|
+
if (!text2.includes("$$")) return text2;
|
|
1470
|
+
const lines = text2.split("\n");
|
|
1471
|
+
const result = [];
|
|
1472
|
+
let inFence = false;
|
|
1473
|
+
let inMathBlock = false;
|
|
1474
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1475
|
+
const line = lines[i] ?? "";
|
|
1476
|
+
const trimmed = line.trim();
|
|
1477
|
+
if (!inMathBlock && /^(`{3,}|~{3,})/.test(trimmed)) {
|
|
1478
|
+
inFence = !inFence;
|
|
1479
|
+
result.push(line);
|
|
1480
|
+
continue;
|
|
1481
|
+
}
|
|
1482
|
+
if (inFence) {
|
|
1483
|
+
result.push(line);
|
|
1484
|
+
continue;
|
|
1485
|
+
}
|
|
1486
|
+
if (trimmed === "$$") {
|
|
1487
|
+
if (!inMathBlock) {
|
|
1488
|
+
const last = result[result.length - 1];
|
|
1489
|
+
if (result.length > 0 && last !== void 0 && last.trim() !== "") {
|
|
1490
|
+
result.push("");
|
|
1491
|
+
}
|
|
1492
|
+
result.push(line);
|
|
1493
|
+
inMathBlock = true;
|
|
1494
|
+
} else {
|
|
1495
|
+
result.push(line);
|
|
1496
|
+
inMathBlock = false;
|
|
1497
|
+
const next = lines[i + 1];
|
|
1498
|
+
if (next !== void 0 && next.trim() !== "") {
|
|
1499
|
+
result.push("");
|
|
1500
|
+
}
|
|
1501
|
+
}
|
|
1502
|
+
} else {
|
|
1503
|
+
result.push(line);
|
|
1504
|
+
}
|
|
1505
|
+
}
|
|
1506
|
+
return result.join("\n");
|
|
1507
|
+
}
|
|
1508
|
+
function normalizeSmartQuotes(text2) {
|
|
1509
|
+
if (!/[“”„‟‘’‚‛]/.test(text2)) return text2;
|
|
1510
|
+
return text2.replace(
|
|
1511
|
+
/(\]\([^\n)]*\s)“([^”\n]*)”(\s*\))/g,
|
|
1512
|
+
(_m, pre, title, post) => `${pre}"${title}"${post}`
|
|
1513
|
+
).replace(
|
|
1514
|
+
/(\]\([^\n)]*\s)“([^”\n]*)”(\s*\))/g,
|
|
1515
|
+
(_m, pre, title, post) => `${pre}"${title}"${post}`
|
|
1516
|
+
).replace(
|
|
1517
|
+
/(\]\([^\n)]*\s)‘([^’\n]*)’(\s*\))/g,
|
|
1518
|
+
(_m, pre, title, post) => `${pre}'${title}'${post}`
|
|
1519
|
+
);
|
|
1520
|
+
}
|
|
1521
|
+
function parseMarkdown(markdown, schemaArg) {
|
|
1522
|
+
const p = getParserFor(schemaArg);
|
|
1523
|
+
try {
|
|
1524
|
+
return p.parse(normalizeSmartQuotes(normalizeMathBlocks(markdown)));
|
|
1525
|
+
} catch (err) {
|
|
1526
|
+
if (typeof console !== "undefined" && console.warn) {
|
|
1527
|
+
console.warn("[parseMarkdown] best-effort fallback for malformed input:", err);
|
|
1528
|
+
}
|
|
1529
|
+
return p.schema.topNodeType.createAndFill();
|
|
1530
|
+
}
|
|
1531
|
+
}
|
|
1532
|
+
|
|
1533
|
+
// src/plugins/editor-props-plugin.ts
|
|
1534
|
+
var editorPropsKey = new PluginKey("moraya-editor-props");
|
|
1535
|
+
function isLocalFilePath(href) {
|
|
1536
|
+
if (href.startsWith("/")) return true;
|
|
1537
|
+
if (href.startsWith("./") || href.startsWith("../")) return true;
|
|
1538
|
+
if (/^[A-Za-z]:[/\\]/.test(href)) return true;
|
|
1539
|
+
if (href.startsWith("file://")) return true;
|
|
1540
|
+
return false;
|
|
1541
|
+
}
|
|
1542
|
+
function resolveLocalPath(href, platform) {
|
|
1543
|
+
let path = href;
|
|
1544
|
+
if (path.startsWith("file:///")) {
|
|
1545
|
+
path = path.slice(7);
|
|
1546
|
+
try {
|
|
1547
|
+
path = decodeURIComponent(path);
|
|
1548
|
+
} catch {
|
|
1549
|
+
}
|
|
1550
|
+
} else if (path.startsWith("file://")) {
|
|
1551
|
+
path = path.slice(5);
|
|
1552
|
+
try {
|
|
1553
|
+
path = decodeURIComponent(path);
|
|
1554
|
+
} catch {
|
|
1555
|
+
}
|
|
1556
|
+
}
|
|
1557
|
+
if (path.startsWith("/") || /^[A-Za-z]:[/\\]/.test(path)) return path;
|
|
1558
|
+
const currentFile = platform.getCurrentFilePath();
|
|
1559
|
+
if (currentFile) {
|
|
1560
|
+
const dir = currentFile.replace(/[/\\][^/\\]*$/, "");
|
|
1561
|
+
return dir + "/" + path;
|
|
1562
|
+
}
|
|
1563
|
+
return path;
|
|
1564
|
+
}
|
|
1565
|
+
function createEditorPropsPlugin(opts) {
|
|
1566
|
+
const { platform, linkOpener } = opts;
|
|
1567
|
+
const isMacOS = platform.isMacOS;
|
|
1568
|
+
let pendingPaste = false;
|
|
1569
|
+
return new Plugin({
|
|
1570
|
+
key: editorPropsKey,
|
|
1571
|
+
props: {
|
|
1572
|
+
/**
|
|
1573
|
+
* Parse pasted plain text as Markdown so syntax renders instead of
|
|
1574
|
+
* being inserted as escaped literal text.
|
|
1575
|
+
*/
|
|
1576
|
+
clipboardTextParser(text2, $context, plain) {
|
|
1577
|
+
if (plain || $context.parent.type.spec.code) return void 0;
|
|
1578
|
+
const doc2 = parseMarkdown(text2);
|
|
1579
|
+
if (doc2.textContent.length === 0 && doc2.content.size <= 2) return void 0;
|
|
1580
|
+
const content = doc2.content;
|
|
1581
|
+
if (content.childCount === 1 && content.firstChild.type.name === "paragraph") {
|
|
1582
|
+
return new Slice(content.firstChild.content, 0, 0);
|
|
1583
|
+
}
|
|
1584
|
+
return new Slice(content, 0, 0);
|
|
1585
|
+
},
|
|
1586
|
+
/**
|
|
1587
|
+
* Safety net for degenerate pastes (empty markdown link, empty <a>, etc.).
|
|
1588
|
+
* Also routes pasted markdown image syntax through the markdown parser.
|
|
1589
|
+
*/
|
|
1590
|
+
handlePaste(view, event, slice) {
|
|
1591
|
+
const plain = event.clipboardData?.getData("text/plain");
|
|
1592
|
+
if (!plain) return false;
|
|
1593
|
+
const trimmed = plain.trim();
|
|
1594
|
+
if (/^!\[/.test(trimmed)) {
|
|
1595
|
+
const doc2 = parseMarkdown(trimmed);
|
|
1596
|
+
if (doc2.content.size > 2) {
|
|
1597
|
+
const content = doc2.content;
|
|
1598
|
+
const inner = content.childCount === 1 && content.firstChild.type.name === "paragraph" ? content.firstChild.content : content;
|
|
1599
|
+
view.dispatch(
|
|
1600
|
+
view.state.tr.replaceSelection(new Slice(inner, 0, 0))
|
|
1601
|
+
);
|
|
1602
|
+
pendingPaste = true;
|
|
1603
|
+
return true;
|
|
1604
|
+
}
|
|
1605
|
+
}
|
|
1606
|
+
const linkMatch = /^\[([^\]]*)\]\(([^)]*)\)$/.exec(trimmed);
|
|
1607
|
+
if (linkMatch && (!linkMatch[1] || !linkMatch[2])) {
|
|
1608
|
+
const textNode = view.state.schema.text(plain);
|
|
1609
|
+
view.dispatch(
|
|
1610
|
+
view.state.tr.replaceSelection(new Slice(Fragment2.from(textNode), 0, 0))
|
|
1611
|
+
);
|
|
1612
|
+
pendingPaste = true;
|
|
1613
|
+
return true;
|
|
1614
|
+
}
|
|
1615
|
+
try {
|
|
1616
|
+
const sliceText = slice.content.textBetween(0, slice.content.size, "", "");
|
|
1617
|
+
if (sliceText.trim().length === 0 && trimmed.length > 0) {
|
|
1618
|
+
const textNode = view.state.schema.text(plain);
|
|
1619
|
+
view.dispatch(
|
|
1620
|
+
view.state.tr.replaceSelection(new Slice(Fragment2.from(textNode), 0, 0))
|
|
1621
|
+
);
|
|
1622
|
+
pendingPaste = true;
|
|
1623
|
+
return true;
|
|
1624
|
+
}
|
|
1625
|
+
} catch {
|
|
1626
|
+
}
|
|
1627
|
+
return false;
|
|
1628
|
+
},
|
|
1629
|
+
/**
|
|
1630
|
+
* Paste language normalization:
|
|
1631
|
+
* Copy class="language-xxx" from <code> to data-language on parent <pre>.
|
|
1632
|
+
*/
|
|
1633
|
+
transformPastedHTML(html) {
|
|
1634
|
+
if (!html.includes("language-")) return html;
|
|
1635
|
+
try {
|
|
1636
|
+
const template = document.createElement("template");
|
|
1637
|
+
template.innerHTML = html;
|
|
1638
|
+
const fragment = template.content;
|
|
1639
|
+
for (const pre of fragment.querySelectorAll("pre")) {
|
|
1640
|
+
if (pre.dataset.language) continue;
|
|
1641
|
+
const code2 = pre.querySelector("code");
|
|
1642
|
+
if (!code2) continue;
|
|
1643
|
+
const match = code2.className.match(/(?:language|lang)-(\S+)/);
|
|
1644
|
+
if (match && match[1]) {
|
|
1645
|
+
pre.dataset.language = match[1];
|
|
1646
|
+
}
|
|
1647
|
+
}
|
|
1648
|
+
return template.innerHTML;
|
|
1649
|
+
} catch {
|
|
1650
|
+
return html;
|
|
1651
|
+
}
|
|
1652
|
+
},
|
|
1653
|
+
handleDOMEvents: {
|
|
1654
|
+
/**
|
|
1655
|
+
* Safety: prevent WebView navigation on any remaining <a> clicks.
|
|
1656
|
+
* (Most <a> tags get expanded to literal text on mousedown, but this
|
|
1657
|
+
* is a fallback in case the click fires before the expand.)
|
|
1658
|
+
*/
|
|
1659
|
+
click(_view, event) {
|
|
1660
|
+
const me = event;
|
|
1661
|
+
const target = me.target;
|
|
1662
|
+
if (!target) return false;
|
|
1663
|
+
const anchor = target.closest("a[href]");
|
|
1664
|
+
if (anchor) {
|
|
1665
|
+
me.preventDefault();
|
|
1666
|
+
}
|
|
1667
|
+
return false;
|
|
1668
|
+
},
|
|
1669
|
+
mousedown(view, event) {
|
|
1670
|
+
const me = event;
|
|
1671
|
+
if (me.button !== 0) return false;
|
|
1672
|
+
const target = me.target;
|
|
1673
|
+
if (!target) return false;
|
|
1674
|
+
if (me.metaKey || me.ctrlKey) {
|
|
1675
|
+
const anchor = target.closest("a[href]");
|
|
1676
|
+
if (anchor) {
|
|
1677
|
+
const href = anchor.getAttribute("href");
|
|
1678
|
+
if (href) {
|
|
1679
|
+
me.preventDefault();
|
|
1680
|
+
const targetHref = isLocalFilePath(href) ? resolveLocalPath(href, platform) : href;
|
|
1681
|
+
try {
|
|
1682
|
+
linkOpener.open(targetHref);
|
|
1683
|
+
} catch (e) {
|
|
1684
|
+
console.warn("[opener] failed:", targetHref, e);
|
|
1685
|
+
}
|
|
1686
|
+
return true;
|
|
1687
|
+
}
|
|
1688
|
+
}
|
|
1689
|
+
}
|
|
1690
|
+
const mathBlock = target.closest('div[data-type="math_block"]');
|
|
1691
|
+
if (!mathBlock) return false;
|
|
1692
|
+
me.preventDefault();
|
|
1693
|
+
try {
|
|
1694
|
+
const pos = view.posAtDOM(mathBlock, 0);
|
|
1695
|
+
const $pos = view.state.doc.resolve(pos);
|
|
1696
|
+
let beforePos = pos;
|
|
1697
|
+
for (let d = $pos.depth; d > 0; d--) {
|
|
1698
|
+
if ($pos.node(d).type.name === "math_block") {
|
|
1699
|
+
beforePos = $pos.before(d);
|
|
1700
|
+
break;
|
|
1701
|
+
}
|
|
1702
|
+
}
|
|
1703
|
+
const $before = view.state.doc.resolve(beforePos);
|
|
1704
|
+
if (!$before.nodeAfter || $before.nodeAfter.type.name !== "math_block") {
|
|
1705
|
+
if ($pos.nodeAfter?.type.name === "math_block") {
|
|
1706
|
+
beforePos = pos;
|
|
1707
|
+
}
|
|
1708
|
+
}
|
|
1709
|
+
const sel = TextSelection.near(view.state.doc.resolve(beforePos), -1);
|
|
1710
|
+
view.dispatch(view.state.tr.setSelection(sel));
|
|
1711
|
+
} catch {
|
|
1712
|
+
}
|
|
1713
|
+
view.focus();
|
|
1714
|
+
return true;
|
|
1715
|
+
},
|
|
1716
|
+
/**
|
|
1717
|
+
* Cmd/Ctrl held → add 'link-hover' class for pointer cursor on links.
|
|
1718
|
+
* Also handles fast AllSelection delete + WKWebView end-of-textblock
|
|
1719
|
+
* Backspace fix at the highest priority interception point.
|
|
1720
|
+
*/
|
|
1721
|
+
keydown(view, event) {
|
|
1722
|
+
if (event.isComposing) return false;
|
|
1723
|
+
if (event.key === "Meta" || event.key === "Control") {
|
|
1724
|
+
view.dom.classList.add("link-hover");
|
|
1725
|
+
}
|
|
1726
|
+
if ((event.key === "Backspace" || event.key === "Delete") && !event.metaKey && !event.ctrlKey && !event.altKey && !event.shiftKey) {
|
|
1727
|
+
try {
|
|
1728
|
+
;
|
|
1729
|
+
view.domObserver?.flush?.();
|
|
1730
|
+
} catch {
|
|
1731
|
+
}
|
|
1732
|
+
const docSize = view.state.doc.content.size;
|
|
1733
|
+
let isAllSelected = false;
|
|
1734
|
+
const sel = view.state.selection;
|
|
1735
|
+
if (sel instanceof AllSelection || docSize > 0 && sel.from <= 1 && sel.to >= docSize - 1) {
|
|
1736
|
+
isAllSelected = true;
|
|
1737
|
+
}
|
|
1738
|
+
if (!isAllSelected && docSize > 0) {
|
|
1739
|
+
try {
|
|
1740
|
+
const domSel = window.getSelection();
|
|
1741
|
+
if (domSel && !domSel.isCollapsed && domSel.rangeCount > 0) {
|
|
1742
|
+
const range = domSel.getRangeAt(0);
|
|
1743
|
+
const editorRange = document.createRange();
|
|
1744
|
+
editorRange.selectNodeContents(view.dom);
|
|
1745
|
+
if (range.compareBoundaryPoints(Range.START_TO_START, editorRange) <= 0 && range.compareBoundaryPoints(Range.END_TO_END, editorRange) >= 0) {
|
|
1746
|
+
isAllSelected = true;
|
|
1747
|
+
}
|
|
1748
|
+
}
|
|
1749
|
+
} catch {
|
|
1750
|
+
}
|
|
1751
|
+
}
|
|
1752
|
+
if (!isAllSelected && docSize > 0) {
|
|
1753
|
+
try {
|
|
1754
|
+
const domSel = window.getSelection();
|
|
1755
|
+
if (domSel && !domSel.isCollapsed) {
|
|
1756
|
+
const selectedText = domSel.toString();
|
|
1757
|
+
const fullText = view.dom.textContent || "";
|
|
1758
|
+
if (selectedText.length > 0 && fullText.length > 0 && selectedText.length >= fullText.length * 0.9) {
|
|
1759
|
+
isAllSelected = true;
|
|
1760
|
+
}
|
|
1761
|
+
}
|
|
1762
|
+
} catch {
|
|
1763
|
+
}
|
|
1764
|
+
}
|
|
1765
|
+
if (isAllSelected) {
|
|
1766
|
+
event.preventDefault();
|
|
1767
|
+
const paragraphType = view.state.schema.nodes.paragraph;
|
|
1768
|
+
if (!paragraphType) return false;
|
|
1769
|
+
const emptyParagraph = paragraphType.create();
|
|
1770
|
+
const tr = view.state.tr.replaceWith(0, docSize, emptyParagraph);
|
|
1771
|
+
tr.setSelection(TextSelection.create(tr.doc, 1));
|
|
1772
|
+
tr.setMeta("full-delete", true);
|
|
1773
|
+
view.dispatch(tr);
|
|
1774
|
+
return true;
|
|
1775
|
+
}
|
|
1776
|
+
if (event.key === "Backspace") {
|
|
1777
|
+
if (sel instanceof TextSelection && sel.empty && sel.$cursor) {
|
|
1778
|
+
const { parent, parentOffset } = sel.$cursor;
|
|
1779
|
+
if (parent.isTextblock && parentOffset === parent.content.size && parentOffset > 0) {
|
|
1780
|
+
const nb = sel.$cursor.nodeBefore;
|
|
1781
|
+
if (nb) {
|
|
1782
|
+
event.preventDefault();
|
|
1783
|
+
if (nb.isText && nb.text) {
|
|
1784
|
+
const code2 = nb.text.charCodeAt(nb.text.length - 1);
|
|
1785
|
+
const delLen = code2 >= 56320 && code2 <= 57343 ? 2 : 1;
|
|
1786
|
+
view.dispatch(view.state.tr.delete(sel.from - delLen, sel.from).scrollIntoView());
|
|
1787
|
+
} else {
|
|
1788
|
+
view.dispatch(view.state.tr.delete(sel.from - nb.nodeSize, sel.from).scrollIntoView());
|
|
1789
|
+
}
|
|
1790
|
+
return true;
|
|
1791
|
+
}
|
|
1792
|
+
}
|
|
1793
|
+
}
|
|
1794
|
+
}
|
|
1795
|
+
}
|
|
1796
|
+
return false;
|
|
1797
|
+
},
|
|
1798
|
+
keyup(view, event) {
|
|
1799
|
+
if (event.key === "Meta" || event.key === "Control") {
|
|
1800
|
+
view.dom.classList.remove("link-hover");
|
|
1801
|
+
}
|
|
1802
|
+
return false;
|
|
1803
|
+
}
|
|
1804
|
+
},
|
|
1805
|
+
/**
|
|
1806
|
+
* Click below content: append a paragraph and place cursor there when
|
|
1807
|
+
* the last node is a code_block / table / etc. and user clicks below it.
|
|
1808
|
+
*/
|
|
1809
|
+
handleClick(view, _pos, event) {
|
|
1810
|
+
if (event.button !== 0) return false;
|
|
1811
|
+
const { doc: doc2 } = view.state;
|
|
1812
|
+
const lastNode = doc2.lastChild;
|
|
1813
|
+
if (!lastNode || lastNode.type.name === "paragraph") return false;
|
|
1814
|
+
const lastNodePos = doc2.content.size - lastNode.nodeSize;
|
|
1815
|
+
const lastDOM = view.nodeDOM(lastNodePos);
|
|
1816
|
+
if (!lastDOM) return false;
|
|
1817
|
+
const rect = lastDOM.getBoundingClientRect();
|
|
1818
|
+
if (event.clientY <= rect.bottom) return false;
|
|
1819
|
+
const paragraphType = view.state.schema.nodes.paragraph;
|
|
1820
|
+
if (!paragraphType) return false;
|
|
1821
|
+
const endPos = doc2.content.size;
|
|
1822
|
+
const paragraph2 = paragraphType.create();
|
|
1823
|
+
const tr = view.state.tr.insert(endPos, paragraph2);
|
|
1824
|
+
tr.setSelection(TextSelection.create(tr.doc, endPos + 1));
|
|
1825
|
+
view.dispatch(tr);
|
|
1826
|
+
view.focus();
|
|
1827
|
+
return true;
|
|
1828
|
+
},
|
|
1829
|
+
/**
|
|
1830
|
+
* Image click: prevent NodeSelection blue highlight, place TextSelection
|
|
1831
|
+
* after the image instead. (math_block is handled in mousedown above.)
|
|
1832
|
+
*/
|
|
1833
|
+
handleClickOn(view, _pos, node, nodePos, event) {
|
|
1834
|
+
if (node.type.name !== "image") return false;
|
|
1835
|
+
if (event.button !== 0) return false;
|
|
1836
|
+
const $pos = view.state.doc.resolve(nodePos + node.nodeSize);
|
|
1837
|
+
const sel = TextSelection.near($pos);
|
|
1838
|
+
view.dispatch(view.state.tr.setSelection(sel));
|
|
1839
|
+
return true;
|
|
1840
|
+
},
|
|
1841
|
+
/**
|
|
1842
|
+
* Keyboard shortcuts (after keymap plugins):
|
|
1843
|
+
* - ArrowRight: escape formatting mark boundary
|
|
1844
|
+
* - Backspace/Delete on AllSelection: fast full-doc deletion
|
|
1845
|
+
*/
|
|
1846
|
+
handleKeyDown(view, event) {
|
|
1847
|
+
if (event.isComposing) return false;
|
|
1848
|
+
if (event.key === "ArrowRight" && !event.shiftKey && !event.metaKey && !event.ctrlKey && !event.altKey) {
|
|
1849
|
+
const sel = view.state.selection;
|
|
1850
|
+
if (sel.empty && sel instanceof TextSelection && sel.$cursor) {
|
|
1851
|
+
const $cursor = sel.$cursor;
|
|
1852
|
+
const ZWSP_MARK_NAMES = ["code", "strong", "em", "strike_through"];
|
|
1853
|
+
const nodeBefore = $cursor.nodeBefore;
|
|
1854
|
+
const nodeAfter = $cursor.nodeAfter;
|
|
1855
|
+
const hasTargetMarkBefore = nodeBefore != null && ZWSP_MARK_NAMES.some((name) => {
|
|
1856
|
+
const mt = view.state.schema.marks[name];
|
|
1857
|
+
return mt && nodeBefore.marks.some((m) => m.type === mt);
|
|
1858
|
+
});
|
|
1859
|
+
if (hasTargetMarkBefore && nodeAfter?.isText && nodeAfter.text?.startsWith("\u200B")) {
|
|
1860
|
+
const nextPos = $cursor.pos + nodeAfter.nodeSize;
|
|
1861
|
+
const $next = view.state.doc.resolve(Math.min(nextPos, view.state.doc.content.size));
|
|
1862
|
+
const nextSel = TextSelection.near($next, 1);
|
|
1863
|
+
const tr = view.state.tr.setSelection(nextSel);
|
|
1864
|
+
tr.setStoredMarks([]);
|
|
1865
|
+
tr.setMeta("code-escape", true);
|
|
1866
|
+
tr.scrollIntoView();
|
|
1867
|
+
view.dispatch(tr);
|
|
1868
|
+
return true;
|
|
1869
|
+
}
|
|
1870
|
+
}
|
|
1871
|
+
}
|
|
1872
|
+
if (event.key === "Backspace" || event.key === "Delete") {
|
|
1873
|
+
const sel = view.state.selection;
|
|
1874
|
+
const docSize = view.state.doc.content.size;
|
|
1875
|
+
const isAllSelected = sel instanceof AllSelection || docSize > 0 && sel.from <= 1 && sel.to >= docSize - 1;
|
|
1876
|
+
if (isAllSelected) {
|
|
1877
|
+
event.preventDefault();
|
|
1878
|
+
const paragraphType = view.state.schema.nodes.paragraph;
|
|
1879
|
+
if (!paragraphType) return false;
|
|
1880
|
+
const emptyParagraph = paragraphType.create();
|
|
1881
|
+
const tr = view.state.tr.replaceWith(0, docSize, emptyParagraph);
|
|
1882
|
+
tr.setSelection(TextSelection.create(tr.doc, 1));
|
|
1883
|
+
tr.setMeta("full-delete", true);
|
|
1884
|
+
view.dispatch(tr);
|
|
1885
|
+
return true;
|
|
1886
|
+
}
|
|
1887
|
+
}
|
|
1888
|
+
return false;
|
|
1889
|
+
},
|
|
1890
|
+
/**
|
|
1891
|
+
* WKWebView caret fix: add 'caret-empty-para' decoration to empty
|
|
1892
|
+
* paragraph under cursor on macOS.
|
|
1893
|
+
*/
|
|
1894
|
+
decorations(state) {
|
|
1895
|
+
if (!isMacOS) return DecorationSet.empty;
|
|
1896
|
+
const { selection } = state;
|
|
1897
|
+
if (!selection.empty) return DecorationSet.empty;
|
|
1898
|
+
const { $from } = selection;
|
|
1899
|
+
const parent = $from.parent;
|
|
1900
|
+
if (parent.type.name === "paragraph" && parent.content.size === 0) {
|
|
1901
|
+
const pos = $from.before();
|
|
1902
|
+
return DecorationSet.create(state.doc, [
|
|
1903
|
+
Decoration.node(pos, pos + parent.nodeSize, { class: "caret-empty-para" })
|
|
1904
|
+
]);
|
|
1905
|
+
}
|
|
1906
|
+
return DecorationSet.empty;
|
|
1907
|
+
}
|
|
1908
|
+
},
|
|
1909
|
+
/**
|
|
1910
|
+
* Scroll-after-paste + empty-doc focus recovery.
|
|
1911
|
+
*/
|
|
1912
|
+
view(editorView) {
|
|
1913
|
+
function onPaste() {
|
|
1914
|
+
pendingPaste = true;
|
|
1915
|
+
}
|
|
1916
|
+
editorView.dom.addEventListener("paste", onPaste, true);
|
|
1917
|
+
function onBlur() {
|
|
1918
|
+
editorView.dom.classList.remove("link-hover");
|
|
1919
|
+
}
|
|
1920
|
+
window.addEventListener("blur", onBlur);
|
|
1921
|
+
return {
|
|
1922
|
+
update(view, prevState) {
|
|
1923
|
+
if (isMacOS && view.state.doc !== prevState.doc) {
|
|
1924
|
+
const docSize = view.state.doc.content.size;
|
|
1925
|
+
const prevDocSize = prevState.doc.content.size;
|
|
1926
|
+
if (docSize <= 4 && prevDocSize > 4) {
|
|
1927
|
+
requestAnimationFrame(() => {
|
|
1928
|
+
try {
|
|
1929
|
+
if (!view.hasFocus()) view.focus();
|
|
1930
|
+
} catch {
|
|
1931
|
+
}
|
|
1932
|
+
});
|
|
1933
|
+
}
|
|
1934
|
+
}
|
|
1935
|
+
if (!pendingPaste || view.state.doc.eq(prevState.doc)) return;
|
|
1936
|
+
pendingPaste = false;
|
|
1937
|
+
requestAnimationFrame(() => {
|
|
1938
|
+
try {
|
|
1939
|
+
const { from } = view.state.selection;
|
|
1940
|
+
const coords = view.coordsAtPos(from);
|
|
1941
|
+
const wrapper = view.dom.closest(".editor-wrapper");
|
|
1942
|
+
if (!wrapper) return;
|
|
1943
|
+
const rect = wrapper.getBoundingClientRect();
|
|
1944
|
+
if (coords.top < rect.top || coords.bottom > rect.bottom) {
|
|
1945
|
+
wrapper.scrollTop += coords.top - rect.top - rect.height / 2;
|
|
1946
|
+
}
|
|
1947
|
+
} catch {
|
|
1948
|
+
}
|
|
1949
|
+
});
|
|
1950
|
+
},
|
|
1951
|
+
destroy() {
|
|
1952
|
+
editorView.dom.removeEventListener("paste", onPaste, true);
|
|
1953
|
+
window.removeEventListener("blur", onBlur);
|
|
1954
|
+
editorView.dom.classList.remove("link-hover");
|
|
1955
|
+
}
|
|
1956
|
+
};
|
|
1957
|
+
}
|
|
1958
|
+
});
|
|
1959
|
+
}
|
|
1960
|
+
export {
|
|
1961
|
+
createEditorPropsPlugin
|
|
1962
|
+
};
|
|
1963
|
+
//# sourceMappingURL=editor-props-plugin.js.map
|