@mseep/affine-mcp-server 2.3.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 +22 -0
- package/README.md +270 -0
- package/bin/affine-mcp +5 -0
- package/dist/auth.js +61 -0
- package/dist/cli.js +726 -0
- package/dist/config.js +178 -0
- package/dist/edgeless/layout.js +222 -0
- package/dist/graphqlClient.js +116 -0
- package/dist/httpAuth.js +147 -0
- package/dist/httpDiagnostics.js +38 -0
- package/dist/index.js +209 -0
- package/dist/markdown/parse.js +559 -0
- package/dist/markdown/render.js +227 -0
- package/dist/markdown/types.js +1 -0
- package/dist/oauth.js +154 -0
- package/dist/sse.js +261 -0
- package/dist/toolSurface.js +349 -0
- package/dist/tools/accessTokens.js +45 -0
- package/dist/tools/auth.js +18 -0
- package/dist/tools/blobStorage.js +136 -0
- package/dist/tools/comments.js +104 -0
- package/dist/tools/docs.js +7478 -0
- package/dist/tools/history.js +22 -0
- package/dist/tools/icons.js +125 -0
- package/dist/tools/notifications.js +79 -0
- package/dist/tools/organize.js +1145 -0
- package/dist/tools/properties.js +426 -0
- package/dist/tools/user.js +13 -0
- package/dist/tools/userCRUD.js +77 -0
- package/dist/tools/workspaces.js +322 -0
- package/dist/util/explorerIcon.js +95 -0
- package/dist/util/mcp.js +28 -0
- package/dist/ws.js +113 -0
- package/docs/assets/edgeless-canvas-demo-advanced-dark.png +0 -0
- package/docs/assets/edgeless-canvas-demo-advanced-light.png +0 -0
- package/docs/client-setup.md +174 -0
- package/docs/configuration-and-deployment.md +265 -0
- package/docs/edgeless-canvas-cookbook.md +226 -0
- package/docs/getting-started.md +229 -0
- package/docs/tool-reference.md +200 -0
- package/docs/workflow-recipes.md +147 -0
- package/package.json +118 -0
- package/tool-manifest.json +99 -0
|
@@ -0,0 +1,559 @@
|
|
|
1
|
+
import MarkdownIt from "markdown-it";
|
|
2
|
+
const md = new MarkdownIt({
|
|
3
|
+
html: false,
|
|
4
|
+
linkify: true,
|
|
5
|
+
breaks: false,
|
|
6
|
+
});
|
|
7
|
+
function addWarning(state, warning) {
|
|
8
|
+
if (!state.warningSet.has(warning)) {
|
|
9
|
+
state.warningSet.add(warning);
|
|
10
|
+
state.warnings.push(warning);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
function getAttr(token, name) {
|
|
14
|
+
if (typeof token.attrGet === "function") {
|
|
15
|
+
return token.attrGet(name) ?? "";
|
|
16
|
+
}
|
|
17
|
+
if (!Array.isArray(token.attrs)) {
|
|
18
|
+
return "";
|
|
19
|
+
}
|
|
20
|
+
for (const [key, value] of token.attrs) {
|
|
21
|
+
if (key === name) {
|
|
22
|
+
return value;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return "";
|
|
26
|
+
}
|
|
27
|
+
function findMatchingToken(tokens, start, openType, closeType) {
|
|
28
|
+
let depth = 0;
|
|
29
|
+
for (let i = start; i < tokens.length; i += 1) {
|
|
30
|
+
const token = tokens[i];
|
|
31
|
+
if (token.type === openType) {
|
|
32
|
+
depth += 1;
|
|
33
|
+
}
|
|
34
|
+
else if (token.type === closeType) {
|
|
35
|
+
depth -= 1;
|
|
36
|
+
if (depth === 0) {
|
|
37
|
+
return i;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return -1;
|
|
42
|
+
}
|
|
43
|
+
function findMatchingInline(tokens, start, openType, closeType) {
|
|
44
|
+
let depth = 0;
|
|
45
|
+
for (let i = start; i < tokens.length; i += 1) {
|
|
46
|
+
const token = tokens[i];
|
|
47
|
+
if (token.type === openType) {
|
|
48
|
+
depth += 1;
|
|
49
|
+
}
|
|
50
|
+
else if (token.type === closeType) {
|
|
51
|
+
depth -= 1;
|
|
52
|
+
if (depth === 0) {
|
|
53
|
+
return i;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return -1;
|
|
58
|
+
}
|
|
59
|
+
function deltaToString(deltas) {
|
|
60
|
+
return deltas.map(delta => delta.insert).join("");
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Strip deltas corresponding to the first line (up to and including the first
|
|
64
|
+
* "\n" separator). Used by callout parsing to remove the `[!NOTE]` marker
|
|
65
|
+
* line from the collected blockquote deltas.
|
|
66
|
+
*/
|
|
67
|
+
function stripFirstDeltaLine(deltas) {
|
|
68
|
+
const result = [];
|
|
69
|
+
let pastNewline = false;
|
|
70
|
+
for (const delta of deltas) {
|
|
71
|
+
if (pastNewline) {
|
|
72
|
+
result.push(delta);
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
const nlIndex = delta.insert.indexOf("\n");
|
|
76
|
+
if (nlIndex >= 0) {
|
|
77
|
+
pastNewline = true;
|
|
78
|
+
const remainder = delta.insert.slice(nlIndex + 1);
|
|
79
|
+
if (remainder.length > 0) {
|
|
80
|
+
result.push({ ...delta, insert: remainder });
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return result;
|
|
85
|
+
}
|
|
86
|
+
function renderInline(children) {
|
|
87
|
+
function applyAttrs(deltas, attrs) {
|
|
88
|
+
return deltas.map(delta => ({
|
|
89
|
+
insert: delta.insert,
|
|
90
|
+
attributes: { ...delta.attributes, ...attrs },
|
|
91
|
+
}));
|
|
92
|
+
}
|
|
93
|
+
function renderRange(start, end) {
|
|
94
|
+
const output = [];
|
|
95
|
+
for (let i = start; i < end; i += 1) {
|
|
96
|
+
const token = children[i];
|
|
97
|
+
switch (token.type) {
|
|
98
|
+
case "text":
|
|
99
|
+
case "html_inline":
|
|
100
|
+
output.push({ insert: token.content });
|
|
101
|
+
break;
|
|
102
|
+
case "code_inline":
|
|
103
|
+
output.push({ insert: token.content, attributes: { code: true } });
|
|
104
|
+
break;
|
|
105
|
+
case "softbreak":
|
|
106
|
+
output.push({ insert: "\n" });
|
|
107
|
+
break;
|
|
108
|
+
case "hardbreak":
|
|
109
|
+
output.push({ insert: " \n" });
|
|
110
|
+
break;
|
|
111
|
+
case "image": {
|
|
112
|
+
const src = getAttr(token, "src");
|
|
113
|
+
const alt = token.content ?? "";
|
|
114
|
+
output.push({ insert: `` });
|
|
115
|
+
break;
|
|
116
|
+
}
|
|
117
|
+
case "link_open": {
|
|
118
|
+
const close = findMatchingInline(children, i, "link_open", "link_close");
|
|
119
|
+
if (close < 0) {
|
|
120
|
+
break;
|
|
121
|
+
}
|
|
122
|
+
const href = getAttr(token, "href");
|
|
123
|
+
const inner = renderRange(i + 1, close);
|
|
124
|
+
output.push(...applyAttrs(inner.length > 0 ? inner : [{ insert: href }], { link: href }));
|
|
125
|
+
i = close;
|
|
126
|
+
break;
|
|
127
|
+
}
|
|
128
|
+
case "strong_open": {
|
|
129
|
+
const close = findMatchingInline(children, i, "strong_open", "strong_close");
|
|
130
|
+
if (close < 0) {
|
|
131
|
+
break;
|
|
132
|
+
}
|
|
133
|
+
output.push(...applyAttrs(renderRange(i + 1, close), { bold: true }));
|
|
134
|
+
i = close;
|
|
135
|
+
break;
|
|
136
|
+
}
|
|
137
|
+
case "em_open": {
|
|
138
|
+
const close = findMatchingInline(children, i, "em_open", "em_close");
|
|
139
|
+
if (close < 0) {
|
|
140
|
+
break;
|
|
141
|
+
}
|
|
142
|
+
output.push(...applyAttrs(renderRange(i + 1, close), { italic: true }));
|
|
143
|
+
i = close;
|
|
144
|
+
break;
|
|
145
|
+
}
|
|
146
|
+
case "s_open": {
|
|
147
|
+
const close = findMatchingInline(children, i, "s_open", "s_close");
|
|
148
|
+
if (close < 0) {
|
|
149
|
+
break;
|
|
150
|
+
}
|
|
151
|
+
output.push(...applyAttrs(renderRange(i + 1, close), { strike: true }));
|
|
152
|
+
i = close;
|
|
153
|
+
break;
|
|
154
|
+
}
|
|
155
|
+
default:
|
|
156
|
+
break;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return output;
|
|
160
|
+
}
|
|
161
|
+
return renderRange(0, children.length);
|
|
162
|
+
}
|
|
163
|
+
function extractSingleLink(children) {
|
|
164
|
+
const filtered = children.filter(token => {
|
|
165
|
+
if (token.type === "softbreak" || token.type === "hardbreak") {
|
|
166
|
+
return false;
|
|
167
|
+
}
|
|
168
|
+
if (token.type === "text" && token.content.trim() === "") {
|
|
169
|
+
return false;
|
|
170
|
+
}
|
|
171
|
+
return true;
|
|
172
|
+
});
|
|
173
|
+
if (filtered.length < 3) {
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
if (filtered[0].type !== "link_open" || filtered[filtered.length - 1].type !== "link_close") {
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
const href = getAttr(filtered[0], "href");
|
|
180
|
+
if (!href) {
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
const inner = filtered.slice(1, filtered.length - 1);
|
|
184
|
+
const text = deltaToString(renderInline(inner)).trim() || href;
|
|
185
|
+
return { href, text };
|
|
186
|
+
}
|
|
187
|
+
function parseTable(tokens, start, end) {
|
|
188
|
+
const rows = [];
|
|
189
|
+
const rowDeltas = [];
|
|
190
|
+
let i = start;
|
|
191
|
+
while (i < end) {
|
|
192
|
+
const token = tokens[i];
|
|
193
|
+
if (token.type === "tr_open") {
|
|
194
|
+
const trClose = findMatchingToken(tokens, i, "tr_open", "tr_close");
|
|
195
|
+
if (trClose < 0 || trClose > end) {
|
|
196
|
+
break;
|
|
197
|
+
}
|
|
198
|
+
const cells = [];
|
|
199
|
+
const cellDeltas = [];
|
|
200
|
+
let j = i + 1;
|
|
201
|
+
while (j < trClose) {
|
|
202
|
+
const cellOpen = tokens[j];
|
|
203
|
+
if (cellOpen.type === "th_open" || cellOpen.type === "td_open") {
|
|
204
|
+
const cellClose = findMatchingToken(tokens, j, cellOpen.type, cellOpen.type === "th_open" ? "th_close" : "td_close");
|
|
205
|
+
if (cellClose < 0 || cellClose > trClose) {
|
|
206
|
+
break;
|
|
207
|
+
}
|
|
208
|
+
let cellText = "";
|
|
209
|
+
let deltas = [];
|
|
210
|
+
for (let k = j + 1; k < cellClose; k += 1) {
|
|
211
|
+
if (tokens[k].type === "inline") {
|
|
212
|
+
deltas = renderInline(tokens[k].children ?? []);
|
|
213
|
+
cellText = deltaToString(deltas).trim();
|
|
214
|
+
break;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
cells.push(cellText);
|
|
218
|
+
cellDeltas.push(deltas);
|
|
219
|
+
j = cellClose + 1;
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
j += 1;
|
|
223
|
+
}
|
|
224
|
+
rows.push(cells);
|
|
225
|
+
rowDeltas.push(cellDeltas);
|
|
226
|
+
i = trClose + 1;
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
i += 1;
|
|
230
|
+
}
|
|
231
|
+
if (rows.length === 0) {
|
|
232
|
+
return null;
|
|
233
|
+
}
|
|
234
|
+
const columns = rows.reduce((max, row) => Math.max(max, row.length), 0);
|
|
235
|
+
if (columns === 0) {
|
|
236
|
+
return null;
|
|
237
|
+
}
|
|
238
|
+
const tableData = rows.map(row => {
|
|
239
|
+
const normalized = [...row];
|
|
240
|
+
while (normalized.length < columns) {
|
|
241
|
+
normalized.push("");
|
|
242
|
+
}
|
|
243
|
+
return normalized;
|
|
244
|
+
});
|
|
245
|
+
const tableCellDeltas = rowDeltas.map(row => {
|
|
246
|
+
const normalized = [...row];
|
|
247
|
+
while (normalized.length < columns) {
|
|
248
|
+
normalized.push([]);
|
|
249
|
+
}
|
|
250
|
+
return normalized;
|
|
251
|
+
});
|
|
252
|
+
return {
|
|
253
|
+
rows: tableData.length,
|
|
254
|
+
columns,
|
|
255
|
+
tableData,
|
|
256
|
+
tableCellDeltas,
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
function collectQuoteText(tokens, start, end) {
|
|
260
|
+
const lines = [];
|
|
261
|
+
const allDeltas = [];
|
|
262
|
+
let firstLine = true;
|
|
263
|
+
for (let i = start; i < end; i += 1) {
|
|
264
|
+
const token = tokens[i];
|
|
265
|
+
if (token.type === "inline") {
|
|
266
|
+
const lineDeltas = renderInline(token.children ?? []);
|
|
267
|
+
const line = deltaToString(lineDeltas).trim();
|
|
268
|
+
if (line) {
|
|
269
|
+
if (!firstLine) {
|
|
270
|
+
allDeltas.push({ insert: "\n" });
|
|
271
|
+
}
|
|
272
|
+
allDeltas.push(...lineDeltas);
|
|
273
|
+
lines.push(line);
|
|
274
|
+
firstLine = false;
|
|
275
|
+
}
|
|
276
|
+
continue;
|
|
277
|
+
}
|
|
278
|
+
if (token.type === "fence" || token.type === "code_block") {
|
|
279
|
+
const language = (token.info ?? "").trim();
|
|
280
|
+
const codeBody = token.content.replace(/\n$/, "");
|
|
281
|
+
const fenceText = `\`\`\`${language}\n${codeBody}\n\`\`\``;
|
|
282
|
+
if (!firstLine) {
|
|
283
|
+
allDeltas.push({ insert: "\n" });
|
|
284
|
+
}
|
|
285
|
+
allDeltas.push({ insert: fenceText });
|
|
286
|
+
lines.push(fenceText);
|
|
287
|
+
firstLine = false;
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
return { text: lines.join("\n"), deltas: allDeltas };
|
|
292
|
+
}
|
|
293
|
+
function parseCalloutAdmonition(text) {
|
|
294
|
+
const lines = text.split("\n");
|
|
295
|
+
const marker = lines[0]?.trim() ?? "";
|
|
296
|
+
if (!/^\[!(NOTE|TIP|IMPORTANT|WARNING|CAUTION)\]$/i.test(marker)) {
|
|
297
|
+
return null;
|
|
298
|
+
}
|
|
299
|
+
return lines.slice(1).join("\n").trim();
|
|
300
|
+
}
|
|
301
|
+
function parseList(tokens, start, end, defaultStyle, state, depth) {
|
|
302
|
+
const operations = [];
|
|
303
|
+
let i = start;
|
|
304
|
+
while (i < end) {
|
|
305
|
+
const token = tokens[i];
|
|
306
|
+
if (token.type === "list_item_open") {
|
|
307
|
+
const close = findMatchingToken(tokens, i, "list_item_open", "list_item_close");
|
|
308
|
+
if (close < 0 || close > end) {
|
|
309
|
+
state.unsupportedCount += 1;
|
|
310
|
+
addWarning(state, "Malformed markdown list item was ignored.");
|
|
311
|
+
break;
|
|
312
|
+
}
|
|
313
|
+
let itemText = "";
|
|
314
|
+
let itemDeltas = [];
|
|
315
|
+
const nestedOperations = [];
|
|
316
|
+
let hasNestedList = false;
|
|
317
|
+
let cursor = i + 1;
|
|
318
|
+
while (cursor < close) {
|
|
319
|
+
const current = tokens[cursor];
|
|
320
|
+
if (!itemText && current.type === "inline") {
|
|
321
|
+
itemDeltas = renderInline(current.children ?? []);
|
|
322
|
+
itemText = deltaToString(itemDeltas).trim();
|
|
323
|
+
}
|
|
324
|
+
if (current.type === "bullet_list_open" || current.type === "ordered_list_open") {
|
|
325
|
+
const nestedClose = findMatchingToken(tokens, cursor, current.type, current.type === "bullet_list_open" ? "bullet_list_close" : "ordered_list_close");
|
|
326
|
+
if (nestedClose < 0 || nestedClose > close) {
|
|
327
|
+
state.unsupportedCount += 1;
|
|
328
|
+
addWarning(state, "Malformed nested list was ignored.");
|
|
329
|
+
break;
|
|
330
|
+
}
|
|
331
|
+
hasNestedList = true;
|
|
332
|
+
const nestedStyle = current.type === "ordered_list_open" ? "numbered" : "bulleted";
|
|
333
|
+
nestedOperations.push(...parseList(tokens, cursor + 1, nestedClose, nestedStyle, state, depth + 1));
|
|
334
|
+
cursor = nestedClose + 1;
|
|
335
|
+
continue;
|
|
336
|
+
}
|
|
337
|
+
cursor += 1;
|
|
338
|
+
}
|
|
339
|
+
let style = defaultStyle;
|
|
340
|
+
let checked;
|
|
341
|
+
const taskMatch = itemText.match(/^\[(\s|x|X)\]\s+([\s\S]*)$/);
|
|
342
|
+
if (taskMatch) {
|
|
343
|
+
style = "todo";
|
|
344
|
+
checked = taskMatch[1].toLowerCase() === "x";
|
|
345
|
+
itemText = taskMatch[2];
|
|
346
|
+
const prefixLen = deltaToString(itemDeltas).length - itemText.length;
|
|
347
|
+
let remaining = prefixLen;
|
|
348
|
+
const trimmedDeltas = [];
|
|
349
|
+
for (const delta of itemDeltas) {
|
|
350
|
+
if (remaining <= 0) {
|
|
351
|
+
trimmedDeltas.push(delta);
|
|
352
|
+
continue;
|
|
353
|
+
}
|
|
354
|
+
if (remaining >= delta.insert.length) {
|
|
355
|
+
remaining -= delta.insert.length;
|
|
356
|
+
continue;
|
|
357
|
+
}
|
|
358
|
+
trimmedDeltas.push({ ...delta, insert: delta.insert.slice(remaining) });
|
|
359
|
+
remaining = 0;
|
|
360
|
+
}
|
|
361
|
+
itemDeltas = trimmedDeltas;
|
|
362
|
+
}
|
|
363
|
+
operations.push({
|
|
364
|
+
type: "list",
|
|
365
|
+
text: itemText,
|
|
366
|
+
style,
|
|
367
|
+
...(style === "todo" ? { checked: Boolean(checked) } : {}),
|
|
368
|
+
deltas: itemDeltas,
|
|
369
|
+
});
|
|
370
|
+
if (hasNestedList) {
|
|
371
|
+
state.unsupportedCount += 1;
|
|
372
|
+
addWarning(state, "Nested markdown lists were flattened to sequential list items.");
|
|
373
|
+
}
|
|
374
|
+
operations.push(...nestedOperations);
|
|
375
|
+
i = close + 1;
|
|
376
|
+
continue;
|
|
377
|
+
}
|
|
378
|
+
i += 1;
|
|
379
|
+
}
|
|
380
|
+
if (depth > 0 && operations.length > 0) {
|
|
381
|
+
state.unsupportedCount += 1;
|
|
382
|
+
addWarning(state, "List nesting depth was reduced during markdown import.");
|
|
383
|
+
}
|
|
384
|
+
return operations;
|
|
385
|
+
}
|
|
386
|
+
function parseTokens(tokens, start, end, state) {
|
|
387
|
+
let i = start;
|
|
388
|
+
while (i < end) {
|
|
389
|
+
const token = tokens[i];
|
|
390
|
+
switch (token.type) {
|
|
391
|
+
case "heading_open": {
|
|
392
|
+
const close = findMatchingToken(tokens, i, "heading_open", "heading_close");
|
|
393
|
+
if (close < 0 || close >= end) {
|
|
394
|
+
state.unsupportedCount += 1;
|
|
395
|
+
addWarning(state, "Malformed markdown heading was ignored.");
|
|
396
|
+
i += 1;
|
|
397
|
+
break;
|
|
398
|
+
}
|
|
399
|
+
const levelNum = Number((token.tag ?? "h1").replace("h", ""));
|
|
400
|
+
const level = Math.max(1, Math.min(6, levelNum));
|
|
401
|
+
const inline = tokens.slice(i + 1, close).find(inner => inner.type === "inline");
|
|
402
|
+
const headingDeltas = inline ? renderInline(inline.children ?? []) : [];
|
|
403
|
+
const text = deltaToString(headingDeltas).trim();
|
|
404
|
+
state.operations.push({ type: "heading", level, text, deltas: headingDeltas });
|
|
405
|
+
i = close + 1;
|
|
406
|
+
break;
|
|
407
|
+
}
|
|
408
|
+
case "paragraph_open": {
|
|
409
|
+
const close = findMatchingToken(tokens, i, "paragraph_open", "paragraph_close");
|
|
410
|
+
if (close < 0 || close >= end) {
|
|
411
|
+
state.unsupportedCount += 1;
|
|
412
|
+
addWarning(state, "Malformed markdown paragraph was ignored.");
|
|
413
|
+
i += 1;
|
|
414
|
+
break;
|
|
415
|
+
}
|
|
416
|
+
const inline = tokens.slice(i + 1, close).find(inner => inner.type === "inline");
|
|
417
|
+
if (!inline) {
|
|
418
|
+
i = close + 1;
|
|
419
|
+
break;
|
|
420
|
+
}
|
|
421
|
+
const children = inline.children ?? [];
|
|
422
|
+
const singleLink = extractSingleLink(children);
|
|
423
|
+
if (singleLink) {
|
|
424
|
+
state.operations.push({
|
|
425
|
+
type: "bookmark",
|
|
426
|
+
url: singleLink.href,
|
|
427
|
+
caption: singleLink.text,
|
|
428
|
+
});
|
|
429
|
+
i = close + 1;
|
|
430
|
+
break;
|
|
431
|
+
}
|
|
432
|
+
if (children.length === 1 && children[0].type === "image") {
|
|
433
|
+
const imageToken = children[0];
|
|
434
|
+
const src = getAttr(imageToken, "src");
|
|
435
|
+
const alt = imageToken.content || undefined;
|
|
436
|
+
if (src) {
|
|
437
|
+
state.unsupportedCount += 1;
|
|
438
|
+
addWarning(state, "Markdown images were imported as bookmark blocks (external image blobs are not auto-uploaded).");
|
|
439
|
+
state.operations.push({ type: "bookmark", url: src, caption: alt });
|
|
440
|
+
}
|
|
441
|
+
i = close + 1;
|
|
442
|
+
break;
|
|
443
|
+
}
|
|
444
|
+
const paragraphDeltas = renderInline(children);
|
|
445
|
+
const text = deltaToString(paragraphDeltas).trim();
|
|
446
|
+
if (text.length > 0) {
|
|
447
|
+
state.operations.push({ type: "paragraph", text, deltas: paragraphDeltas });
|
|
448
|
+
}
|
|
449
|
+
i = close + 1;
|
|
450
|
+
break;
|
|
451
|
+
}
|
|
452
|
+
case "fence":
|
|
453
|
+
case "code_block": {
|
|
454
|
+
const language = (token.info ?? "").trim() || undefined;
|
|
455
|
+
const code = token.content.replace(/\n$/, "");
|
|
456
|
+
state.operations.push({ type: "code", text: code, language });
|
|
457
|
+
i += 1;
|
|
458
|
+
break;
|
|
459
|
+
}
|
|
460
|
+
case "hr":
|
|
461
|
+
state.operations.push({ type: "divider" });
|
|
462
|
+
i += 1;
|
|
463
|
+
break;
|
|
464
|
+
case "blockquote_open": {
|
|
465
|
+
const close = findMatchingToken(tokens, i, "blockquote_open", "blockquote_close");
|
|
466
|
+
if (close < 0 || close >= end) {
|
|
467
|
+
state.unsupportedCount += 1;
|
|
468
|
+
addWarning(state, "Malformed blockquote was ignored.");
|
|
469
|
+
i += 1;
|
|
470
|
+
break;
|
|
471
|
+
}
|
|
472
|
+
const quoteResult = collectQuoteText(tokens, i + 1, close);
|
|
473
|
+
const quoteText = quoteResult.text.trim();
|
|
474
|
+
const calloutText = parseCalloutAdmonition(quoteText);
|
|
475
|
+
if (calloutText !== null) {
|
|
476
|
+
state.operations.push({ type: "callout", text: calloutText, deltas: stripFirstDeltaLine(quoteResult.deltas) });
|
|
477
|
+
}
|
|
478
|
+
else if (quoteText.length > 0) {
|
|
479
|
+
state.operations.push({ type: "quote", text: quoteText, deltas: quoteResult.deltas });
|
|
480
|
+
}
|
|
481
|
+
i = close + 1;
|
|
482
|
+
break;
|
|
483
|
+
}
|
|
484
|
+
case "bullet_list_open":
|
|
485
|
+
case "ordered_list_open": {
|
|
486
|
+
const close = findMatchingToken(tokens, i, token.type, token.type === "bullet_list_open" ? "bullet_list_close" : "ordered_list_close");
|
|
487
|
+
if (close < 0 || close >= end) {
|
|
488
|
+
state.unsupportedCount += 1;
|
|
489
|
+
addWarning(state, "Malformed markdown list was ignored.");
|
|
490
|
+
i += 1;
|
|
491
|
+
break;
|
|
492
|
+
}
|
|
493
|
+
const style = token.type === "ordered_list_open" ? "numbered" : "bulleted";
|
|
494
|
+
state.operations.push(...parseList(tokens, i + 1, close, style, state, 0));
|
|
495
|
+
i = close + 1;
|
|
496
|
+
break;
|
|
497
|
+
}
|
|
498
|
+
case "table_open": {
|
|
499
|
+
const close = findMatchingToken(tokens, i, "table_open", "table_close");
|
|
500
|
+
if (close < 0 || close >= end) {
|
|
501
|
+
state.unsupportedCount += 1;
|
|
502
|
+
addWarning(state, "Malformed markdown table was ignored.");
|
|
503
|
+
i += 1;
|
|
504
|
+
break;
|
|
505
|
+
}
|
|
506
|
+
const parsedTable = parseTable(tokens, i + 1, close);
|
|
507
|
+
if (!parsedTable) {
|
|
508
|
+
state.unsupportedCount += 1;
|
|
509
|
+
addWarning(state, "Unsupported markdown table structure was ignored.");
|
|
510
|
+
i = close + 1;
|
|
511
|
+
break;
|
|
512
|
+
}
|
|
513
|
+
state.operations.push({
|
|
514
|
+
type: "table",
|
|
515
|
+
rows: parsedTable.rows,
|
|
516
|
+
columns: parsedTable.columns,
|
|
517
|
+
tableData: parsedTable.tableData,
|
|
518
|
+
tableCellDeltas: parsedTable.tableCellDeltas,
|
|
519
|
+
});
|
|
520
|
+
i = close + 1;
|
|
521
|
+
break;
|
|
522
|
+
}
|
|
523
|
+
case "html_block": {
|
|
524
|
+
const raw = token.content.trim();
|
|
525
|
+
if (raw) {
|
|
526
|
+
state.unsupportedCount += 1;
|
|
527
|
+
addWarning(state, "HTML blocks were imported as plain paragraph text.");
|
|
528
|
+
state.operations.push({ type: "paragraph", text: raw });
|
|
529
|
+
}
|
|
530
|
+
i += 1;
|
|
531
|
+
break;
|
|
532
|
+
}
|
|
533
|
+
default:
|
|
534
|
+
i += 1;
|
|
535
|
+
break;
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
export function parseMarkdownToOperations(markdown) {
|
|
540
|
+
const state = {
|
|
541
|
+
operations: [],
|
|
542
|
+
warnings: [],
|
|
543
|
+
warningSet: new Set(),
|
|
544
|
+
unsupportedCount: 0,
|
|
545
|
+
};
|
|
546
|
+
const source = markdown ?? "";
|
|
547
|
+
const tokens = md.parse(source, {});
|
|
548
|
+
parseTokens(tokens, 0, tokens.length, state);
|
|
549
|
+
return {
|
|
550
|
+
operations: state.operations,
|
|
551
|
+
warnings: state.warnings,
|
|
552
|
+
lossy: state.unsupportedCount > 0,
|
|
553
|
+
stats: {
|
|
554
|
+
inputChars: source.length,
|
|
555
|
+
blockCount: state.operations.length,
|
|
556
|
+
unsupportedCount: state.unsupportedCount,
|
|
557
|
+
},
|
|
558
|
+
};
|
|
559
|
+
}
|