@rathkit/rte-core 0.1.0-alpha.3 → 0.1.0-alpha.5
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/README.md +1 -1
- package/dist/autoformat/autoformat.js +18 -3
- package/dist/base64.js +5 -1
- package/dist/dom/index.d.ts +1 -1
- package/dist/dom/render.js +26 -15
- package/dist/dom/view.d.ts +59 -6
- package/dist/dom/view.js +195 -38
- package/dist/edit.d.ts +6 -0
- package/dist/edit.js +101 -11
- package/dist/engine.d.ts +26 -4
- package/dist/engine.js +113 -23
- package/dist/export.d.ts +2 -2
- package/dist/export.js +22 -6
- package/dist/footnotes/index.d.ts +2 -0
- package/dist/footnotes/index.js +2 -0
- package/dist/footnotes/plugin.d.ts +6 -0
- package/dist/footnotes/plugin.js +130 -0
- package/dist/footnotes/reconcile.d.ts +4 -0
- package/dist/footnotes/reconcile.js +155 -0
- package/dist/html/parse.d.ts +8 -2
- package/dist/html/parse.js +6 -2
- package/dist/images/images.js +4 -3
- package/dist/images/index.d.ts +5 -1
- package/dist/images/index.js +3 -1
- package/dist/images/plugin.js +2 -1
- package/dist/{dom/imageview.d.ts → images/view.d.ts} +9 -6
- package/dist/{dom/imageview.js → images/view.js} +20 -14
- package/dist/import.d.ts +18 -2
- package/dist/import.js +87 -42
- package/dist/index.d.ts +4 -1
- package/dist/index.js +3 -1
- package/dist/lists/lists.js +55 -30
- package/dist/markdown/detect.d.ts +1 -0
- package/dist/markdown/detect.js +81 -0
- package/dist/markdown/export.d.ts +10 -0
- package/dist/markdown/export.js +252 -0
- package/dist/markdown/index.d.ts +5 -0
- package/dist/markdown/index.js +4 -0
- package/dist/markdown/inline.d.ts +37 -0
- package/dist/markdown/inline.js +604 -0
- package/dist/markdown/parse.d.ts +4 -0
- package/dist/markdown/parse.js +586 -0
- package/dist/markdown/plugin.d.ts +20 -0
- package/dist/markdown/plugin.js +56 -0
- package/dist/model.d.ts +30 -2
- package/dist/model.js +2 -0
- package/dist/notes.d.ts +6 -0
- package/dist/notes.js +49 -0
- package/dist/ops.d.ts +6 -1
- package/dist/ops.js +5 -4
- package/dist/paste.js +9 -2
- package/dist/path.d.ts +10 -1
- package/dist/path.js +110 -11
- package/dist/plugin.d.ts +53 -2
- package/dist/policy.d.ts +41 -0
- package/dist/policy.js +40 -0
- package/dist/standard/index.d.ts +5 -3
- package/dist/standard/index.js +6 -5
- package/dist/stats.js +1 -1
- package/dist/svg/svg.js +9 -1
- package/dist/{dom/tableview.d.ts → tables/cells.d.ts} +7 -6
- package/dist/{dom/tableview.js → tables/cells.js} +16 -12
- package/dist/{dom/tablegrips.d.ts → tables/grips.d.ts} +5 -4
- package/dist/{dom/tablegrips.js → tables/grips.js} +10 -9
- package/dist/tables/index.d.ts +5 -1
- package/dist/tables/index.js +3 -1
- package/dist/tables/view.d.ts +2 -0
- package/dist/tables/view.js +15 -0
- package/dist/warning.d.ts +16 -0
- package/dist/warning.js +5 -0
- package/package.json +23 -1
|
@@ -0,0 +1,586 @@
|
|
|
1
|
+
import { NOTE_ID } from "../model.js";
|
|
2
|
+
import { noteIds } from "../notes.js";
|
|
3
|
+
import { asChildren } from "../path.js";
|
|
4
|
+
import { NOTE_LABEL, parseInline, refLabel, unescape } from "./inline.js";
|
|
5
|
+
const MAX_DEPTH = 24;
|
|
6
|
+
const leading = (line) => {
|
|
7
|
+
let n = 0;
|
|
8
|
+
while (n < line.length && line.charCodeAt(n) === 32)
|
|
9
|
+
n++;
|
|
10
|
+
return n;
|
|
11
|
+
};
|
|
12
|
+
const isBlank = (line) => line.trim() === "";
|
|
13
|
+
function expandTabs(line) {
|
|
14
|
+
if (!line.includes("\t"))
|
|
15
|
+
return line;
|
|
16
|
+
let col = 0, i = 0, out = "";
|
|
17
|
+
for (; i < line.length; i++) {
|
|
18
|
+
const c = line[i];
|
|
19
|
+
if (c === " ") {
|
|
20
|
+
out += c;
|
|
21
|
+
col++;
|
|
22
|
+
}
|
|
23
|
+
else if (c === "\t") {
|
|
24
|
+
const n = 4 - (col % 4);
|
|
25
|
+
out += " ".repeat(n);
|
|
26
|
+
col += n;
|
|
27
|
+
}
|
|
28
|
+
else
|
|
29
|
+
break;
|
|
30
|
+
}
|
|
31
|
+
return out + line.slice(i);
|
|
32
|
+
}
|
|
33
|
+
function isFence(line) {
|
|
34
|
+
const indent = leading(line);
|
|
35
|
+
if (indent > 3)
|
|
36
|
+
return undefined;
|
|
37
|
+
const ch = line[indent];
|
|
38
|
+
if (ch !== "`" && ch !== "~")
|
|
39
|
+
return undefined;
|
|
40
|
+
let len = 0;
|
|
41
|
+
while (line[indent + len] === ch)
|
|
42
|
+
len++;
|
|
43
|
+
if (len < 3)
|
|
44
|
+
return undefined;
|
|
45
|
+
const info = line.slice(indent + len);
|
|
46
|
+
if (ch === "`" && info.includes("`"))
|
|
47
|
+
return undefined;
|
|
48
|
+
return { ch, len, indent, info: info.trim() };
|
|
49
|
+
}
|
|
50
|
+
function isRule(line) {
|
|
51
|
+
const indent = leading(line);
|
|
52
|
+
if (indent > 3)
|
|
53
|
+
return false;
|
|
54
|
+
const ch = line[indent];
|
|
55
|
+
if (ch !== "*" && ch !== "-" && ch !== "_")
|
|
56
|
+
return false;
|
|
57
|
+
let count = 0;
|
|
58
|
+
for (let i = indent; i < line.length; i++) {
|
|
59
|
+
const c = line[i];
|
|
60
|
+
if (c === ch)
|
|
61
|
+
count++;
|
|
62
|
+
else if (c !== " " && c !== "\t")
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
return count >= 3;
|
|
66
|
+
}
|
|
67
|
+
function atx(line) {
|
|
68
|
+
const indent = leading(line);
|
|
69
|
+
if (indent > 3)
|
|
70
|
+
return undefined;
|
|
71
|
+
let level = 0;
|
|
72
|
+
while (line[indent + level] === "#")
|
|
73
|
+
level++;
|
|
74
|
+
if (level < 1 || level > 6)
|
|
75
|
+
return undefined;
|
|
76
|
+
const rest = line.slice(indent + level);
|
|
77
|
+
if (rest !== "" && rest[0] !== " " && rest[0] !== "\t")
|
|
78
|
+
return undefined;
|
|
79
|
+
let text = rest.trim();
|
|
80
|
+
let end = text.length;
|
|
81
|
+
while (end > 0 && text[end - 1] === "#")
|
|
82
|
+
end--;
|
|
83
|
+
if (end < text.length && (end === 0 || text[end - 1] === " " || text[end - 1] === "\t"))
|
|
84
|
+
text = text.slice(0, end).trimEnd();
|
|
85
|
+
return { level: level, text };
|
|
86
|
+
}
|
|
87
|
+
function marker(line) {
|
|
88
|
+
const indent = leading(line);
|
|
89
|
+
if (indent > 3)
|
|
90
|
+
return undefined;
|
|
91
|
+
let p = indent;
|
|
92
|
+
let type;
|
|
93
|
+
let char;
|
|
94
|
+
let number = 0;
|
|
95
|
+
const c = line[p];
|
|
96
|
+
if (c === "-" || c === "+" || c === "*") {
|
|
97
|
+
type = "ul";
|
|
98
|
+
char = c;
|
|
99
|
+
p++;
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
let d = 0;
|
|
103
|
+
while (p < line.length && line.charCodeAt(p) >= 48 && line.charCodeAt(p) <= 57 && d < 10) {
|
|
104
|
+
number = number * 10 + (line.charCodeAt(p) - 48);
|
|
105
|
+
p++;
|
|
106
|
+
d++;
|
|
107
|
+
}
|
|
108
|
+
if (d === 0 || d > 9)
|
|
109
|
+
return undefined;
|
|
110
|
+
const delim = line[p];
|
|
111
|
+
if (delim !== "." && delim !== ")")
|
|
112
|
+
return undefined;
|
|
113
|
+
type = "ol";
|
|
114
|
+
char = delim;
|
|
115
|
+
p++;
|
|
116
|
+
}
|
|
117
|
+
const width = p - indent;
|
|
118
|
+
const after = line[p];
|
|
119
|
+
if (after !== undefined && after !== " " && after !== "\t")
|
|
120
|
+
return undefined;
|
|
121
|
+
const rest = line.slice(p);
|
|
122
|
+
const empty = rest.trim() === "";
|
|
123
|
+
const spaces = leading(rest);
|
|
124
|
+
const gap = empty || spaces > 4 ? 1 : spaces;
|
|
125
|
+
return { type, char, number, indent, width, contentIndent: indent + width + gap, empty };
|
|
126
|
+
}
|
|
127
|
+
function interrupts(line, next) {
|
|
128
|
+
if (atx(line) || isFence(line) || isRule(line))
|
|
129
|
+
return true;
|
|
130
|
+
if (leading(line) <= 3 && line.trimStart().startsWith(">"))
|
|
131
|
+
return true;
|
|
132
|
+
const m = marker(line);
|
|
133
|
+
if (m && !m.empty && (m.type === "ul" || m.number === 1))
|
|
134
|
+
return true;
|
|
135
|
+
return next !== undefined && tableAt(line, next) !== undefined;
|
|
136
|
+
}
|
|
137
|
+
function setextLevel(line) {
|
|
138
|
+
const indent = leading(line);
|
|
139
|
+
if (indent > 3)
|
|
140
|
+
return undefined;
|
|
141
|
+
const ch = line[indent];
|
|
142
|
+
if (ch !== "=" && ch !== "-")
|
|
143
|
+
return undefined;
|
|
144
|
+
let i = indent;
|
|
145
|
+
while (line[i] === ch)
|
|
146
|
+
i++;
|
|
147
|
+
if (line.slice(i).trim() !== "")
|
|
148
|
+
return undefined;
|
|
149
|
+
return ch === "=" ? 1 : 2;
|
|
150
|
+
}
|
|
151
|
+
function cellsOf(line) {
|
|
152
|
+
let text = line.trim();
|
|
153
|
+
if (text.startsWith("|"))
|
|
154
|
+
text = text.slice(1);
|
|
155
|
+
if (text.endsWith("|") && !text.endsWith("\\|"))
|
|
156
|
+
text = text.slice(0, -1);
|
|
157
|
+
const cells = [];
|
|
158
|
+
let cell = "";
|
|
159
|
+
for (let i = 0; i < text.length; i++) {
|
|
160
|
+
const c = text[i];
|
|
161
|
+
if (c === "\\" && text[i + 1] === "|") {
|
|
162
|
+
cell += "\\|";
|
|
163
|
+
i++;
|
|
164
|
+
}
|
|
165
|
+
else if (c === "|") {
|
|
166
|
+
cells.push(cell.trim());
|
|
167
|
+
cell = "";
|
|
168
|
+
}
|
|
169
|
+
else
|
|
170
|
+
cell += c;
|
|
171
|
+
}
|
|
172
|
+
cells.push(cell.trim());
|
|
173
|
+
return cells;
|
|
174
|
+
}
|
|
175
|
+
function alignOf(cell) {
|
|
176
|
+
const t = cell.trim();
|
|
177
|
+
if (t.length === 0)
|
|
178
|
+
return null;
|
|
179
|
+
const left = t.startsWith(":"), right = t.endsWith(":");
|
|
180
|
+
const dashes = t.slice(left ? 1 : 0, right ? -1 : undefined);
|
|
181
|
+
if (dashes === "" || /[^-]/.test(dashes))
|
|
182
|
+
return null;
|
|
183
|
+
return left && right ? "center" : right ? "right" : left ? "left" : undefined;
|
|
184
|
+
}
|
|
185
|
+
function tableAt(header, delimiter) {
|
|
186
|
+
if (!header.includes("|") || !delimiter.includes("-"))
|
|
187
|
+
return undefined;
|
|
188
|
+
const head = cellsOf(header);
|
|
189
|
+
const marks = cellsOf(delimiter);
|
|
190
|
+
if (head.length !== marks.length || head.length < 1)
|
|
191
|
+
return undefined;
|
|
192
|
+
const aligns = [];
|
|
193
|
+
for (const m of marks) {
|
|
194
|
+
const a = alignOf(m);
|
|
195
|
+
if (a === null)
|
|
196
|
+
return undefined;
|
|
197
|
+
aligns.push(a);
|
|
198
|
+
}
|
|
199
|
+
if (head.length === 1 && !header.trim().startsWith("|") && !header.trim().endsWith("|"))
|
|
200
|
+
return undefined;
|
|
201
|
+
return { head, aligns };
|
|
202
|
+
}
|
|
203
|
+
class FenceState {
|
|
204
|
+
open = null;
|
|
205
|
+
feed(line) {
|
|
206
|
+
const f = isFence(line);
|
|
207
|
+
if (this.open) {
|
|
208
|
+
if (f && f.ch === this.open.ch && f.len >= this.open.len && f.info === "")
|
|
209
|
+
this.open = null;
|
|
210
|
+
}
|
|
211
|
+
else if (f)
|
|
212
|
+
this.open = { ch: f.ch, len: f.len };
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
const continuesParagraph = (previous, fence) => previous !== undefined && fence.open === null && !isBlank(previous) && !atx(previous) && !isRule(previous) && !setextLevel(previous) && leading(previous) < 4;
|
|
216
|
+
function refDef(line) {
|
|
217
|
+
const indent = leading(line);
|
|
218
|
+
if (indent > 3 || line[indent] !== "[")
|
|
219
|
+
return undefined;
|
|
220
|
+
let i = indent + 1;
|
|
221
|
+
const from = i;
|
|
222
|
+
while (i < line.length && line[i] !== "]" && line[i] !== "[" && i - from < 1000) {
|
|
223
|
+
if (line[i] === "\\")
|
|
224
|
+
i++;
|
|
225
|
+
i++;
|
|
226
|
+
}
|
|
227
|
+
if (line[i] !== "]" || line[i + 1] !== ":")
|
|
228
|
+
return undefined;
|
|
229
|
+
const label = line.slice(from, i);
|
|
230
|
+
if (label.trim() === "")
|
|
231
|
+
return undefined;
|
|
232
|
+
i += 2;
|
|
233
|
+
while (line[i] === " " || line[i] === "\t")
|
|
234
|
+
i++;
|
|
235
|
+
let dest;
|
|
236
|
+
if (line[i] === "<") {
|
|
237
|
+
const end = line.indexOf(">", i + 1);
|
|
238
|
+
if (end === -1 || /[\s<]/.test(line.slice(i + 1, end)))
|
|
239
|
+
return undefined;
|
|
240
|
+
dest = line.slice(i + 1, end);
|
|
241
|
+
i = end + 1;
|
|
242
|
+
}
|
|
243
|
+
else {
|
|
244
|
+
const start = i;
|
|
245
|
+
while (i < line.length && line[i] !== " " && line[i] !== "\t")
|
|
246
|
+
i++;
|
|
247
|
+
dest = line.slice(start, i);
|
|
248
|
+
}
|
|
249
|
+
if (dest === "")
|
|
250
|
+
return undefined;
|
|
251
|
+
while (line[i] === " " || line[i] === "\t")
|
|
252
|
+
i++;
|
|
253
|
+
if (i < line.length) {
|
|
254
|
+
const open = line[i];
|
|
255
|
+
const close = open === "(" ? ")" : open;
|
|
256
|
+
if ((open !== '"' && open !== "'" && open !== "(") || line[i - 1] === undefined || i === 0 || (line[i - 1] !== " " && line[i - 1] !== "\t"))
|
|
257
|
+
return undefined;
|
|
258
|
+
const end = line.lastIndexOf(close);
|
|
259
|
+
if (end <= i || line.slice(end + 1).trim() !== "")
|
|
260
|
+
return undefined;
|
|
261
|
+
}
|
|
262
|
+
return { label, dest: unescape(dest) };
|
|
263
|
+
}
|
|
264
|
+
function collectRefs(lines) {
|
|
265
|
+
const refs = new Map();
|
|
266
|
+
const keys = new Map();
|
|
267
|
+
const fence = new FenceState();
|
|
268
|
+
let inParagraph = false;
|
|
269
|
+
for (const raw of lines) {
|
|
270
|
+
let line = raw;
|
|
271
|
+
for (;;) {
|
|
272
|
+
const quoted = leading(line) <= 3 && line.trimStart().startsWith(">");
|
|
273
|
+
if (!quoted)
|
|
274
|
+
break;
|
|
275
|
+
line = line.trimStart().slice(1);
|
|
276
|
+
if (line.startsWith(" "))
|
|
277
|
+
line = line.slice(1);
|
|
278
|
+
}
|
|
279
|
+
fence.feed(line);
|
|
280
|
+
if (fence.open) {
|
|
281
|
+
inParagraph = false;
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
284
|
+
const note = inParagraph || quotedLine(raw) ? undefined : noteDef(line);
|
|
285
|
+
if (note) {
|
|
286
|
+
const key = refLabel(note.label);
|
|
287
|
+
if (!keys.has(key))
|
|
288
|
+
keys.set(key, noteIdFor(note.label, keys));
|
|
289
|
+
inParagraph = false;
|
|
290
|
+
continue;
|
|
291
|
+
}
|
|
292
|
+
const def = inParagraph ? undefined : refDef(line);
|
|
293
|
+
if (def) {
|
|
294
|
+
const key = refLabel(def.label);
|
|
295
|
+
if (!refs.has(key))
|
|
296
|
+
refs.set(key, { dest: def.dest });
|
|
297
|
+
}
|
|
298
|
+
inParagraph = !def && continuesParagraph(line, fence);
|
|
299
|
+
}
|
|
300
|
+
return { refs, keys };
|
|
301
|
+
}
|
|
302
|
+
const quotedLine = (line) => leading(line) <= 3 && line.trimStart().startsWith(">");
|
|
303
|
+
function noteDef(line) {
|
|
304
|
+
const m = /^ {0,3}\[\^([^\]\s[]{1,64})\]:[ \t]*(.*)$/.exec(line);
|
|
305
|
+
return m && NOTE_LABEL.test(m[1]) ? { label: m[1], rest: m[2] } : undefined;
|
|
306
|
+
}
|
|
307
|
+
function noteIdFor(label, taken) {
|
|
308
|
+
const base = label.replace(/[^\w-]/g, "").slice(0, 32);
|
|
309
|
+
const used = new Set(taken.values());
|
|
310
|
+
for (let n = 0;; n++) {
|
|
311
|
+
const id = base !== "" && n === 0 ? base : `n${taken.size + n + 1}`;
|
|
312
|
+
if (NOTE_ID.test(id) && !used.has(id))
|
|
313
|
+
return id;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
function paragraph(text, options, align) {
|
|
317
|
+
const content = parseInline(text.trim(), options);
|
|
318
|
+
if (content.length === 0)
|
|
319
|
+
return undefined;
|
|
320
|
+
return { type: "p", content, ...(align ? { align } : {}) };
|
|
321
|
+
}
|
|
322
|
+
function listItem(lines, depth, options) {
|
|
323
|
+
let checked;
|
|
324
|
+
const first = lines[0] ?? "";
|
|
325
|
+
const task = /^\[([ xX])\](?:[ \t]|$)/.exec(first);
|
|
326
|
+
if (task) {
|
|
327
|
+
checked = task[1] !== " ";
|
|
328
|
+
lines = [first.slice(task[0].length), ...lines.slice(1)];
|
|
329
|
+
}
|
|
330
|
+
return { ...itemOf(parseBlocks(lines, depth + 1, options)), ...(checked !== undefined ? { checked } : {}) };
|
|
331
|
+
}
|
|
332
|
+
function itemOf(blocks) {
|
|
333
|
+
let content = [];
|
|
334
|
+
const children = [];
|
|
335
|
+
let leading = true;
|
|
336
|
+
blocks.forEach((b, i) => {
|
|
337
|
+
if (leading && b.type === "p" && (i === 0 || content.length > 0)) {
|
|
338
|
+
content = i === 0 ? [...b.content] : [...content, { type: "br", paragraph: true }, ...b.content];
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
leading = false;
|
|
342
|
+
children.push(...asChildren([b]));
|
|
343
|
+
});
|
|
344
|
+
return { content, children };
|
|
345
|
+
}
|
|
346
|
+
function parseList(lines, start, depth, options) {
|
|
347
|
+
const first = marker(lines[start]);
|
|
348
|
+
const items = [];
|
|
349
|
+
let i = start;
|
|
350
|
+
while (i < lines.length) {
|
|
351
|
+
const m = marker(lines[i]);
|
|
352
|
+
if (!m || m.type !== first.type || m.char !== first.char)
|
|
353
|
+
break;
|
|
354
|
+
if (isRule(lines[i]))
|
|
355
|
+
break;
|
|
356
|
+
const body = [lines[i].slice(m.contentIndent < lines[i].length ? m.contentIndent : lines[i].length)];
|
|
357
|
+
if (m.empty)
|
|
358
|
+
body[0] = "";
|
|
359
|
+
i++;
|
|
360
|
+
let blanks = 0;
|
|
361
|
+
const fenced = new FenceState();
|
|
362
|
+
fenced.feed(body[0] ?? "");
|
|
363
|
+
while (i < lines.length) {
|
|
364
|
+
const line = lines[i];
|
|
365
|
+
if (isBlank(line)) {
|
|
366
|
+
blanks++;
|
|
367
|
+
i++;
|
|
368
|
+
continue;
|
|
369
|
+
}
|
|
370
|
+
const ind = leading(line);
|
|
371
|
+
if (ind >= m.contentIndent) {
|
|
372
|
+
for (let b = 0; b < blanks; b++)
|
|
373
|
+
body.push("");
|
|
374
|
+
blanks = 0;
|
|
375
|
+
body.push(line.slice(m.contentIndent));
|
|
376
|
+
fenced.feed(line.slice(m.contentIndent));
|
|
377
|
+
i++;
|
|
378
|
+
continue;
|
|
379
|
+
}
|
|
380
|
+
if (blanks === 0 && !marker(line) && !interrupts(line, lines[i + 1]) && continuesParagraph(body[body.length - 1], fenced)) {
|
|
381
|
+
body.push(line.trimStart());
|
|
382
|
+
i++;
|
|
383
|
+
continue;
|
|
384
|
+
}
|
|
385
|
+
break;
|
|
386
|
+
}
|
|
387
|
+
items.push(listItem(body, depth, options));
|
|
388
|
+
const nextMarker = i < lines.length ? marker(lines[i]) : undefined;
|
|
389
|
+
if (!nextMarker || nextMarker.type !== first.type || nextMarker.char !== first.char)
|
|
390
|
+
break;
|
|
391
|
+
}
|
|
392
|
+
const list = { type: first.type, items, ...(first.type === "ol" && first.number !== 1 ? { start: first.number } : {}) };
|
|
393
|
+
return { list, next: i };
|
|
394
|
+
}
|
|
395
|
+
function parseBlocks(lines, depth, options) {
|
|
396
|
+
const out = [];
|
|
397
|
+
if (depth >= MAX_DEPTH) {
|
|
398
|
+
const p = paragraph(lines.join("\n"), options);
|
|
399
|
+
return p ? [p] : [];
|
|
400
|
+
}
|
|
401
|
+
let i = 0;
|
|
402
|
+
while (i < lines.length) {
|
|
403
|
+
const line = lines[i];
|
|
404
|
+
if (isBlank(line)) {
|
|
405
|
+
i++;
|
|
406
|
+
continue;
|
|
407
|
+
}
|
|
408
|
+
const fence = isFence(line);
|
|
409
|
+
if (fence) {
|
|
410
|
+
const body = [];
|
|
411
|
+
i++;
|
|
412
|
+
while (i < lines.length) {
|
|
413
|
+
const l = lines[i];
|
|
414
|
+
const close = isFence(l);
|
|
415
|
+
if (close && close.ch === fence.ch && close.len >= fence.len && close.info === "") {
|
|
416
|
+
i++;
|
|
417
|
+
break;
|
|
418
|
+
}
|
|
419
|
+
body.push(l.slice(Math.min(leading(l), fence.indent)));
|
|
420
|
+
i++;
|
|
421
|
+
}
|
|
422
|
+
out.push({ type: "pre", text: body.join("\n") });
|
|
423
|
+
continue;
|
|
424
|
+
}
|
|
425
|
+
const heading = atx(line);
|
|
426
|
+
if (heading) {
|
|
427
|
+
const content = parseInline(heading.text, options);
|
|
428
|
+
out.push({ type: "h", level: heading.level, content });
|
|
429
|
+
i++;
|
|
430
|
+
continue;
|
|
431
|
+
}
|
|
432
|
+
if (isRule(line)) {
|
|
433
|
+
out.push({ type: "hr" });
|
|
434
|
+
i++;
|
|
435
|
+
continue;
|
|
436
|
+
}
|
|
437
|
+
if (leading(line) <= 3 && line.trimStart().startsWith(">")) {
|
|
438
|
+
const inner = [];
|
|
439
|
+
const fenced = new FenceState();
|
|
440
|
+
while (i < lines.length) {
|
|
441
|
+
const l = lines[i];
|
|
442
|
+
if (leading(l) <= 3 && l.trimStart().startsWith(">")) {
|
|
443
|
+
const t = l.trimStart().slice(1);
|
|
444
|
+
const content = t.startsWith(" ") ? t.slice(1) : t;
|
|
445
|
+
inner.push(content);
|
|
446
|
+
fenced.feed(content);
|
|
447
|
+
i++;
|
|
448
|
+
}
|
|
449
|
+
else if (!isBlank(l) && !interrupts(l, lines[i + 1]) && continuesParagraph(inner[inner.length - 1], fenced)) {
|
|
450
|
+
inner.push(l.trimStart());
|
|
451
|
+
i++;
|
|
452
|
+
}
|
|
453
|
+
else
|
|
454
|
+
break;
|
|
455
|
+
}
|
|
456
|
+
const quoted = parseBlocks(inner, depth + 1, options);
|
|
457
|
+
if (quoted.length > 0)
|
|
458
|
+
out.push({ type: "quote", blocks: quoted });
|
|
459
|
+
continue;
|
|
460
|
+
}
|
|
461
|
+
const m = marker(line);
|
|
462
|
+
if (m) {
|
|
463
|
+
const { list, next } = parseList(lines, i, depth, options);
|
|
464
|
+
out.push(list);
|
|
465
|
+
i = next;
|
|
466
|
+
continue;
|
|
467
|
+
}
|
|
468
|
+
const table = i + 1 < lines.length ? tableAt(line, lines[i + 1]) : undefined;
|
|
469
|
+
if (table) {
|
|
470
|
+
const cell = (text, header, col) => {
|
|
471
|
+
const p = paragraph(text, options, table.aligns[col]);
|
|
472
|
+
return { header, colspan: 1, rowspan: 1, blocks: p ? [p] : [{ type: "p", content: [] }] };
|
|
473
|
+
};
|
|
474
|
+
const rows = [table.head.map((t, c) => cell(t, true, c))];
|
|
475
|
+
i += 2;
|
|
476
|
+
while (i < lines.length && !isBlank(lines[i]) && !interrupts(lines[i], undefined)) {
|
|
477
|
+
const cells = cellsOf(lines[i]);
|
|
478
|
+
rows.push(table.head.map((_, c) => cell(cells[c] ?? "", false, c)));
|
|
479
|
+
i++;
|
|
480
|
+
}
|
|
481
|
+
out.push({ type: "table", rows });
|
|
482
|
+
continue;
|
|
483
|
+
}
|
|
484
|
+
const note = options.notes ? noteDef(line) : undefined;
|
|
485
|
+
const key = note ? refLabel(note.label) : "";
|
|
486
|
+
if (note && options.notes?.keys.has(key)) {
|
|
487
|
+
const body = [note.rest];
|
|
488
|
+
const fenced = new FenceState();
|
|
489
|
+
fenced.feed(note.rest);
|
|
490
|
+
i++;
|
|
491
|
+
while (i < lines.length) {
|
|
492
|
+
const l = lines[i];
|
|
493
|
+
if (isBlank(l)) {
|
|
494
|
+
let j = i;
|
|
495
|
+
while (j < lines.length && isBlank(lines[j]))
|
|
496
|
+
j++;
|
|
497
|
+
if (j >= lines.length || leading(lines[j]) < 4)
|
|
498
|
+
break;
|
|
499
|
+
for (; i < j; i++)
|
|
500
|
+
body.push("");
|
|
501
|
+
continue;
|
|
502
|
+
}
|
|
503
|
+
if (leading(l) >= 4) {
|
|
504
|
+
body.push(l.slice(4));
|
|
505
|
+
fenced.feed(l.slice(4));
|
|
506
|
+
i++;
|
|
507
|
+
continue;
|
|
508
|
+
}
|
|
509
|
+
if (!noteDef(l) && !interrupts(l, lines[i + 1]) && continuesParagraph(body[body.length - 1], fenced)) {
|
|
510
|
+
body.push(l.trimStart());
|
|
511
|
+
i++;
|
|
512
|
+
continue;
|
|
513
|
+
}
|
|
514
|
+
break;
|
|
515
|
+
}
|
|
516
|
+
if (!options.notes.items.has(key))
|
|
517
|
+
options.notes.items.set(key, itemOf(parseBlocks(body, depth + 1, { ...options, notes: undefined })));
|
|
518
|
+
continue;
|
|
519
|
+
}
|
|
520
|
+
if (leading(line) < 4 && refDef(line)) {
|
|
521
|
+
i++;
|
|
522
|
+
continue;
|
|
523
|
+
}
|
|
524
|
+
if (leading(line) >= 4) {
|
|
525
|
+
const body = [];
|
|
526
|
+
while (i < lines.length && (isBlank(lines[i]) || leading(lines[i]) >= 4)) {
|
|
527
|
+
body.push(lines[i].slice(Math.min(4, leading(lines[i]))));
|
|
528
|
+
i++;
|
|
529
|
+
}
|
|
530
|
+
while (body.length > 0 && isBlank(body[body.length - 1]))
|
|
531
|
+
body.pop();
|
|
532
|
+
out.push({ type: "pre", text: body.join("\n") });
|
|
533
|
+
continue;
|
|
534
|
+
}
|
|
535
|
+
const text = [line];
|
|
536
|
+
i++;
|
|
537
|
+
let level;
|
|
538
|
+
while (i < lines.length) {
|
|
539
|
+
const l = lines[i];
|
|
540
|
+
level = setextLevel(l);
|
|
541
|
+
if (level) {
|
|
542
|
+
i++;
|
|
543
|
+
break;
|
|
544
|
+
}
|
|
545
|
+
if (isBlank(l) || interrupts(l, lines[i + 1]))
|
|
546
|
+
break;
|
|
547
|
+
text.push(l);
|
|
548
|
+
i++;
|
|
549
|
+
}
|
|
550
|
+
if (level) {
|
|
551
|
+
out.push({ type: "h", level, content: parseInline(text.map((t) => t.trim()).join("\n"), options) });
|
|
552
|
+
}
|
|
553
|
+
else {
|
|
554
|
+
const p = paragraph(text.map((t) => t.trimStart()).join("\n"), options);
|
|
555
|
+
if (p)
|
|
556
|
+
out.push(p);
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
return out;
|
|
560
|
+
}
|
|
561
|
+
export function parseMarkdown(source, options = {}) {
|
|
562
|
+
const text = source.replace(/\r\n?|\u2028|\u2029/g, "\n").replace(/\0/g, "�");
|
|
563
|
+
const lines = text.split("\n").map(expandTabs);
|
|
564
|
+
const { refs, keys } = collectRefs(lines);
|
|
565
|
+
const used = new Map();
|
|
566
|
+
const items = new Map();
|
|
567
|
+
const count = new Map();
|
|
568
|
+
const notes = {
|
|
569
|
+
keys, items, used,
|
|
570
|
+
use(key) {
|
|
571
|
+
const base = keys.get(key);
|
|
572
|
+
if (base === undefined)
|
|
573
|
+
return undefined;
|
|
574
|
+
const n = (count.get(key) ?? 0) + 1;
|
|
575
|
+
count.set(key, n);
|
|
576
|
+
const id = n === 1 ? base : `${base.slice(0, 34)}-${n}`;
|
|
577
|
+
used.set(id, key);
|
|
578
|
+
return id;
|
|
579
|
+
},
|
|
580
|
+
};
|
|
581
|
+
const blocks = parseBlocks(lines, 0, { ...options, refs, notes: keys.size > 0 ? notes : undefined });
|
|
582
|
+
if (used.size === 0)
|
|
583
|
+
return blocks;
|
|
584
|
+
const order = noteIds({ blocks });
|
|
585
|
+
return [...blocks, { type: "ol", notes: true, items: order.map((id) => ({ ...(items.get(used.get(id)) ?? { content: [], children: [] }), id })) }];
|
|
586
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { Plugin } from "../plugin.ts";
|
|
2
|
+
export interface MarkdownPasteOptions {
|
|
3
|
+
/**
|
|
4
|
+
* `detect` (default): plain text that clearly looks like Markdown is read as Markdown; anything else pastes as typed.
|
|
5
|
+
* `always`: every plain-text paste is read as Markdown. `never`: the plugin does nothing.
|
|
6
|
+
*/
|
|
7
|
+
readonly paste?: "detect" | "always" | "never";
|
|
8
|
+
/**
|
|
9
|
+
* What copy, cut and drag write besides the editor's HTML and plain text. `"markdown"` (default) adds the selection as
|
|
10
|
+
* `text/markdown`; `"plain"` also makes the plain text Markdown, for pasting into a text field; `"off"` adds nothing.
|
|
11
|
+
*/
|
|
12
|
+
readonly copy?: "markdown" | "plain" | "off";
|
|
13
|
+
/** Write underline, superscript and subscript as inline HTML when copying (see `MarkdownExportOptions`). Default false. */
|
|
14
|
+
readonly inlineHtml?: boolean;
|
|
15
|
+
/** URL schemes a link may use besides relative and fragment addresses. Default: http, https, mailto, tel. */
|
|
16
|
+
readonly linkSchemes?: readonly string[];
|
|
17
|
+
/** Largest `data:` image address, in characters. */
|
|
18
|
+
readonly maxDataImageLength?: number;
|
|
19
|
+
}
|
|
20
|
+
export declare function markdown(options?: MarkdownPasteOptions): Plugin;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { kindAt, getBlock } from "../path.js";
|
|
2
|
+
import { exportMarkdown } from "./export.js";
|
|
3
|
+
import { parseMarkdown } from "./parse.js";
|
|
4
|
+
import { looksLikeMarkdown } from "./detect.js";
|
|
5
|
+
export function markdown(options = {}) {
|
|
6
|
+
const mode = options.paste ?? "detect";
|
|
7
|
+
const copy = options.copy ?? "markdown";
|
|
8
|
+
return {
|
|
9
|
+
id: "rathkit.markdown",
|
|
10
|
+
api: "1",
|
|
11
|
+
setup(host) {
|
|
12
|
+
if (copy !== "off") {
|
|
13
|
+
host.copyFormat((blocks) => {
|
|
14
|
+
const text = exportMarkdown({ blocks }, { inlineHtml: options.inlineHtml === true });
|
|
15
|
+
return copy === "plain" ? { "text/markdown": text, "text/plain": text } : { "text/markdown": text };
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
host.command({
|
|
19
|
+
name: "markdown.copy", label: "Copy as Markdown", mutates: false,
|
|
20
|
+
run: (ctx) => {
|
|
21
|
+
const clipboard = globalThis.navigator?.clipboard;
|
|
22
|
+
if (!clipboard)
|
|
23
|
+
return false;
|
|
24
|
+
const { doc, selection } = ctx.snapshot;
|
|
25
|
+
const blocks = host.ops.isCollapsed(selection) ? doc.blocks : host.ops.extractSelection(doc, selection);
|
|
26
|
+
clipboard.writeText(exportMarkdown({ blocks }, { inlineHtml: options.inlineHtml === true })).catch((error) => host.editor.report({ code: "copy-failed", plugin: "rathkit.markdown", message: `Copy as Markdown failed: ${error instanceof Error ? error.message : String(error)}`, error }));
|
|
27
|
+
return true;
|
|
28
|
+
},
|
|
29
|
+
});
|
|
30
|
+
if (mode === "never")
|
|
31
|
+
return;
|
|
32
|
+
host.pasteHandler((data) => {
|
|
33
|
+
if (data.html && data.html.trim() !== "")
|
|
34
|
+
return undefined;
|
|
35
|
+
const text = data.text;
|
|
36
|
+
if (!text || text.trim() === "")
|
|
37
|
+
return undefined;
|
|
38
|
+
const { doc, selection } = host.editor.snapshot;
|
|
39
|
+
const path = selection.focus.path;
|
|
40
|
+
if (kindAt(doc, path) === "pre" || getBlock(doc, path)?.type === "pre")
|
|
41
|
+
return undefined;
|
|
42
|
+
if (text.length > (host.editor.importOptions.maxInputLength ?? Infinity))
|
|
43
|
+
return undefined;
|
|
44
|
+
if (mode === "detect" && !looksLikeMarkdown(text))
|
|
45
|
+
return undefined;
|
|
46
|
+
const blocks = parseMarkdown(text, {
|
|
47
|
+
...(options.linkSchemes ? { linkSchemes: options.linkSchemes } : {}),
|
|
48
|
+
...(options.maxDataImageLength !== undefined ? { maxDataImageLength: options.maxDataImageLength } : {}),
|
|
49
|
+
sanitizeSvg: host.editor.importOptions.sanitizeSvg,
|
|
50
|
+
relativeImages: host.editor.importOptions.pasteRelativeImages ?? false,
|
|
51
|
+
});
|
|
52
|
+
return blocks.length > 0 ? blocks : undefined;
|
|
53
|
+
});
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
}
|