@domternal/extension-markdown 0.12.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 +71 -0
- package/dist/index.cjs +1123 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +201 -0
- package/dist/index.d.ts +201 -0
- package/dist/index.js +1102 -0
- package/dist/index.js.map +1 -0
- package/package.json +67 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1102 @@
|
|
|
1
|
+
import { Extension } from '@domternal/core';
|
|
2
|
+
import MarkdownIt from 'markdown-it';
|
|
3
|
+
import { Slice, Mark, Fragment } from '@domternal/pm/model';
|
|
4
|
+
import { PluginKey, Plugin } from '@domternal/pm/state';
|
|
5
|
+
|
|
6
|
+
// src/Markdown.ts
|
|
7
|
+
|
|
8
|
+
// src/parser/mathRules.ts
|
|
9
|
+
var mathInlineRule = (state, silent) => {
|
|
10
|
+
const start = state.pos;
|
|
11
|
+
if (state.src.charCodeAt(start) !== 36) return false;
|
|
12
|
+
if (state.src.charCodeAt(start + 1) === 36) return false;
|
|
13
|
+
let pos = start + 1;
|
|
14
|
+
while (pos < state.posMax) {
|
|
15
|
+
const code = state.src.charCodeAt(pos);
|
|
16
|
+
if (code === 92) {
|
|
17
|
+
pos += 2;
|
|
18
|
+
continue;
|
|
19
|
+
}
|
|
20
|
+
if (code === 36) break;
|
|
21
|
+
if (code === 10) return false;
|
|
22
|
+
pos += 1;
|
|
23
|
+
}
|
|
24
|
+
if (pos >= state.posMax || pos === start + 1) return false;
|
|
25
|
+
if (/\s/.test(state.src.charAt(start + 1))) return false;
|
|
26
|
+
if (/\s/.test(state.src.charAt(pos - 1))) return false;
|
|
27
|
+
if (/\d/.test(state.src.charAt(pos + 1))) return false;
|
|
28
|
+
if (!silent) {
|
|
29
|
+
const token = state.push("math_inline", "", 0);
|
|
30
|
+
token.content = state.src.slice(start + 1, pos);
|
|
31
|
+
}
|
|
32
|
+
state.pos = pos + 1;
|
|
33
|
+
return true;
|
|
34
|
+
};
|
|
35
|
+
var mathBlockRule = (state, startLine, endLine, silent) => {
|
|
36
|
+
const start = (state.bMarks[startLine] ?? 0) + (state.tShift[startLine] ?? 0);
|
|
37
|
+
const lineEnd = state.eMarks[startLine];
|
|
38
|
+
const firstLine = state.src.slice(start, lineEnd);
|
|
39
|
+
if (!firstLine.startsWith("$$")) return false;
|
|
40
|
+
const singleLine = firstLine.length > 4 && firstLine.endsWith("$$");
|
|
41
|
+
if (!singleLine && firstLine.trim() !== "$$") return false;
|
|
42
|
+
if (silent) return true;
|
|
43
|
+
let content;
|
|
44
|
+
let nextLine = startLine + 1;
|
|
45
|
+
if (singleLine) {
|
|
46
|
+
content = firstLine.slice(2, -2).trim();
|
|
47
|
+
} else {
|
|
48
|
+
const lines = [];
|
|
49
|
+
let closed = false;
|
|
50
|
+
for (; nextLine < endLine; nextLine++) {
|
|
51
|
+
const lineStart = (state.bMarks[nextLine] ?? 0) + (state.tShift[nextLine] ?? 0);
|
|
52
|
+
const line = state.src.slice(lineStart, state.eMarks[nextLine]);
|
|
53
|
+
if (line.trim() === "$$") {
|
|
54
|
+
closed = true;
|
|
55
|
+
nextLine += 1;
|
|
56
|
+
break;
|
|
57
|
+
}
|
|
58
|
+
lines.push(line);
|
|
59
|
+
}
|
|
60
|
+
if (!closed) return false;
|
|
61
|
+
content = lines.join("\n");
|
|
62
|
+
}
|
|
63
|
+
const token = state.push("math_block", "", 0);
|
|
64
|
+
token.content = content;
|
|
65
|
+
token.map = [startLine, singleLine ? startLine + 1 : nextLine];
|
|
66
|
+
state.line = singleLine ? startLine + 1 : nextLine;
|
|
67
|
+
return true;
|
|
68
|
+
};
|
|
69
|
+
function addMathInlineRule(md) {
|
|
70
|
+
md.inline.ruler.after("escape", "math_inline", mathInlineRule);
|
|
71
|
+
}
|
|
72
|
+
function addMathBlockRule(md) {
|
|
73
|
+
md.block.ruler.after("fence", "math_block", mathBlockRule, {
|
|
74
|
+
alt: ["paragraph", "reference", "blockquote", "list"]
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
function maybeMerge(a, b) {
|
|
78
|
+
if (a.isText && b.isText && Mark.sameSet(a.marks, b.marks)) {
|
|
79
|
+
return a.withText(
|
|
80
|
+
(a.text ?? "") + (b.text ?? "")
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
var MarkdownParseState = class {
|
|
86
|
+
schema;
|
|
87
|
+
stack;
|
|
88
|
+
marks = Mark.none;
|
|
89
|
+
constructor(schema) {
|
|
90
|
+
this.schema = schema;
|
|
91
|
+
this.stack = [
|
|
92
|
+
{ type: schema.topNodeType, attrs: null, content: [], discardWhenEmpty: false }
|
|
93
|
+
];
|
|
94
|
+
}
|
|
95
|
+
top() {
|
|
96
|
+
const frame = this.stack[this.stack.length - 1];
|
|
97
|
+
if (frame === void 0) throw new Error("markdown parse stack underflow");
|
|
98
|
+
return frame;
|
|
99
|
+
}
|
|
100
|
+
/** The node type currently being built (for split decisions). */
|
|
101
|
+
topType() {
|
|
102
|
+
return this.top().type;
|
|
103
|
+
}
|
|
104
|
+
push(node) {
|
|
105
|
+
this.top().content.push(node);
|
|
106
|
+
}
|
|
107
|
+
addText(text) {
|
|
108
|
+
if (text === "") return;
|
|
109
|
+
const content = this.top().content;
|
|
110
|
+
const last = content[content.length - 1];
|
|
111
|
+
const node = this.schema.text(text, this.marks);
|
|
112
|
+
const merged = last !== void 0 ? maybeMerge(last, node) : null;
|
|
113
|
+
if (merged !== null) {
|
|
114
|
+
content[content.length - 1] = merged;
|
|
115
|
+
} else {
|
|
116
|
+
content.push(node);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
openMark(mark) {
|
|
120
|
+
this.marks = mark.addToSet(this.marks);
|
|
121
|
+
}
|
|
122
|
+
closeMark(type) {
|
|
123
|
+
this.marks = type.removeFromSet(this.marks);
|
|
124
|
+
}
|
|
125
|
+
addNode(type, attrs, content) {
|
|
126
|
+
const node = type.createAndFill(attrs, content, type.isInline ? this.marks : void 0);
|
|
127
|
+
if (node === null) return null;
|
|
128
|
+
this.push(node);
|
|
129
|
+
return node;
|
|
130
|
+
}
|
|
131
|
+
openNode(type, attrs = null, discardWhenEmpty = false) {
|
|
132
|
+
this.stack.push({ type, attrs, content: [], discardWhenEmpty });
|
|
133
|
+
}
|
|
134
|
+
closeNode() {
|
|
135
|
+
if (this.marks.length > 0) this.marks = Mark.none;
|
|
136
|
+
const frame = this.stack.pop();
|
|
137
|
+
if (frame === void 0) throw new Error("markdown parse stack underflow");
|
|
138
|
+
if (frame.discardWhenEmpty && frame.content.length === 0) return null;
|
|
139
|
+
const node = frame.type.createAndFill(frame.attrs, frame.content);
|
|
140
|
+
if (node === null) return null;
|
|
141
|
+
this.push(node);
|
|
142
|
+
return node;
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Close the innermost frame and reopen the same type after inserting
|
|
146
|
+
* `node` as a sibling: how a block-level image escapes its paragraph.
|
|
147
|
+
*/
|
|
148
|
+
splitAround(node) {
|
|
149
|
+
const frame = this.stack.pop();
|
|
150
|
+
if (frame === void 0) throw new Error("markdown parse stack underflow");
|
|
151
|
+
if (frame.content.length > 0) {
|
|
152
|
+
const closed = frame.type.createAndFill(frame.attrs, frame.content);
|
|
153
|
+
if (closed !== null) this.push(closed);
|
|
154
|
+
}
|
|
155
|
+
this.push(node);
|
|
156
|
+
this.openNode(frame.type, frame.attrs, true);
|
|
157
|
+
}
|
|
158
|
+
/** Build the final document; called once after all tokens are consumed. */
|
|
159
|
+
finish() {
|
|
160
|
+
while (this.stack.length > 1) this.closeNode();
|
|
161
|
+
const frame = this.stack.pop();
|
|
162
|
+
if (frame === void 0) throw new Error("markdown parse stack underflow");
|
|
163
|
+
const doc = frame.type.createAndFill(frame.attrs, frame.content);
|
|
164
|
+
if (doc === null) throw new Error("markdown parse produced an invalid document");
|
|
165
|
+
return doc;
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
// src/parser/parser.ts
|
|
170
|
+
function attrFrom(token, name) {
|
|
171
|
+
const value = token.attrGet(name);
|
|
172
|
+
return value === null || value === "" ? null : value;
|
|
173
|
+
}
|
|
174
|
+
function cellAlign(token) {
|
|
175
|
+
const style = token.attrGet("style");
|
|
176
|
+
if (style === null) return null;
|
|
177
|
+
const match = /text-align:\s*(left|center|right)/.exec(style);
|
|
178
|
+
const align = match?.[1];
|
|
179
|
+
return align === void 0 ? null : { textAlign: align };
|
|
180
|
+
}
|
|
181
|
+
function altText(token) {
|
|
182
|
+
return (token.children ?? []).filter((child) => child.type === "text" || child.type === "text_special").map((child) => child.content).join("");
|
|
183
|
+
}
|
|
184
|
+
var TASK_PREFIX = /^\[( |x|X)\](?: |$)/;
|
|
185
|
+
function transformTaskLists(doc, schema) {
|
|
186
|
+
const taskList = schema.nodes["taskList"];
|
|
187
|
+
const taskItem = schema.nodes["taskItem"];
|
|
188
|
+
const bulletList = schema.nodes["bulletList"];
|
|
189
|
+
const listItem = schema.nodes["listItem"];
|
|
190
|
+
if (taskList === void 0 || taskItem === void 0 || bulletList === void 0 || listItem === void 0) {
|
|
191
|
+
return doc;
|
|
192
|
+
}
|
|
193
|
+
const itemMarker = (item) => {
|
|
194
|
+
if (item.type !== listItem) return null;
|
|
195
|
+
const paragraph = item.firstChild;
|
|
196
|
+
if (paragraph?.isTextblock !== true) return null;
|
|
197
|
+
const first = paragraph.firstChild;
|
|
198
|
+
if (first?.isText !== true) return null;
|
|
199
|
+
if (first.marks.length > 0) return null;
|
|
200
|
+
const match = TASK_PREFIX.exec(first.text ?? "");
|
|
201
|
+
return match === null ? null : match[1] ?? " ";
|
|
202
|
+
};
|
|
203
|
+
const convertItem = (item) => {
|
|
204
|
+
const marker = itemMarker(item);
|
|
205
|
+
const children = [];
|
|
206
|
+
item.forEach((child, _offset, index) => {
|
|
207
|
+
if (index === 0 && child.isTextblock) {
|
|
208
|
+
const inline = [];
|
|
209
|
+
child.forEach((inlineChild, _o, inlineIndex) => {
|
|
210
|
+
if (inlineIndex === 0 && inlineChild.isText) {
|
|
211
|
+
const stripped = (inlineChild.text ?? "").replace(TASK_PREFIX, "");
|
|
212
|
+
if (stripped !== "") {
|
|
213
|
+
inline.push(
|
|
214
|
+
inlineChild.withText(stripped)
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
inline.push(inlineChild);
|
|
220
|
+
});
|
|
221
|
+
children.push(child.copy(Fragment.from(inline)));
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
children.push(child);
|
|
225
|
+
});
|
|
226
|
+
return taskItem.create({ checked: marker === "x" || marker === "X" }, children);
|
|
227
|
+
};
|
|
228
|
+
const map = (node) => {
|
|
229
|
+
if (node.isLeaf) return node;
|
|
230
|
+
const mapped = [];
|
|
231
|
+
node.forEach((child) => {
|
|
232
|
+
mapped.push(map(child));
|
|
233
|
+
});
|
|
234
|
+
const rebuilt = node.copy(Fragment.from(mapped));
|
|
235
|
+
if (rebuilt.type !== bulletList || rebuilt.childCount === 0) return rebuilt;
|
|
236
|
+
const items = [];
|
|
237
|
+
rebuilt.forEach((item) => {
|
|
238
|
+
items.push(item);
|
|
239
|
+
});
|
|
240
|
+
if (!items.every((item) => itemMarker(item) !== null)) return rebuilt;
|
|
241
|
+
return taskList.create(null, items.map(convertItem));
|
|
242
|
+
};
|
|
243
|
+
return map(doc);
|
|
244
|
+
}
|
|
245
|
+
function buildMarkdownIt(schema) {
|
|
246
|
+
const md = new MarkdownIt("default", { html: false, linkify: true });
|
|
247
|
+
const hasTableSchema = schema.nodes["table"] !== void 0 && schema.nodes["tableRow"] !== void 0 && schema.nodes["tableCell"] !== void 0 && schema.nodes["tableHeader"] !== void 0;
|
|
248
|
+
if (!hasTableSchema) md.disable("table");
|
|
249
|
+
if (schema.nodes["image"] === void 0) md.disable("image");
|
|
250
|
+
if (schema.marks["strike"] === void 0) md.disable("strikethrough");
|
|
251
|
+
if (schema.marks["link"] === void 0) {
|
|
252
|
+
md.disable(["link", "linkify", "autolink"]);
|
|
253
|
+
md.set({ linkify: false });
|
|
254
|
+
}
|
|
255
|
+
if (schema.nodes["mathInline"] !== void 0) addMathInlineRule(md);
|
|
256
|
+
if (schema.nodes["mathBlock"] !== void 0) addMathBlockRule(md);
|
|
257
|
+
return md;
|
|
258
|
+
}
|
|
259
|
+
function createMarkdownParser(schema) {
|
|
260
|
+
const md = buildMarkdownIt(schema);
|
|
261
|
+
const handlers = {};
|
|
262
|
+
const handleTokens = (state, tokens) => {
|
|
263
|
+
for (const token of tokens) {
|
|
264
|
+
const handler = handlers[token.type];
|
|
265
|
+
if (handler === void 0) {
|
|
266
|
+
throw new Error(`No markdown token handler for "${token.type}"`);
|
|
267
|
+
}
|
|
268
|
+
handler(state, token);
|
|
269
|
+
}
|
|
270
|
+
};
|
|
271
|
+
const node = (name) => schema.nodes[name];
|
|
272
|
+
const block = (tokenName, nodeName, getAttrs, fallback) => {
|
|
273
|
+
const type = node(nodeName);
|
|
274
|
+
if (type === void 0) {
|
|
275
|
+
const fallbackType = fallback === "paragraph" ? node("paragraph") : void 0;
|
|
276
|
+
handlers[`${tokenName}_open`] = (state) => {
|
|
277
|
+
if (fallbackType !== void 0) state.openNode(fallbackType);
|
|
278
|
+
};
|
|
279
|
+
handlers[`${tokenName}_close`] = (state) => {
|
|
280
|
+
if (fallbackType !== void 0) state.closeNode();
|
|
281
|
+
};
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
handlers[`${tokenName}_open`] = (state, token) => {
|
|
285
|
+
state.openNode(type, getAttrs?.(token) ?? null);
|
|
286
|
+
};
|
|
287
|
+
handlers[`${tokenName}_close`] = (state) => {
|
|
288
|
+
state.closeNode();
|
|
289
|
+
};
|
|
290
|
+
};
|
|
291
|
+
const inlineMark = (tokenName, markName) => {
|
|
292
|
+
const type = schema.marks[markName];
|
|
293
|
+
if (type === void 0) {
|
|
294
|
+
ignore(`${tokenName}_open`, `${tokenName}_close`);
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
handlers[`${tokenName}_open`] = (state, token) => {
|
|
298
|
+
state.openMark(
|
|
299
|
+
type.create(
|
|
300
|
+
tokenName === "link" ? { href: attrFrom(token, "href"), title: attrFrom(token, "title") } : null
|
|
301
|
+
)
|
|
302
|
+
);
|
|
303
|
+
};
|
|
304
|
+
handlers[`${tokenName}_close`] = (state) => {
|
|
305
|
+
state.closeMark(type);
|
|
306
|
+
};
|
|
307
|
+
};
|
|
308
|
+
const cell = (tokenName, nodeName) => {
|
|
309
|
+
const cellType = node(nodeName);
|
|
310
|
+
const paragraphType = node("paragraph");
|
|
311
|
+
if (cellType === void 0 || paragraphType === void 0) return;
|
|
312
|
+
handlers[`${tokenName}_open`] = (state, token) => {
|
|
313
|
+
state.openNode(cellType, cellAlign(token));
|
|
314
|
+
state.openNode(paragraphType);
|
|
315
|
+
};
|
|
316
|
+
handlers[`${tokenName}_close`] = (state) => {
|
|
317
|
+
state.closeNode();
|
|
318
|
+
state.closeNode();
|
|
319
|
+
};
|
|
320
|
+
};
|
|
321
|
+
const ignore = (...tokenNames) => {
|
|
322
|
+
for (const name of tokenNames) {
|
|
323
|
+
handlers[name] = () => {
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
};
|
|
327
|
+
block("paragraph", "paragraph");
|
|
328
|
+
block("heading", "heading", (token) => ({ level: Number(token.tag.slice(1)) || 1 }), "paragraph");
|
|
329
|
+
block("blockquote", "blockquote");
|
|
330
|
+
block("bullet_list", "bulletList");
|
|
331
|
+
block("ordered_list", "orderedList", (token) => {
|
|
332
|
+
const start = Number(token.attrGet("start") ?? "1");
|
|
333
|
+
return { start: Number.isFinite(start) && start >= 1 ? start : 1 };
|
|
334
|
+
});
|
|
335
|
+
block("list_item", "listItem");
|
|
336
|
+
block("table", "table");
|
|
337
|
+
block("tr", "tableRow");
|
|
338
|
+
cell("th", "tableHeader");
|
|
339
|
+
cell("td", "tableCell");
|
|
340
|
+
ignore("thead_open", "thead_close", "tbody_open", "tbody_close");
|
|
341
|
+
handlers["inline"] = (state, token) => {
|
|
342
|
+
handleTokens(state, token.children ?? []);
|
|
343
|
+
};
|
|
344
|
+
handlers["text"] = (state, token) => {
|
|
345
|
+
state.addText(token.content);
|
|
346
|
+
};
|
|
347
|
+
handlers["text_special"] = (state, token) => {
|
|
348
|
+
state.addText(token.content);
|
|
349
|
+
};
|
|
350
|
+
handlers["softbreak"] = (state) => {
|
|
351
|
+
state.addText(" ");
|
|
352
|
+
};
|
|
353
|
+
const hardBreakType = node("hardBreak");
|
|
354
|
+
handlers["hardbreak"] = (state) => {
|
|
355
|
+
if (hardBreakType !== void 0) {
|
|
356
|
+
state.addNode(hardBreakType, null);
|
|
357
|
+
} else {
|
|
358
|
+
state.addText(" ");
|
|
359
|
+
}
|
|
360
|
+
};
|
|
361
|
+
const codeBlockType = node("codeBlock") ?? node("paragraph");
|
|
362
|
+
if (codeBlockType !== void 0) {
|
|
363
|
+
const addCodeBlock = (state, content, language) => {
|
|
364
|
+
const text = content.replace(/\n$/, "");
|
|
365
|
+
const attrs = codeBlockType.name === "codeBlock" ? { language } : null;
|
|
366
|
+
state.addNode(codeBlockType, attrs, text === "" ? [] : [schema.text(text)]);
|
|
367
|
+
};
|
|
368
|
+
handlers["fence"] = (state, token) => {
|
|
369
|
+
const language = token.info.trim().split(/\s+/)[0] ?? "";
|
|
370
|
+
addCodeBlock(state, token.content, language === "" ? null : language);
|
|
371
|
+
};
|
|
372
|
+
handlers["code_block"] = (state, token) => {
|
|
373
|
+
addCodeBlock(state, token.content, null);
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
const horizontalRuleType = node("horizontalRule");
|
|
377
|
+
handlers["hr"] = (state) => {
|
|
378
|
+
if (horizontalRuleType !== void 0) state.addNode(horizontalRuleType, null);
|
|
379
|
+
};
|
|
380
|
+
const codeMarkType = schema.marks["code"];
|
|
381
|
+
handlers["code_inline"] = (state, token) => {
|
|
382
|
+
if (codeMarkType === void 0) {
|
|
383
|
+
state.addText(token.content);
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
const mark = codeMarkType.create(null);
|
|
387
|
+
state.openMark(mark);
|
|
388
|
+
state.addText(token.content);
|
|
389
|
+
state.closeMark(codeMarkType);
|
|
390
|
+
};
|
|
391
|
+
inlineMark("strong", "bold");
|
|
392
|
+
inlineMark("em", "italic");
|
|
393
|
+
inlineMark("s", "strike");
|
|
394
|
+
inlineMark("link", "link");
|
|
395
|
+
const imageType = node("image");
|
|
396
|
+
if (imageType !== void 0) {
|
|
397
|
+
handlers["image"] = (state, token) => {
|
|
398
|
+
const src = attrFrom(token, "src");
|
|
399
|
+
if (src === null) return;
|
|
400
|
+
const alt = altText(token);
|
|
401
|
+
const attrs = { src, alt: alt === "" ? null : alt, title: attrFrom(token, "title") };
|
|
402
|
+
if (imageType.isInline) {
|
|
403
|
+
state.addNode(imageType, attrs);
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
const image = imageType.createAndFill(attrs);
|
|
407
|
+
if (image === null) return;
|
|
408
|
+
if (state.topType().name === "paragraph") {
|
|
409
|
+
state.splitAround(image);
|
|
410
|
+
} else {
|
|
411
|
+
state.addText(alt);
|
|
412
|
+
}
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
const mathInlineType = node("mathInline");
|
|
416
|
+
if (mathInlineType !== void 0) {
|
|
417
|
+
handlers["math_inline"] = (state, token) => {
|
|
418
|
+
state.addNode(mathInlineType, { latex: token.content });
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
const mathBlockType = node("mathBlock");
|
|
422
|
+
if (mathBlockType !== void 0) {
|
|
423
|
+
handlers["math_block"] = (state, token) => {
|
|
424
|
+
state.addNode(mathBlockType, { latex: token.content });
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
return {
|
|
428
|
+
parse(markdown) {
|
|
429
|
+
const state = new MarkdownParseState(schema);
|
|
430
|
+
handleTokens(state, md.parse(markdown, {}));
|
|
431
|
+
return transformTaskLists(state.finish(), schema);
|
|
432
|
+
}
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
function parseMarkdown(markdown, schema) {
|
|
436
|
+
return createMarkdownParser(schema).parse(markdown);
|
|
437
|
+
}
|
|
438
|
+
var markdownPastePluginKey = new PluginKey("markdownPaste");
|
|
439
|
+
var BLOCK_SYNTAX = /^(#{1,6} |> |[-*+] |\d+\. |```|\$\$|\[( |x|X)\] |\|.{1,999}\||(---|\*\*\*|___)\s*$)/m;
|
|
440
|
+
var INLINE_SYNTAX = /(\*\*[^*\n]{1,999}\*\*|__[^_\n]{1,999}__|\[[^\]\n]{1,999}\]\([^)\n]{1,999}\)|`[^`\n]{1,999}`|~~[^~\n]{1,999}~~|!\[[^\]\n]{0,999}\]\([^)\n]{1,999}\))/;
|
|
441
|
+
function looksLikeMarkdown(text) {
|
|
442
|
+
return BLOCK_SYNTAX.test(text) || INLINE_SYNTAX.test(text);
|
|
443
|
+
}
|
|
444
|
+
function markdownPastePlugin(getParser) {
|
|
445
|
+
return new Plugin({
|
|
446
|
+
key: markdownPastePluginKey,
|
|
447
|
+
props: {
|
|
448
|
+
handlePaste(view, event) {
|
|
449
|
+
const clipboard = event.clipboardData;
|
|
450
|
+
if (clipboard === null) return false;
|
|
451
|
+
if (clipboard.getData("text/html") !== "") return false;
|
|
452
|
+
const text = clipboard.getData("text/plain");
|
|
453
|
+
if (text === "" || !looksLikeMarkdown(text)) return false;
|
|
454
|
+
if (view.state.selection.$from.parent.type.spec.code === true) return false;
|
|
455
|
+
let doc;
|
|
456
|
+
try {
|
|
457
|
+
doc = getParser().parse(text);
|
|
458
|
+
} catch {
|
|
459
|
+
return false;
|
|
460
|
+
}
|
|
461
|
+
const first = doc.content.firstChild;
|
|
462
|
+
const slice = doc.content.childCount === 1 && first !== null && first.type.name === "paragraph" ? new Slice(first.content, 0, 0) : new Slice(doc.content, 0, 0);
|
|
463
|
+
view.dispatch(view.state.tr.replaceSelection(slice).scrollIntoView());
|
|
464
|
+
return true;
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
});
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// src/serializer/state.ts
|
|
471
|
+
var BACKTICKS = /`+/g;
|
|
472
|
+
function backticksFor(node, side) {
|
|
473
|
+
let len = 0;
|
|
474
|
+
const text = node.isText ? node.text ?? "" : "";
|
|
475
|
+
for (const match of text.matchAll(BACKTICKS)) {
|
|
476
|
+
len = Math.max(len, match[0].length);
|
|
477
|
+
}
|
|
478
|
+
const result = len > 0 ? "`".repeat(len + 1) : "`";
|
|
479
|
+
if (side === -1) return result + (text.startsWith("`") ? " " : "");
|
|
480
|
+
return (text.endsWith("`") ? " " : "") + result;
|
|
481
|
+
}
|
|
482
|
+
var MarkdownSerializerState = class {
|
|
483
|
+
nodes;
|
|
484
|
+
marks;
|
|
485
|
+
warnings = [];
|
|
486
|
+
tightLists;
|
|
487
|
+
delim = "";
|
|
488
|
+
out = "";
|
|
489
|
+
closed = null;
|
|
490
|
+
inTightList = false;
|
|
491
|
+
// True until the first content of a textblock is written; drives
|
|
492
|
+
// start-of-line escaping (a list item's first line starts after `- `,
|
|
493
|
+
// which the output buffer alone cannot reveal).
|
|
494
|
+
atBlockStart = false;
|
|
495
|
+
constructor(specs, options = {}) {
|
|
496
|
+
this.nodes = specs.nodes;
|
|
497
|
+
this.marks = specs.marks;
|
|
498
|
+
this.tightLists = options.tightLists ?? true;
|
|
499
|
+
}
|
|
500
|
+
warn(code, message, nodeType) {
|
|
501
|
+
const duplicate = this.warnings.some(
|
|
502
|
+
(w) => w.code === code && w.message === message && w.nodeType === nodeType
|
|
503
|
+
);
|
|
504
|
+
if (duplicate) return;
|
|
505
|
+
this.warnings.push(nodeType === void 0 ? { code, message } : { code, message, nodeType });
|
|
506
|
+
}
|
|
507
|
+
flushClose(size = 2) {
|
|
508
|
+
if (this.closed === null) return;
|
|
509
|
+
if (!this.atBlank()) this.out += "\n";
|
|
510
|
+
if (size > 1) {
|
|
511
|
+
let delimMin = this.delim;
|
|
512
|
+
let end = delimMin.length;
|
|
513
|
+
while (end > 0 && /\s/.test(delimMin.charAt(end - 1))) end -= 1;
|
|
514
|
+
delimMin = delimMin.slice(0, end);
|
|
515
|
+
for (let i = 1; i < size; i++) this.out += delimMin + "\n";
|
|
516
|
+
}
|
|
517
|
+
this.closed = null;
|
|
518
|
+
}
|
|
519
|
+
/**
|
|
520
|
+
* Render a block wrapped in a line prefix: `firstDelim` on its first line,
|
|
521
|
+
* `delim` on every following line (blockquote `> `, list item indent).
|
|
522
|
+
*/
|
|
523
|
+
wrapBlock(delim, firstDelim, node, f) {
|
|
524
|
+
const old = this.delim;
|
|
525
|
+
this.write(firstDelim ?? delim);
|
|
526
|
+
this.delim += delim;
|
|
527
|
+
f();
|
|
528
|
+
this.delim = old;
|
|
529
|
+
this.closeBlock(node);
|
|
530
|
+
}
|
|
531
|
+
atBlank() {
|
|
532
|
+
return /(^|\n)$/.test(this.out);
|
|
533
|
+
}
|
|
534
|
+
ensureNewLine() {
|
|
535
|
+
if (!this.atBlank()) this.out += "\n";
|
|
536
|
+
}
|
|
537
|
+
write(content) {
|
|
538
|
+
this.flushClose();
|
|
539
|
+
if (this.delim !== "" && this.atBlank()) this.out += this.delim;
|
|
540
|
+
if (content !== void 0) this.out += content;
|
|
541
|
+
}
|
|
542
|
+
/** Close the block: the next write emits the separating blank line. */
|
|
543
|
+
closeBlock(node) {
|
|
544
|
+
this.closed = node;
|
|
545
|
+
}
|
|
546
|
+
text(text, escape = true) {
|
|
547
|
+
const lines = text.split("\n");
|
|
548
|
+
for (let i = 0; i < lines.length; i++) {
|
|
549
|
+
this.write();
|
|
550
|
+
const line = lines[i] ?? "";
|
|
551
|
+
if (!escape && line.startsWith("[") && /(^|[^\\])!$/.test(this.out)) {
|
|
552
|
+
this.out = this.out.slice(0, this.out.length - 1) + "\\!";
|
|
553
|
+
}
|
|
554
|
+
this.out += escape ? this.esc(line, this.atBlockStart) : line;
|
|
555
|
+
if (i !== lines.length - 1) this.out += "\n";
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
render(node, parent, index) {
|
|
559
|
+
const serializer = this.nodes[node.type.name];
|
|
560
|
+
if (serializer === void 0) {
|
|
561
|
+
this.warn(
|
|
562
|
+
"unsupported-node",
|
|
563
|
+
node.type.isLeaf ? `Node type "${node.type.name}" has no Markdown mapping; node omitted` : `Node type "${node.type.name}" has no Markdown mapping; content flattened`,
|
|
564
|
+
node.type.name
|
|
565
|
+
);
|
|
566
|
+
if (!node.type.isLeaf) {
|
|
567
|
+
if (node.type.inlineContent) {
|
|
568
|
+
this.renderInline(node);
|
|
569
|
+
} else {
|
|
570
|
+
this.renderContent(node);
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
if (node.isBlock) this.closeBlock(node);
|
|
574
|
+
return;
|
|
575
|
+
}
|
|
576
|
+
serializer(this, node, parent, index);
|
|
577
|
+
}
|
|
578
|
+
renderContent(parent) {
|
|
579
|
+
parent.forEach((node, _offset, index) => {
|
|
580
|
+
this.render(node, parent, index);
|
|
581
|
+
});
|
|
582
|
+
}
|
|
583
|
+
renderInline(parent, fromBlockStart = true) {
|
|
584
|
+
this.atBlockStart = fromBlockStart;
|
|
585
|
+
const active = [];
|
|
586
|
+
let trailing = "";
|
|
587
|
+
const progress = (node, index) => {
|
|
588
|
+
let marks = node ? node.marks : [];
|
|
589
|
+
marks = marks.filter((mark) => {
|
|
590
|
+
if (this.marks[mark.type.name] !== void 0) return true;
|
|
591
|
+
this.warn(
|
|
592
|
+
"unsupported-mark",
|
|
593
|
+
`Mark type "${mark.type.name}" has no Markdown mapping; formatting dropped`,
|
|
594
|
+
mark.type.name
|
|
595
|
+
);
|
|
596
|
+
return false;
|
|
597
|
+
});
|
|
598
|
+
if (node !== null && node.type.name === "hardBreak") {
|
|
599
|
+
marks = marks.filter((mark) => {
|
|
600
|
+
if (index + 1 === parent.childCount) return false;
|
|
601
|
+
const next = parent.child(index + 1);
|
|
602
|
+
return mark.isInSet(next.marks) && (!next.isText || /\S/.test(next.text ?? ""));
|
|
603
|
+
});
|
|
604
|
+
}
|
|
605
|
+
let leading = trailing;
|
|
606
|
+
trailing = "";
|
|
607
|
+
if (node?.isText === true && marks.some((mark) => {
|
|
608
|
+
const spec = this.marks[mark.type.name];
|
|
609
|
+
return spec?.expelEnclosingWhitespace === true;
|
|
610
|
+
})) {
|
|
611
|
+
const text = node.text ?? "";
|
|
612
|
+
let coreStart = 0;
|
|
613
|
+
while (coreStart < text.length && /\s/.test(text.charAt(coreStart))) coreStart += 1;
|
|
614
|
+
let coreEnd = text.length;
|
|
615
|
+
while (coreEnd > coreStart && /\s/.test(text.charAt(coreEnd - 1))) coreEnd -= 1;
|
|
616
|
+
const lead = text.slice(0, coreStart);
|
|
617
|
+
const core = text.slice(coreStart, coreEnd);
|
|
618
|
+
const trail = text.slice(coreEnd);
|
|
619
|
+
if (lead !== "" || trail !== "") {
|
|
620
|
+
leading += lead;
|
|
621
|
+
trailing = trail;
|
|
622
|
+
if (core === "") {
|
|
623
|
+
marks = active.slice();
|
|
624
|
+
node = null;
|
|
625
|
+
} else {
|
|
626
|
+
node = node.withText(core);
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
const inner = marks.length > 0 ? marks[marks.length - 1] : void 0;
|
|
631
|
+
const noEsc = inner !== void 0 && this.marks[inner.type.name]?.escape === false;
|
|
632
|
+
const len = marks.length - (noEsc ? 1 : 0);
|
|
633
|
+
outer: for (let i = 0; i < len; i++) {
|
|
634
|
+
const mark = marks[i];
|
|
635
|
+
if (mark === void 0) break;
|
|
636
|
+
if (this.marks[mark.type.name]?.mixable !== true) break;
|
|
637
|
+
for (let j = 0; j < active.length; j++) {
|
|
638
|
+
const other = active[j];
|
|
639
|
+
if (other === void 0) break;
|
|
640
|
+
if (this.marks[other.type.name]?.mixable !== true) break;
|
|
641
|
+
if (mark.eq(other)) {
|
|
642
|
+
if (i > j) {
|
|
643
|
+
marks = [
|
|
644
|
+
...marks.slice(0, j),
|
|
645
|
+
mark,
|
|
646
|
+
...marks.slice(j, i),
|
|
647
|
+
...marks.slice(i + 1, len)
|
|
648
|
+
];
|
|
649
|
+
} else if (j > i) {
|
|
650
|
+
marks = [
|
|
651
|
+
...marks.slice(0, i),
|
|
652
|
+
...marks.slice(i + 1, j),
|
|
653
|
+
mark,
|
|
654
|
+
...marks.slice(j, len)
|
|
655
|
+
];
|
|
656
|
+
}
|
|
657
|
+
continue outer;
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
let keep = 0;
|
|
662
|
+
while (keep < Math.min(active.length, len)) {
|
|
663
|
+
const activeMark = active[keep];
|
|
664
|
+
const nextMark = marks[keep];
|
|
665
|
+
if (activeMark === void 0 || nextMark === void 0) break;
|
|
666
|
+
if (!activeMark.eq(nextMark)) break;
|
|
667
|
+
keep += 1;
|
|
668
|
+
}
|
|
669
|
+
while (keep < active.length) {
|
|
670
|
+
const mark = active.pop();
|
|
671
|
+
if (mark === void 0) break;
|
|
672
|
+
this.text(this.markString(mark, false, parent, index), false);
|
|
673
|
+
}
|
|
674
|
+
if (leading !== "") this.text(leading);
|
|
675
|
+
if (node !== null) {
|
|
676
|
+
while (active.length < len) {
|
|
677
|
+
const mark = marks[active.length];
|
|
678
|
+
if (mark === void 0) break;
|
|
679
|
+
active.push(mark);
|
|
680
|
+
this.text(this.markString(mark, true, parent, index), false);
|
|
681
|
+
this.atBlockStart = false;
|
|
682
|
+
}
|
|
683
|
+
if (inner !== void 0 && noEsc && node.isText) {
|
|
684
|
+
this.text(
|
|
685
|
+
this.markString(inner, true, parent, index) + (node.text ?? "") + this.markString(inner, false, parent, index + 1),
|
|
686
|
+
false
|
|
687
|
+
);
|
|
688
|
+
} else {
|
|
689
|
+
this.render(node, parent, index);
|
|
690
|
+
}
|
|
691
|
+
this.atBlockStart = false;
|
|
692
|
+
}
|
|
693
|
+
};
|
|
694
|
+
parent.forEach((node, _offset, index) => {
|
|
695
|
+
progress(node, index);
|
|
696
|
+
});
|
|
697
|
+
progress(null, parent.childCount);
|
|
698
|
+
this.atBlockStart = false;
|
|
699
|
+
}
|
|
700
|
+
renderList(node, delim, firstDelim) {
|
|
701
|
+
if (this.closed !== null && this.closed.type === node.type) {
|
|
702
|
+
this.flushClose(3);
|
|
703
|
+
} else if (this.inTightList) {
|
|
704
|
+
this.flushClose(1);
|
|
705
|
+
}
|
|
706
|
+
const isTight = this.tightLists;
|
|
707
|
+
const prevTight = this.inTightList;
|
|
708
|
+
this.inTightList = isTight;
|
|
709
|
+
node.forEach((child, _offset, index) => {
|
|
710
|
+
if (index > 0 && isTight) this.flushClose(1);
|
|
711
|
+
this.wrapBlock(delim, firstDelim(index), node, () => {
|
|
712
|
+
this.render(child, node, index);
|
|
713
|
+
});
|
|
714
|
+
});
|
|
715
|
+
this.inTightList = prevTight;
|
|
716
|
+
}
|
|
717
|
+
/**
|
|
718
|
+
* Escape Markdown syntax in plain text. Beyond the CommonMark set this
|
|
719
|
+
* also escapes `|` (table cells), `$` (this package's own math rules), and
|
|
720
|
+
* `<` plus entity-like `&` (raw-HTML/autolink/entity ambiguity on external
|
|
721
|
+
* renderers): all render as the literal character everywhere.
|
|
722
|
+
*/
|
|
723
|
+
esc(str, startOfLine = false) {
|
|
724
|
+
let escaped = str.replace(/[`*\\~[\]_<$|&]/g, (m, i) => {
|
|
725
|
+
if (m === "_") {
|
|
726
|
+
return i > 0 && i + 1 < str.length && /\w/.test(str[i - 1] ?? "") && /\w/.test(str[i + 1] ?? "") ? m : "\\" + m;
|
|
727
|
+
}
|
|
728
|
+
if (m === "&") return /[a-z#]/i.test(str[i + 1] ?? "") ? "\\&" : m;
|
|
729
|
+
return "\\" + m;
|
|
730
|
+
});
|
|
731
|
+
if (startOfLine) {
|
|
732
|
+
escaped = escaped.replace(/^([-*>]|\+ )/, "\\$&").replace(/^(\s*)(#{1,6})(\s|$)/, "$1\\$2$3").replace(/^(\s*\d+)\.(\s|$)/, "$1\\.$2");
|
|
733
|
+
}
|
|
734
|
+
return escaped;
|
|
735
|
+
}
|
|
736
|
+
markString(mark, open, parent, index) {
|
|
737
|
+
const spec = this.marks[mark.type.name];
|
|
738
|
+
if (spec === void 0) return "";
|
|
739
|
+
const value = open ? spec.open : spec.close;
|
|
740
|
+
return typeof value === "string" ? value : value(this, mark, parent, index);
|
|
741
|
+
}
|
|
742
|
+
/** The accumulated output; call once after rendering the document. */
|
|
743
|
+
finish() {
|
|
744
|
+
return this.out;
|
|
745
|
+
}
|
|
746
|
+
};
|
|
747
|
+
|
|
748
|
+
// src/serializer/specs.ts
|
|
749
|
+
function attrString(node, name) {
|
|
750
|
+
const value = node.attrs[name];
|
|
751
|
+
return typeof value === "string" && value !== "" ? value : null;
|
|
752
|
+
}
|
|
753
|
+
function attrNumber(node, name) {
|
|
754
|
+
const value = node.attrs[name];
|
|
755
|
+
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
756
|
+
}
|
|
757
|
+
function warnLossyBlockAttrs(state, node) {
|
|
758
|
+
const textAlign = attrString(node, "textAlign");
|
|
759
|
+
if (textAlign !== null && textAlign !== "left") {
|
|
760
|
+
state.warn("lossy-attribute", "Text alignment is not representable in Markdown", node.type.name);
|
|
761
|
+
}
|
|
762
|
+
if (attrString(node, "lineHeight") !== null) {
|
|
763
|
+
state.warn("lossy-attribute", "Line height is not representable in Markdown", node.type.name);
|
|
764
|
+
}
|
|
765
|
+
if (attrString(node, "backgroundColor") !== null || attrString(node, "backgroundColorToken") !== null) {
|
|
766
|
+
state.warn("lossy-attribute", "Block background color is not representable in Markdown", node.type.name);
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
function escapeLinkDestination(url) {
|
|
770
|
+
return url.replace(/[\\()"]/g, "\\$&");
|
|
771
|
+
}
|
|
772
|
+
function escapeLinkTitle(title) {
|
|
773
|
+
return title.replace(/[\\"]/g, "\\$&");
|
|
774
|
+
}
|
|
775
|
+
function isPlainUrl(link, parent, index) {
|
|
776
|
+
const href = link.attrs["href"];
|
|
777
|
+
if (typeof href !== "string" || attrTitle(link) !== null || !/^\w+:/.test(href)) return false;
|
|
778
|
+
const content = parent.child(index);
|
|
779
|
+
if (!content.isText || content.text !== href || content.marks[content.marks.length - 1] !== link) {
|
|
780
|
+
return false;
|
|
781
|
+
}
|
|
782
|
+
return index === parent.childCount - 1 || !link.isInSet(parent.child(index + 1).marks);
|
|
783
|
+
}
|
|
784
|
+
function attrTitle(mark) {
|
|
785
|
+
const title = mark.attrs["title"];
|
|
786
|
+
return typeof title === "string" && title !== "" ? title : null;
|
|
787
|
+
}
|
|
788
|
+
function leafText(node) {
|
|
789
|
+
const spec = node.type.spec.leafText;
|
|
790
|
+
return typeof spec === "function" ? spec(node) : null;
|
|
791
|
+
}
|
|
792
|
+
function serializeCell(state, cell) {
|
|
793
|
+
const sub = new MarkdownSerializerState(
|
|
794
|
+
{ nodes: state.nodes, marks: state.marks },
|
|
795
|
+
{ tightLists: state.tightLists }
|
|
796
|
+
);
|
|
797
|
+
sub.renderContent(cell);
|
|
798
|
+
for (const warning of sub.warnings) state.warn(warning.code, warning.message, warning.nodeType);
|
|
799
|
+
const raw = sub.finish();
|
|
800
|
+
if (cell.childCount > 1 || raw.includes("\n")) {
|
|
801
|
+
state.warn("lossy-structure", "Table cell content flattened to one line", cell.type.name);
|
|
802
|
+
}
|
|
803
|
+
return raw.replace(/\\\n/g, " ").replace(/\n+/g, " ").trim().replace(/(?<!\\)\|/g, "\\|");
|
|
804
|
+
}
|
|
805
|
+
function separatorFor(alignment) {
|
|
806
|
+
if (alignment === "center") return ":---:";
|
|
807
|
+
if (alignment === "right") return "---:";
|
|
808
|
+
return "---";
|
|
809
|
+
}
|
|
810
|
+
var table = (state, node) => {
|
|
811
|
+
if (node.childCount === 0) return;
|
|
812
|
+
const rows = [];
|
|
813
|
+
const alignments = [];
|
|
814
|
+
node.forEach((row, _rowOffset, rowIndex) => {
|
|
815
|
+
const cells = [];
|
|
816
|
+
row.forEach((cell, _cellOffset, cellIndex) => {
|
|
817
|
+
if ((attrNumber(cell, "colspan") ?? 1) > 1 || (attrNumber(cell, "rowspan") ?? 1) > 1) {
|
|
818
|
+
state.warn("lossy-structure", "Merged table cells are not representable in Markdown", node.type.name);
|
|
819
|
+
}
|
|
820
|
+
if (attrString(cell, "background") !== null) {
|
|
821
|
+
state.warn("lossy-attribute", "Table cell background is not representable in Markdown", cell.type.name);
|
|
822
|
+
}
|
|
823
|
+
if (attrString(cell, "verticalAlign") !== null) {
|
|
824
|
+
state.warn("lossy-attribute", "Table cell vertical alignment is not representable in Markdown", cell.type.name);
|
|
825
|
+
}
|
|
826
|
+
if (rowIndex === 0) {
|
|
827
|
+
alignments.push(attrString(cell, "textAlign"));
|
|
828
|
+
} else {
|
|
829
|
+
const align = attrString(cell, "textAlign");
|
|
830
|
+
if (align !== null && align !== (alignments[cellIndex] ?? null)) {
|
|
831
|
+
state.warn("lossy-attribute", "Cell text alignment differing from its column is not representable in Markdown", cell.type.name);
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
cells.push(serializeCell(state, cell));
|
|
835
|
+
});
|
|
836
|
+
rows.push(cells);
|
|
837
|
+
});
|
|
838
|
+
const columnCount = Math.max(...rows.map((cells) => cells.length), alignments.length);
|
|
839
|
+
const line = (cells) => {
|
|
840
|
+
const padded = Array.from({ length: columnCount }, (_v, i) => cells[i] ?? "");
|
|
841
|
+
return `| ${padded.join(" | ")} |`;
|
|
842
|
+
};
|
|
843
|
+
const [headerRow, ...bodyRows] = rows;
|
|
844
|
+
state.write(line(headerRow ?? []));
|
|
845
|
+
state.ensureNewLine();
|
|
846
|
+
state.write(
|
|
847
|
+
`| ${Array.from({ length: columnCount }, (_v, i) => separatorFor(alignments[i] ?? null)).join(" | ")} |`
|
|
848
|
+
);
|
|
849
|
+
for (const cells of bodyRows) {
|
|
850
|
+
state.ensureNewLine();
|
|
851
|
+
state.write(line(cells));
|
|
852
|
+
}
|
|
853
|
+
state.closeBlock(node);
|
|
854
|
+
};
|
|
855
|
+
var defaultNodeSerializers = {
|
|
856
|
+
text: (state, node) => {
|
|
857
|
+
state.text(node.text ?? "");
|
|
858
|
+
},
|
|
859
|
+
paragraph: (state, node) => {
|
|
860
|
+
warnLossyBlockAttrs(state, node);
|
|
861
|
+
state.renderInline(node);
|
|
862
|
+
state.closeBlock(node);
|
|
863
|
+
},
|
|
864
|
+
heading: (state, node) => {
|
|
865
|
+
warnLossyBlockAttrs(state, node);
|
|
866
|
+
const level = attrNumber(node, "level") ?? 1;
|
|
867
|
+
state.write("#".repeat(Math.min(Math.max(level, 1), 6)) + " ");
|
|
868
|
+
state.renderInline(node, false);
|
|
869
|
+
state.closeBlock(node);
|
|
870
|
+
},
|
|
871
|
+
blockquote: (state, node) => {
|
|
872
|
+
state.wrapBlock("> ", null, node, () => {
|
|
873
|
+
state.renderContent(node);
|
|
874
|
+
});
|
|
875
|
+
},
|
|
876
|
+
codeBlock: (state, node) => {
|
|
877
|
+
const runs = node.textContent.match(/`{3,}/g);
|
|
878
|
+
const fence = "`".repeat(Math.max(3, ...(runs ?? [""]).map((run) => run.length + 1)));
|
|
879
|
+
const language = (attrString(node, "language") ?? "").split(/\s+/)[0]?.replace(/`/g, "") ?? "";
|
|
880
|
+
state.write(fence + language + "\n");
|
|
881
|
+
state.text(node.textContent, false);
|
|
882
|
+
state.ensureNewLine();
|
|
883
|
+
state.write(fence);
|
|
884
|
+
state.closeBlock(node);
|
|
885
|
+
},
|
|
886
|
+
horizontalRule: (state, node) => {
|
|
887
|
+
state.write("---");
|
|
888
|
+
state.closeBlock(node);
|
|
889
|
+
},
|
|
890
|
+
hardBreak: (state, node, parent, index) => {
|
|
891
|
+
for (let i = index + 1; i < parent.childCount; i++) {
|
|
892
|
+
if (parent.child(i).type !== node.type) {
|
|
893
|
+
state.write("\\\n");
|
|
894
|
+
return;
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
},
|
|
898
|
+
bulletList: (state, node) => {
|
|
899
|
+
state.renderList(node, " ", () => "- ");
|
|
900
|
+
},
|
|
901
|
+
orderedList: (state, node) => {
|
|
902
|
+
const start = attrNumber(node, "start") ?? 1;
|
|
903
|
+
const maxWidth = String(start + node.childCount - 1).length;
|
|
904
|
+
const indent = " ".repeat(maxWidth + 2);
|
|
905
|
+
state.renderList(node, indent, (index) => {
|
|
906
|
+
const marker = String(start + index);
|
|
907
|
+
return " ".repeat(maxWidth - marker.length) + marker + ". ";
|
|
908
|
+
});
|
|
909
|
+
},
|
|
910
|
+
listItem: (state, node) => {
|
|
911
|
+
state.renderContent(node);
|
|
912
|
+
},
|
|
913
|
+
taskList: (state, node) => {
|
|
914
|
+
state.renderList(node, " ", () => "- ");
|
|
915
|
+
},
|
|
916
|
+
taskItem: (state, node) => {
|
|
917
|
+
state.write(`[${node.attrs["checked"] === true ? "x" : " "}] `);
|
|
918
|
+
state.renderContent(node);
|
|
919
|
+
},
|
|
920
|
+
image: (state, node) => {
|
|
921
|
+
const src = attrString(node, "src");
|
|
922
|
+
if (src === null) {
|
|
923
|
+
state.warn("unsupported-node", "Image without src omitted", node.type.name);
|
|
924
|
+
return;
|
|
925
|
+
}
|
|
926
|
+
const width = node.attrs["width"];
|
|
927
|
+
const height = node.attrs["height"];
|
|
928
|
+
if (width !== null && width !== void 0 || height !== null && height !== void 0) {
|
|
929
|
+
state.warn("lossy-attribute", "Image dimensions are not representable in Markdown", node.type.name);
|
|
930
|
+
}
|
|
931
|
+
const alt = node.attrs["alt"];
|
|
932
|
+
const title = attrString(node, "title");
|
|
933
|
+
state.write(
|
|
934
|
+
`}${title !== null ? ` "${escapeLinkTitle(title)}"` : ""})`
|
|
935
|
+
);
|
|
936
|
+
if (!node.isInline) state.closeBlock(node);
|
|
937
|
+
},
|
|
938
|
+
table,
|
|
939
|
+
tableRow: () => {
|
|
940
|
+
},
|
|
941
|
+
details: (state, node) => {
|
|
942
|
+
state.warn("lossy-structure", "Toggle block flattened: summary becomes a bold paragraph", node.type.name);
|
|
943
|
+
state.renderContent(node);
|
|
944
|
+
},
|
|
945
|
+
detailsSummary: (state, node) => {
|
|
946
|
+
if (node.content.size === 0) {
|
|
947
|
+
state.closeBlock(node);
|
|
948
|
+
return;
|
|
949
|
+
}
|
|
950
|
+
state.write("**");
|
|
951
|
+
state.renderInline(node, false);
|
|
952
|
+
state.write("**");
|
|
953
|
+
state.closeBlock(node);
|
|
954
|
+
},
|
|
955
|
+
detailsContent: (state, node) => {
|
|
956
|
+
state.renderContent(node);
|
|
957
|
+
},
|
|
958
|
+
emoji: (state, node) => {
|
|
959
|
+
const name = attrString(node, "name");
|
|
960
|
+
const glyph = leafText(node);
|
|
961
|
+
state.text(glyph !== null && glyph !== "" ? glyph : name !== null ? `:${name}:` : "");
|
|
962
|
+
},
|
|
963
|
+
mention: (state, node) => {
|
|
964
|
+
const label = attrString(node, "label");
|
|
965
|
+
const text = leafText(node);
|
|
966
|
+
state.text(text !== null && text !== "" ? text : label !== null ? `@${label}` : "");
|
|
967
|
+
state.warn("lossy-structure", "Mentions serialize as plain text", node.type.name);
|
|
968
|
+
},
|
|
969
|
+
mathInline: (state, node) => {
|
|
970
|
+
state.write(`$${attrString(node, "latex") ?? ""}$`);
|
|
971
|
+
},
|
|
972
|
+
mathBlock: (state, node) => {
|
|
973
|
+
state.write("$$\n");
|
|
974
|
+
state.text(attrString(node, "latex") ?? "", false);
|
|
975
|
+
state.ensureNewLine();
|
|
976
|
+
state.write("$$");
|
|
977
|
+
state.closeBlock(node);
|
|
978
|
+
},
|
|
979
|
+
tableOfContents: (state, node) => {
|
|
980
|
+
state.warn("unsupported-node", "Table of contents block omitted (generated content)", node.type.name);
|
|
981
|
+
}
|
|
982
|
+
};
|
|
983
|
+
var defaultMarkSpecs = {
|
|
984
|
+
bold: { open: "**", close: "**", mixable: true, expelEnclosingWhitespace: true },
|
|
985
|
+
italic: { open: "*", close: "*", mixable: true, expelEnclosingWhitespace: true },
|
|
986
|
+
strike: { open: "~~", close: "~~", mixable: true, expelEnclosingWhitespace: true },
|
|
987
|
+
code: {
|
|
988
|
+
open: (_state, _mark, parent, index) => backticksFor(parent.child(index), -1),
|
|
989
|
+
close: (_state, _mark, parent, index) => backticksFor(parent.child(index - 1), 1),
|
|
990
|
+
escape: false
|
|
991
|
+
},
|
|
992
|
+
link: {
|
|
993
|
+
open: (_state, mark, parent, index) => isPlainUrl(mark, parent, index) ? "<" : "[",
|
|
994
|
+
close: (_state, mark, parent, index) => {
|
|
995
|
+
if (isPlainUrl(mark, parent, index - 1)) return ">";
|
|
996
|
+
const href = mark.attrs["href"];
|
|
997
|
+
const title = attrTitle(mark);
|
|
998
|
+
return `](${escapeLinkDestination(typeof href === "string" ? href : "")}${title !== null ? ` "${escapeLinkTitle(title)}"` : ""})`;
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
};
|
|
1002
|
+
|
|
1003
|
+
// src/serializer/serializer.ts
|
|
1004
|
+
function createMarkdownSerializer(specs = {}, options = {}) {
|
|
1005
|
+
const nodes = { ...defaultNodeSerializers, ...specs.nodes };
|
|
1006
|
+
const marks = { ...defaultMarkSpecs, ...specs.marks };
|
|
1007
|
+
return {
|
|
1008
|
+
serialize(node) {
|
|
1009
|
+
const state = new MarkdownSerializerState({ nodes, marks }, options);
|
|
1010
|
+
if (node.isTextblock) {
|
|
1011
|
+
state.renderInline(node);
|
|
1012
|
+
} else {
|
|
1013
|
+
state.renderContent(node);
|
|
1014
|
+
}
|
|
1015
|
+
return { markdown: state.finish().replace(/\n+$/, ""), warnings: state.warnings };
|
|
1016
|
+
}
|
|
1017
|
+
};
|
|
1018
|
+
}
|
|
1019
|
+
function serializeMarkdown(node, options = {}) {
|
|
1020
|
+
const { specs, ...serializerOptions } = options;
|
|
1021
|
+
return createMarkdownSerializer(specs, serializerOptions).serialize(node);
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
// src/Markdown.ts
|
|
1025
|
+
function getMarkdown(editor) {
|
|
1026
|
+
const storage = editor.storage["markdown"];
|
|
1027
|
+
const serializer = storage?.serializer ?? createMarkdownSerializer();
|
|
1028
|
+
return serializer.serialize(editor.state.doc);
|
|
1029
|
+
}
|
|
1030
|
+
var Markdown = Extension.create({
|
|
1031
|
+
name: "markdown",
|
|
1032
|
+
addOptions() {
|
|
1033
|
+
return {
|
|
1034
|
+
paste: true,
|
|
1035
|
+
tightLists: true,
|
|
1036
|
+
specs: null
|
|
1037
|
+
};
|
|
1038
|
+
},
|
|
1039
|
+
addStorage() {
|
|
1040
|
+
return {
|
|
1041
|
+
parser: null,
|
|
1042
|
+
serializer: null
|
|
1043
|
+
};
|
|
1044
|
+
},
|
|
1045
|
+
onCreate() {
|
|
1046
|
+
const schema = this.editor?.state.schema;
|
|
1047
|
+
if (schema !== void 0) this.storage.parser ??= createMarkdownParser(schema);
|
|
1048
|
+
this.storage.serializer ??= createMarkdownSerializer(this.options.specs ?? {}, {
|
|
1049
|
+
tightLists: this.options.tightLists
|
|
1050
|
+
});
|
|
1051
|
+
},
|
|
1052
|
+
addCommands() {
|
|
1053
|
+
const parserFor = (schema) => {
|
|
1054
|
+
this.storage.parser ??= createMarkdownParser(schema);
|
|
1055
|
+
return this.storage.parser;
|
|
1056
|
+
};
|
|
1057
|
+
return {
|
|
1058
|
+
insertMarkdown: (markdown) => ({ state, commands }) => {
|
|
1059
|
+
const doc = parserFor(state.schema).parse(markdown);
|
|
1060
|
+
const first = doc.content.firstChild;
|
|
1061
|
+
const content = doc.content.childCount === 1 && first !== null && first.type.name === "paragraph" ? first.content.toJSON() : doc.content.toJSON();
|
|
1062
|
+
return commands.insertContent(content ?? []);
|
|
1063
|
+
},
|
|
1064
|
+
setMarkdownContent: (markdown, options) => ({ state, commands }) => {
|
|
1065
|
+
const doc = parserFor(state.schema).parse(markdown);
|
|
1066
|
+
return commands.setContent(doc.toJSON(), options);
|
|
1067
|
+
}
|
|
1068
|
+
};
|
|
1069
|
+
},
|
|
1070
|
+
addProseMirrorPlugins() {
|
|
1071
|
+
if (!this.options.paste) return [];
|
|
1072
|
+
return [
|
|
1073
|
+
markdownPastePlugin(() => {
|
|
1074
|
+
const schema = this.editor?.state.schema;
|
|
1075
|
+
if (schema !== void 0) this.storage.parser ??= createMarkdownParser(schema);
|
|
1076
|
+
const parser = this.storage.parser;
|
|
1077
|
+
if (parser === null) throw new Error("Markdown parser is unavailable before editor create");
|
|
1078
|
+
return parser;
|
|
1079
|
+
})
|
|
1080
|
+
];
|
|
1081
|
+
}
|
|
1082
|
+
});
|
|
1083
|
+
|
|
1084
|
+
// src/download.ts
|
|
1085
|
+
function downloadMarkdown(editor, fileName = "document.md") {
|
|
1086
|
+
const result = getMarkdown(editor);
|
|
1087
|
+
const blob = new Blob([result.markdown], { type: "text/markdown;charset=utf-8" });
|
|
1088
|
+
const url = URL.createObjectURL(blob);
|
|
1089
|
+
const anchor = document.createElement("a");
|
|
1090
|
+
anchor.href = url;
|
|
1091
|
+
anchor.download = fileName;
|
|
1092
|
+
anchor.rel = "noopener";
|
|
1093
|
+
document.body.appendChild(anchor);
|
|
1094
|
+
anchor.click();
|
|
1095
|
+
anchor.remove();
|
|
1096
|
+
URL.revokeObjectURL(url);
|
|
1097
|
+
return result;
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
export { Markdown, backticksFor, createMarkdownParser, createMarkdownSerializer, Markdown as default, defaultMarkSpecs, defaultNodeSerializers, downloadMarkdown, getMarkdown, looksLikeMarkdown, markdownPastePlugin, markdownPastePluginKey, parseMarkdown, serializeMarkdown };
|
|
1101
|
+
//# sourceMappingURL=index.js.map
|
|
1102
|
+
//# sourceMappingURL=index.js.map
|