@portabletext/markdown 2.0.0 → 2.2.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/README.md +200 -25
- package/dist/index.d.ts +460 -148
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2020 -310
- package/dist/index.js.map +1 -1
- package/package.json +6 -3
package/dist/index.js
CHANGED
|
@@ -1,10 +1,36 @@
|
|
|
1
|
+
import { cleanupEfficiency, makeDiff } from "@sanity/diff-match-patch";
|
|
1
2
|
import { compileSchema, defineSchema, isSpan, isTextBlock, isTypedObject } from "@portabletext/schema";
|
|
2
|
-
import { buildMarksTree, isPortableTextBlock, isPortableTextListItemBlock, isPortableTextToolkitSpan, isPortableTextToolkitTextNode, spanToPlainText } from "@portabletext/toolkit";
|
|
3
|
-
import
|
|
3
|
+
import { buildMarksTree, isPortableTextBlock, isPortableTextListItemBlock, isPortableTextSpan, isPortableTextToolkitSpan, isPortableTextToolkitTextNode, spanToPlainText } from "@portabletext/toolkit";
|
|
4
|
+
import LinkifyIt from "linkify-it";
|
|
4
5
|
import markdownit from "markdown-it";
|
|
6
|
+
import { alert } from "@mdit/plugin-alert";
|
|
5
7
|
function defaultKeyGenerator() {
|
|
6
8
|
return randomKey(12);
|
|
7
9
|
}
|
|
10
|
+
/**
|
|
11
|
+
* Wraps a caller-supplied key generator so one conversion can never
|
|
12
|
+
* mint the same key twice. Keys attach content to its identity at
|
|
13
|
+
* creation time (a span's `marks` entry points at the mark definition
|
|
14
|
+
* minted with the same key), so a generator that repeats a key does
|
|
15
|
+
* not just violate sibling uniqueness, it makes ownership ambiguous in
|
|
16
|
+
* a way no later pass can repair: two definitions sharing a key leave
|
|
17
|
+
* every referencing span attributable to either. Bounded retries, then
|
|
18
|
+
* deterministic suffixing, mirroring how sibling-uniqueness repair
|
|
19
|
+
* treats a generator that keeps returning claimed keys.
|
|
20
|
+
*/
|
|
21
|
+
function uniqueKeyGenerator(generator) {
|
|
22
|
+
let mintedKeys = /* @__PURE__ */ new Set();
|
|
23
|
+
return () => {
|
|
24
|
+
let candidate = generator();
|
|
25
|
+
for (let attempt = 0; attempt < 3 && mintedKeys.has(candidate); attempt++) candidate = generator();
|
|
26
|
+
if (mintedKeys.has(candidate)) {
|
|
27
|
+
let base = candidate, suffix = 2;
|
|
28
|
+
for (; mintedKeys.has(`${base}-${suffix}`);) suffix++;
|
|
29
|
+
candidate = `${base}-${suffix}`;
|
|
30
|
+
}
|
|
31
|
+
return mintedKeys.add(candidate), candidate;
|
|
32
|
+
};
|
|
33
|
+
}
|
|
8
34
|
const getByteHexTable = (() => {
|
|
9
35
|
let table;
|
|
10
36
|
return () => {
|
|
@@ -91,27 +117,431 @@ function buildListIndexMap(blocks) {
|
|
|
91
117
|
listDepthMap
|
|
92
118
|
};
|
|
93
119
|
}
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
120
|
+
/**
|
|
121
|
+
* The CommonMark ASCII punctuation set. Only these characters can be
|
|
122
|
+
* backslash-escaped into a literal without changing the parsed text.
|
|
123
|
+
*/
|
|
124
|
+
const ASCII_PUNCTUATION = /[!-/:-@[-`{-~]/, ENTITY_REFERENCE = /&(?:[a-zA-Z][a-zA-Z0-9]*|#[0-9]+|#[xX][0-9a-fA-F]+);/g, BACKSLASH_BEFORE_PUNCTUATION = RegExp(`\\\\(?=${ASCII_PUNCTUATION.source})`, "g"), TILDE_RUN = /~{2,}/g, HTML_LIKE_ANGLE_BRACKET = /<(?=[a-zA-Z/!?])/g, BRACKET_BEFORE_LINK_OPEN = /\](?=[([])/g, UNICODE_PUNCTUATION_OR_SYMBOL = /^(?:\p{P}|\p{S})$/u, linkify = new LinkifyIt();
|
|
125
|
+
/**
|
|
126
|
+
* Plans the escaped replacement for every plain-text leaf a block's children
|
|
127
|
+
* will produce, in the exact left-to-right order `renderText` visits them
|
|
128
|
+
* (mirroring `buildMarksTree`'s own `text.split('\n')` leaf splitting).
|
|
129
|
+
*
|
|
130
|
+
* Escaping runs ahead of rendering, over the flat span sequence: some
|
|
131
|
+
* hazards only exist across a leaf boundary (an ordered-list marker, a
|
|
132
|
+
* ref-def label, an emphasis run) because an annotation or decorator mark
|
|
133
|
+
* that introduces no markup of its own splices its children in seamlessly.
|
|
134
|
+
* The plan works line by line (a block's children joined into text, split
|
|
135
|
+
* at hard breaks) rather than leaf by leaf: each line's leaves are joined
|
|
136
|
+
* into one string first, opaque children (inline objects, or leaves a
|
|
137
|
+
* custom renderer will replace) masked with a sentinel that can't match any
|
|
138
|
+
* hazard, and every hazard - inline and line-start alike - is detected once
|
|
139
|
+
* against that real, complete line, with true left/right context on both
|
|
140
|
+
* sides. Detected hazards become position-tracked edits against the line's
|
|
141
|
+
* raw text, which are then split back into each contributing leaf's own
|
|
142
|
+
* escaped text; only that composition step is leaf-scoped.
|
|
143
|
+
*
|
|
144
|
+
* A joined line's text that markdown-it's own linkify pass (bundled as
|
|
145
|
+
* `linkify-it`) would claim as a bare URL or email is masked from most
|
|
146
|
+
* edits: the linkify carve-out promises that substring round-trips
|
|
147
|
+
* byte-identical, gaining only a link mark, so escaping inside it would
|
|
148
|
+
* corrupt text linkify is about to claim as a link's visible text. An
|
|
149
|
+
* entity-reference or backtick escape is never masked (see `computeLinkifyMask`
|
|
150
|
+
* for why), and a claim spliced across a decorator boundary is never masked
|
|
151
|
+
* in the first place.
|
|
152
|
+
*
|
|
153
|
+
* `isHeading` is set for ATX headings: only the first joined line sits
|
|
154
|
+
* inside the `# ` prefix an ATX heading can never be reparsed as a block
|
|
155
|
+
* construct within, so line-leading hazards are skipped there; a hard
|
|
156
|
+
* break's later lines are ordinary markdown lines and get the full
|
|
157
|
+
* line-start battery. That first line carries a line-*end* hazard of its
|
|
158
|
+
* own instead: a trailing `#`-run reads back as the heading's own optional
|
|
159
|
+
* closing sequence.
|
|
160
|
+
*
|
|
161
|
+
* `isListItem` is set when the block renders as list-item content: a
|
|
162
|
+
* `[ ] `/`[x] `/`[X] ` at the very start of the first joined line reads
|
|
163
|
+
* back as a GFM task-list checkbox, regardless of the list's own item type.
|
|
164
|
+
*
|
|
165
|
+
* `hardBreakOutputHasNewline` says whether the renderer's actual hard-break
|
|
166
|
+
* output contains a newline. A custom `hardBreak` can render to something
|
|
167
|
+
* with no newline of its own (eg `() => '<br />'`), in which case the
|
|
168
|
+
* leaves on either side of it land on the same rendered line, not two: a
|
|
169
|
+
* hard break like that can't be planned as a line boundary, so it's walled
|
|
170
|
+
* off as an opaque segment instead, the same protection an inline object's
|
|
171
|
+
* unknown rendered text already gets.
|
|
172
|
+
*/
|
|
173
|
+
function planLeafEscaping(children, markDefs, options) {
|
|
174
|
+
let linkMarkKeys = new Set(markDefs.filter((def) => def._type === "link").map((def) => def._key)), markDefKeys = new Set(markDefs.map((def) => def._key)), pieces = [];
|
|
175
|
+
for (let child of children) if (isPortableTextSpan(child)) {
|
|
176
|
+
let isLinkLabel = (child.marks ?? []).some((mark) => linkMarkKeys.has(mark)), markSignature = (child.marks ?? []).filter((mark) => !markDefKeys.has(mark)).sort().join(",");
|
|
177
|
+
child.text.split("\n").forEach((line, index) => {
|
|
178
|
+
index > 0 && pieces.push(options.hardBreakOutputHasNewline ? { kind: "hardBreak" } : { kind: "opaque" }), pieces.push({
|
|
179
|
+
kind: "text",
|
|
180
|
+
raw: line,
|
|
181
|
+
isLinkLabel,
|
|
182
|
+
markSignature
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
} else pieces.push({ kind: "opaque" });
|
|
186
|
+
let pieceOutputs = pieces.map(() => ""), lineIndex = 0, lineText = "", lineChars = [], lineIsLinkLabelChar = [], lineMarkSignature = [], flushLine = () => {
|
|
187
|
+
processLine({
|
|
188
|
+
text: lineText,
|
|
189
|
+
chars: lineChars,
|
|
190
|
+
isLinkLabelChar: lineIsLinkLabelChar,
|
|
191
|
+
markSignature: lineMarkSignature,
|
|
192
|
+
lineIndex,
|
|
193
|
+
isHeading: options.isHeading,
|
|
194
|
+
isListItem: options.isListItem,
|
|
195
|
+
pieceOutputs
|
|
196
|
+
}), lineIndex++, lineText = "", lineChars = [], lineIsLinkLabelChar = [], lineMarkSignature = [];
|
|
197
|
+
};
|
|
198
|
+
for (let pieceIndex = 0; pieceIndex < pieces.length; pieceIndex++) {
|
|
199
|
+
let piece = pieces[pieceIndex];
|
|
200
|
+
if (!piece || piece.kind === "hardBreak") {
|
|
201
|
+
flushLine();
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
if (piece.kind === "opaque") {
|
|
205
|
+
lineText += "\0", lineChars.push(null), lineIsLinkLabelChar.push(!1), lineMarkSignature.push("");
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
let prepared = piece.isLinkLabel ? escapeLinkLabelBrackets(piece.raw) : piece.raw;
|
|
209
|
+
for (let offset = 0; offset < prepared.length; offset++) lineText += prepared[offset], lineChars.push({
|
|
210
|
+
pieceIndex,
|
|
211
|
+
offset
|
|
212
|
+
}), lineIsLinkLabelChar.push(piece.isLinkLabel), lineMarkSignature.push(piece.markSignature);
|
|
98
213
|
}
|
|
99
|
-
|
|
100
|
-
|
|
214
|
+
flushLine();
|
|
215
|
+
let escaped = [];
|
|
216
|
+
return pieces.forEach((piece, index) => {
|
|
217
|
+
piece.kind === "text" && escaped.push(pieceOutputs[index] ?? "");
|
|
218
|
+
}), escaped;
|
|
219
|
+
}
|
|
220
|
+
function processLine(args) {
|
|
221
|
+
let { text, chars, isLinkLabelChar, markSignature, pieceOutputs } = args, linkifyMask = computeLinkifyMask(text, chars, isLinkLabelChar, markSignature);
|
|
222
|
+
applyEdits(text, chars, [...collectInlineEdits(text, isLinkLabelChar), ...collectLineStartEdits(text, args)].filter((edit) => edit.bypassLinkifyMask || !isMasked(edit, linkifyMask)), pieceOutputs);
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Marks every character of this line that markdown-it's linkify pass would
|
|
226
|
+
* claim as part of a bare URL or email. Link-label and opaque characters
|
|
227
|
+
* are blanked out first: a link label's visible text sits inside `[...]`
|
|
228
|
+
* markup real linkify never reconsiders, and an opaque child's rendered
|
|
229
|
+
* text is unknown at plan time, so neither should join or seed a match.
|
|
230
|
+
*
|
|
231
|
+
* The probe only sees this line's raw, undecoded text, one hazard pass
|
|
232
|
+
* ahead of markdown-it's own pipeline: it runs linkify against inline
|
|
233
|
+
* tokenization and entity decoding, not before them. A claim survives only
|
|
234
|
+
* if it lies entirely inside one run of identical decorator marks: a
|
|
235
|
+
* decorator boundary crossing it splices that decorator's delimiters
|
|
236
|
+
* (`**`, `` ` ``, ...) into the middle of the range real linkify would see,
|
|
237
|
+
* which breaks the very claim being trusted. An annotation-only boundary
|
|
238
|
+
* (a link's own label text is already excluded above; any other
|
|
239
|
+
* annotation type falls back to rendering with no delimiters at all,
|
|
240
|
+
* same as an unregistered decorator) never splices, so it can't invalidate
|
|
241
|
+
* a claim either.
|
|
242
|
+
*/
|
|
243
|
+
function computeLinkifyMask(text, chars, isLinkLabelChar, markSignature) {
|
|
244
|
+
let mask = Array(text.length).fill(!1);
|
|
245
|
+
if (!/[.:@]/.test(text)) return mask;
|
|
246
|
+
let probe = "";
|
|
247
|
+
for (let index = 0; index < text.length; index++) probe += chars[index] === null || isLinkLabelChar[index] ? " " : text[index];
|
|
248
|
+
let matches = linkify.match(probe) ?? [];
|
|
249
|
+
for (let match of matches) {
|
|
250
|
+
if (match.schema === "") continue;
|
|
251
|
+
let signature = markSignature[match.index], staysWithinOneMarkRun = !0;
|
|
252
|
+
for (let index = match.index; index < match.lastIndex; index++) if (markSignature[index] !== signature) {
|
|
253
|
+
staysWithinOneMarkRun = !1;
|
|
254
|
+
break;
|
|
255
|
+
}
|
|
256
|
+
if (staysWithinOneMarkRun) for (let index = match.index; index < match.lastIndex; index++) mask[index] = !0;
|
|
257
|
+
}
|
|
258
|
+
return mask;
|
|
259
|
+
}
|
|
260
|
+
function isMasked(edit, mask) {
|
|
261
|
+
let end = edit.at + Math.max(edit.deleteCount, 1);
|
|
262
|
+
for (let index = edit.at; index < end; index++) if (mask[index]) return !0;
|
|
263
|
+
return !1;
|
|
264
|
+
}
|
|
265
|
+
/** Rewrites a line's raw text into each contributing leaf's escaped text by
|
|
266
|
+
* walking it once, left to right, applying at most one edit per position.
|
|
267
|
+
* Every hazard is keyed off its own trigger character - a backslash, a
|
|
268
|
+
* tilde, a backtick, an `&`, a `<`, a `*`/`_`, a `]`, or (line-start only,
|
|
269
|
+
* one hazard per line) a `#`, `>`, `[`, `-`/`+`/`*`, the `.`/`)` after an
|
|
270
|
+
* ordered-list marker's digits, `=`, 4 spaces, or a tab - and no two of
|
|
271
|
+
* those characters coincide at one position, so two edits can never target
|
|
272
|
+
* the same position. */
|
|
273
|
+
function applyEdits(text, chars, edits, pieceOutputs) {
|
|
274
|
+
let editsByPosition = /* @__PURE__ */ new Map();
|
|
275
|
+
for (let edit of edits) {
|
|
276
|
+
if (editsByPosition.has(edit.at)) throw Error(`Two hazard edits targeted the same position (${edit.at}); hazard trigger characters are assumed disjoint by construction.`);
|
|
277
|
+
editsByPosition.set(edit.at, edit);
|
|
278
|
+
}
|
|
279
|
+
let index = 0;
|
|
280
|
+
for (; index < text.length;) {
|
|
281
|
+
let edit = editsByPosition.get(index), owner = chars[index];
|
|
282
|
+
if (edit && (owner && (pieceOutputs[owner.pieceIndex] = (pieceOutputs[owner.pieceIndex] ?? "") + edit.insert), edit.deleteCount > 0)) {
|
|
283
|
+
index += edit.deleteCount;
|
|
284
|
+
continue;
|
|
285
|
+
}
|
|
286
|
+
owner && (pieceOutputs[owner.pieceIndex] = (pieceOutputs[owner.pieceIndex] ?? "") + (text[index] ?? "")), index++;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
/**
|
|
290
|
+
* Hazards that can appear anywhere on a line: emphasis/strikethrough runs,
|
|
291
|
+
* a backtick, an entity reference, an HTML/autolink-shaped `<`, a literal
|
|
292
|
+
* backslash before punctuation, and a `]` immediately before `(`/`[`
|
|
293
|
+
* (which would otherwise read back as a link/image open).
|
|
294
|
+
*/
|
|
295
|
+
function collectInlineEdits(text, isLinkLabelChar) {
|
|
296
|
+
let edits = [];
|
|
297
|
+
for (let match of text.matchAll(BACKSLASH_BEFORE_PUNCTUATION)) {
|
|
298
|
+
let at = match.index ?? 0;
|
|
299
|
+
isLinkLabelChar[at] || edits.push({
|
|
300
|
+
at,
|
|
301
|
+
deleteCount: 0,
|
|
302
|
+
insert: "\\"
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
for (let match of text.matchAll(TILDE_RUN)) {
|
|
306
|
+
let start = match.index ?? 0;
|
|
307
|
+
for (let index = start; index < start + match[0].length; index++) edits.push({
|
|
308
|
+
at: index,
|
|
309
|
+
deleteCount: 0,
|
|
310
|
+
insert: "\\"
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
for (let index = 0; index < text.length; index++) text[index] === "`" && edits.push({
|
|
314
|
+
at: index,
|
|
315
|
+
deleteCount: 0,
|
|
316
|
+
insert: "\\",
|
|
317
|
+
bypassLinkifyMask: !0
|
|
318
|
+
});
|
|
319
|
+
for (let match of text.matchAll(ENTITY_REFERENCE)) edits.push({
|
|
320
|
+
at: match.index ?? 0,
|
|
321
|
+
deleteCount: 0,
|
|
322
|
+
insert: "\\",
|
|
323
|
+
bypassLinkifyMask: !0
|
|
324
|
+
});
|
|
325
|
+
for (let match of text.matchAll(HTML_LIKE_ANGLE_BRACKET)) edits.push({
|
|
326
|
+
at: match.index ?? 0,
|
|
327
|
+
deleteCount: 0,
|
|
328
|
+
insert: "\\"
|
|
329
|
+
});
|
|
330
|
+
edits.push(...collectEmphasisEdits(text));
|
|
331
|
+
for (let match of text.matchAll(BRACKET_BEFORE_LINK_OPEN)) {
|
|
332
|
+
let at = match.index ?? 0;
|
|
333
|
+
isLinkLabelChar[at] || edits.push({
|
|
334
|
+
at,
|
|
335
|
+
deleteCount: 0,
|
|
336
|
+
insert: "\\"
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
return edits;
|
|
340
|
+
}
|
|
341
|
+
function isWhitespace(char) {
|
|
342
|
+
return char === void 0 || /\s/.test(char);
|
|
343
|
+
}
|
|
344
|
+
function isPunctuation(char) {
|
|
345
|
+
return char !== void 0 && UNICODE_PUNCTUATION_OR_SYMBOL.test(char);
|
|
346
|
+
}
|
|
347
|
+
/**
|
|
348
|
+
* The full code point sitting immediately before `index`: two UTF-16 code
|
|
349
|
+
* units for an astral character (eg an emoji) whose low surrogate lands at
|
|
350
|
+
* `index - 1`, one otherwise.
|
|
351
|
+
*/
|
|
352
|
+
function codePointBefore(text, index) {
|
|
353
|
+
if (!(index <= 0)) return index >= 2 && isLowSurrogate(text[index - 1]) && isHighSurrogate(text[index - 2]) ? text.slice(index - 2, index) : text[index - 1];
|
|
354
|
+
}
|
|
355
|
+
/**
|
|
356
|
+
* The full code point sitting immediately at `index`: two UTF-16 code units
|
|
357
|
+
* for an astral character whose high surrogate lands at `index`, one
|
|
358
|
+
* otherwise.
|
|
359
|
+
*/
|
|
360
|
+
function codePointAt(text, index) {
|
|
361
|
+
if (!(index >= text.length)) return isHighSurrogate(text[index]) && isLowSurrogate(text[index + 1]) ? text.slice(index, index + 2) : text[index];
|
|
362
|
+
}
|
|
363
|
+
function isHighSurrogate(char) {
|
|
364
|
+
if (char === void 0) return !1;
|
|
365
|
+
let code = char.charCodeAt(0);
|
|
366
|
+
return code >= 55296 && code <= 56319;
|
|
367
|
+
}
|
|
368
|
+
function isLowSurrogate(char) {
|
|
369
|
+
if (char === void 0) return !1;
|
|
370
|
+
let code = char.charCodeAt(0);
|
|
371
|
+
return code >= 56320 && code <= 57343;
|
|
372
|
+
}
|
|
373
|
+
function isLeftFlanking(before, after) {
|
|
374
|
+
return isWhitespace(after) ? !1 : !isPunctuation(after) || isWhitespace(before) || isPunctuation(before);
|
|
375
|
+
}
|
|
376
|
+
function isRightFlanking(before, after) {
|
|
377
|
+
return isWhitespace(before) ? !1 : !isPunctuation(before) || isWhitespace(after) || isPunctuation(after);
|
|
378
|
+
}
|
|
379
|
+
/**
|
|
380
|
+
* Finds `*`/`_` runs CommonMark would treat as flanking delimiters, using
|
|
381
|
+
* each run's true neighbors on the joined line (the start/end of the line
|
|
382
|
+
* itself counts as whitespace, matching the spec's treatment of line
|
|
383
|
+
* boundaries).
|
|
384
|
+
*/
|
|
385
|
+
function collectEmphasisEdits(text) {
|
|
386
|
+
let edits = [], index = 0;
|
|
387
|
+
for (; index < text.length;) {
|
|
388
|
+
let char = text[index];
|
|
389
|
+
if (char !== "*" && char !== "_") {
|
|
390
|
+
index++;
|
|
391
|
+
continue;
|
|
392
|
+
}
|
|
393
|
+
let end = index;
|
|
394
|
+
for (; end < text.length && text[end] === char;) end++;
|
|
395
|
+
let before = codePointBefore(text, index), after = codePointAt(text, end), leftFlanking = isLeftFlanking(before, after), rightFlanking = isRightFlanking(before, after), canOpen = char === "_" ? leftFlanking && (!rightFlanking || isPunctuation(before)) : leftFlanking, canClose = char === "_" ? rightFlanking && (!leftFlanking || isPunctuation(after)) : rightFlanking;
|
|
396
|
+
if (canOpen || canClose) for (let position = index; position < end; position++) edits.push({
|
|
397
|
+
at: position,
|
|
398
|
+
deleteCount: 0,
|
|
399
|
+
insert: "\\"
|
|
400
|
+
});
|
|
401
|
+
index = end;
|
|
402
|
+
}
|
|
403
|
+
return edits;
|
|
404
|
+
}
|
|
405
|
+
/**
|
|
406
|
+
* Hazards that only matter at the start (or, for a handful of whole-line
|
|
407
|
+
* constructs, the start *and* end) of a line: headings, blockquotes, list
|
|
408
|
+
* markers, ref-defs, setext underlines, thematic breaks, indented code, and
|
|
409
|
+
* a list item's own GFM task-checkbox prefix. A fence needs no branch of
|
|
410
|
+
* its own here: the inline backtick/tilde escaping every line already
|
|
411
|
+
* neutralizes the run a fence needs, so it can never open one on reparse.
|
|
412
|
+
* The remaining branches are mutually exclusive by construction
|
|
413
|
+
* (each targets a disjoint leading character) and return as soon as one
|
|
414
|
+
* matches, mirroring how CommonMark itself commits to one block-start
|
|
415
|
+
* interpretation per line; the checkbox branch above is the one exception,
|
|
416
|
+
* since a list item's checkbox prefix and, say, its heading marker are two
|
|
417
|
+
* independent hazards that can both apply to the same first line.
|
|
418
|
+
*/
|
|
419
|
+
function collectLineStartEdits(text, context) {
|
|
420
|
+
let edits = [], isFirstLine = context.lineIndex === 0, leadingSpaces = /^ {0,3}/.exec(text)?.[0].length ?? 0, rest = text.slice(leadingSpaces);
|
|
421
|
+
if (context.isListItem && isFirstLine && /^\[[ xX]\] /.test(rest) && edits.push({
|
|
422
|
+
at: leadingSpaces,
|
|
423
|
+
deleteCount: 0,
|
|
424
|
+
insert: "\\"
|
|
425
|
+
}), context.isHeading && isFirstLine) {
|
|
426
|
+
let closingSequence = /^(?:(.*[ \t]))?(#+[ \t]*)$/.exec(text);
|
|
427
|
+
return closingSequence && edits.push({
|
|
428
|
+
at: closingSequence[1]?.length ?? 0,
|
|
429
|
+
deleteCount: 0,
|
|
430
|
+
insert: "\\"
|
|
431
|
+
}), edits;
|
|
432
|
+
}
|
|
433
|
+
let orderedListMarker = /^ {0,3}(\d{1,9})([.)])(?=[ \t]|$)/.exec(text);
|
|
434
|
+
if (orderedListMarker) return edits.push({
|
|
435
|
+
at: orderedListMarker[0].length - 1,
|
|
436
|
+
deleteCount: 0,
|
|
437
|
+
insert: "\\"
|
|
438
|
+
}), edits;
|
|
439
|
+
if (/^#{1,6}(?:[ \t]|$)/.test(rest) || rest.startsWith(">") || /^\[[^\]\n]*\]:/.test(rest) || /^[-+*](?:[ \t]|$)/.test(rest) || /^ {0,3}([-*_])(?:[ \t]*\1){2,}[ \t]*$/.test(text) || /^ {0,3}=+[ \t]*$/.test(text) || /^ {0,3}-+[ \t]*$/.test(text)) return edits.push({
|
|
440
|
+
at: leadingSpaces,
|
|
441
|
+
deleteCount: 0,
|
|
442
|
+
insert: "\\"
|
|
443
|
+
}), edits;
|
|
444
|
+
if (/^ {4}/.test(text)) return edits.push({
|
|
445
|
+
at: 0,
|
|
446
|
+
deleteCount: 1,
|
|
447
|
+
insert: " "
|
|
448
|
+
}), edits;
|
|
449
|
+
let tabIndent = /^ {0,3}\t/.exec(text);
|
|
450
|
+
return tabIndent && edits.push({
|
|
451
|
+
at: tabIndent[0].length - 1,
|
|
452
|
+
deleteCount: 1,
|
|
453
|
+
insert: "	"
|
|
454
|
+
}), edits;
|
|
455
|
+
}
|
|
456
|
+
/**
|
|
457
|
+
* Text rendered inside a link label needs every `[`, `]` and `\` escaped
|
|
458
|
+
* unconditionally, on top of the general-purpose hazard escaping every
|
|
459
|
+
* line gets: a link label must stay bracket-balanced, and any literal
|
|
460
|
+
* backslash in it needs protecting regardless of what follows (unlike
|
|
461
|
+
* plain text, where only a backslash immediately before punctuation is a
|
|
462
|
+
* hazard).
|
|
463
|
+
*/
|
|
464
|
+
function escapeLinkLabelBrackets(text) {
|
|
465
|
+
return text.replace(/[[\]\\]/g, (char) => `\\${char}`);
|
|
466
|
+
}
|
|
467
|
+
/**
|
|
468
|
+
* Blocks currently known to be a list item's first content block: it shares
|
|
469
|
+
* its first line with the list marker (and, for a task item, its GFM
|
|
470
|
+
* checkbox), which changes how `renderBlock` plans line-start hazard
|
|
471
|
+
* escaping. Internal to this package so the signal never reaches the
|
|
472
|
+
* public `Serializable`/`RenderNode` types a custom renderer's `.d.ts`
|
|
473
|
+
* would otherwise expose it through.
|
|
474
|
+
*
|
|
475
|
+
* A block is marked right before rendering it; the `renderNode` call that
|
|
476
|
+
* dispatches to `renderBlock` consumes the membership on the way past so a
|
|
477
|
+
* later, unrelated render of the same object (still possible - `renderNode`
|
|
478
|
+
* accepts any `TypedObject`) doesn't inherit a stale claim.
|
|
479
|
+
*/
|
|
480
|
+
const listItemFirstBlocks = /* @__PURE__ */ new WeakSet();
|
|
481
|
+
function markListItemFirstBlock(block) {
|
|
482
|
+
listItemFirstBlocks.add(block);
|
|
483
|
+
}
|
|
484
|
+
function consumeListItemFirstBlock(block) {
|
|
485
|
+
let isListItemFirstBlock = listItemFirstBlocks.has(block);
|
|
486
|
+
return listItemFirstBlocks.delete(block), isListItemFirstBlock;
|
|
487
|
+
}
|
|
488
|
+
/**
|
|
489
|
+
* ATX headings are single-line, inline-only leaf blocks: an ATX heading's
|
|
490
|
+
* first line sits inside its `# ` prefix and can never be reparsed as a
|
|
491
|
+
* block construct, so line-leading hazards never apply there. A hard
|
|
492
|
+
* break's later lines are ordinary markdown lines outside that prefix and
|
|
493
|
+
* get the full line-start battery, same as any other block's continuation.
|
|
494
|
+
*/
|
|
495
|
+
const HEADING_STYLES = /* @__PURE__ */ new Set([
|
|
496
|
+
"h1",
|
|
497
|
+
"h2",
|
|
498
|
+
"h3",
|
|
499
|
+
"h4",
|
|
500
|
+
"h5",
|
|
501
|
+
"h6"
|
|
502
|
+
]), createRenderNode = (renderers, listIndexMap, listDepthMap) => {
|
|
503
|
+
let escapedTextByNode = /* @__PURE__ */ new WeakMap(), hardBreakOutputHasNewline = renderers.hardBreak().includes("\n");
|
|
504
|
+
function renderBlockChildren(node, isHeading, isListItem = !1) {
|
|
505
|
+
let chunks = planLeafEscaping(node.children ?? [], node.markDefs ?? [], {
|
|
506
|
+
isHeading,
|
|
507
|
+
isListItem,
|
|
508
|
+
hardBreakOutputHasNewline
|
|
509
|
+
}), tree = buildMarksTree(node);
|
|
510
|
+
return assignEscapedText(tree, chunks), tree.map((child, i) => renderNode({
|
|
101
511
|
node: child,
|
|
102
512
|
isInline: !0,
|
|
103
513
|
index: i,
|
|
104
514
|
renderNode
|
|
105
515
|
})).join("");
|
|
516
|
+
}
|
|
517
|
+
function assignEscapedText(nodes, chunks) {
|
|
518
|
+
let pointer = 0, visit = (node) => {
|
|
519
|
+
if (isPortableTextToolkitTextNode(node)) {
|
|
520
|
+
if (node.text !== "\n") {
|
|
521
|
+
let escaped = chunks[pointer];
|
|
522
|
+
escaped !== void 0 && escapedTextByNode.set(node, escaped), pointer++;
|
|
523
|
+
}
|
|
524
|
+
return;
|
|
525
|
+
}
|
|
526
|
+
isPortableTextToolkitSpan(node) && node.children.forEach(visit);
|
|
527
|
+
};
|
|
528
|
+
nodes.forEach(visit);
|
|
529
|
+
}
|
|
530
|
+
function renderNode(options) {
|
|
531
|
+
let { node, index, isInline } = options;
|
|
532
|
+
return isPortableTextListItemBlock(node) ? renderListItem(node, index) : isPortableTextToolkitSpan(node) ? renderSpan(node) : isPortableTextBlock(node) ? renderBlock(node, index, isInline, consumeListItemFirstBlock(node)) : isPortableTextToolkitTextNode(node) ? renderText(node) : renderCustomBlock(node, index, isInline);
|
|
533
|
+
}
|
|
534
|
+
function renderListItem(node, index) {
|
|
535
|
+
let renderer = renderers.listItem, itemHandler = (typeof renderer == "function" ? renderer : renderer[node.listItem]) || renderers.unknownListItem, children;
|
|
106
536
|
if (node.style && node.style !== "normal") {
|
|
107
537
|
let { listItem: _listItem, ...blockNode } = node;
|
|
108
|
-
children = renderNode({
|
|
538
|
+
markListItemFirstBlock(blockNode), children = renderNode({
|
|
109
539
|
node: blockNode,
|
|
110
540
|
index,
|
|
111
541
|
isInline: !1,
|
|
112
542
|
renderNode
|
|
113
543
|
}), children = children.replace(/\n+$/, "");
|
|
114
|
-
}
|
|
544
|
+
} else children = renderBlockChildren(node, !1, !0);
|
|
115
545
|
return itemHandler({
|
|
116
546
|
value: node,
|
|
117
547
|
index,
|
|
@@ -138,21 +568,18 @@ const createRenderNode = (renderers, listIndexMap, listDepthMap) => {
|
|
|
138
568
|
children: children.join("")
|
|
139
569
|
});
|
|
140
570
|
}
|
|
141
|
-
function renderBlock(node, index, isInline) {
|
|
142
|
-
let
|
|
143
|
-
|
|
571
|
+
function renderBlock(node, index, isInline, isListItem) {
|
|
572
|
+
let style = node.style || "normal", children = renderBlockChildren(node, HEADING_STYLES.has(style), isListItem);
|
|
573
|
+
return ((typeof renderers.block == "function" ? renderers.block : renderers.block[style]) || renderers.unknownBlockStyle)({
|
|
144
574
|
index,
|
|
145
575
|
isInline,
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
return ((typeof renderers.block == "function" ? renderers.block : renderers.block[style]) || renderers.unknownBlockStyle)({
|
|
149
|
-
...props,
|
|
150
|
-
value: props.node,
|
|
576
|
+
children,
|
|
577
|
+
value: node,
|
|
151
578
|
renderNode
|
|
152
579
|
});
|
|
153
580
|
}
|
|
154
581
|
function renderText(node) {
|
|
155
|
-
return node.text === "\n" ? renderers.hardBreak() : node.text;
|
|
582
|
+
return node.text === "\n" ? renderers.hardBreak() : escapedTextByNode.get(node) ?? node.text;
|
|
156
583
|
}
|
|
157
584
|
function renderCustomBlock(value, index, isInline) {
|
|
158
585
|
return (renderers.types[value._type] ?? renderers.unknownType)({
|
|
@@ -163,26 +590,7 @@ const createRenderNode = (renderers, listIndexMap, listDepthMap) => {
|
|
|
163
590
|
});
|
|
164
591
|
}
|
|
165
592
|
return renderNode;
|
|
166
|
-
}
|
|
167
|
-
function serializeBlock(options) {
|
|
168
|
-
let { node, index, isInline, renderNode } = options, renderedChildren = buildMarksTree(node).map((child, i) => renderNode({
|
|
169
|
-
node: child,
|
|
170
|
-
isInline: !0,
|
|
171
|
-
index: i,
|
|
172
|
-
renderNode
|
|
173
|
-
}));
|
|
174
|
-
return {
|
|
175
|
-
_key: node._key || defaultKeyGenerator(),
|
|
176
|
-
children: renderedChildren.join(""),
|
|
177
|
-
index,
|
|
178
|
-
isInline,
|
|
179
|
-
node
|
|
180
|
-
};
|
|
181
|
-
}
|
|
182
|
-
/**
|
|
183
|
-
* @public
|
|
184
|
-
*/
|
|
185
|
-
const DefaultBlockSpacingRenderer = ({ current, next }) => isPortableTextListItemBlock(current) && isPortableTextListItemBlock(next) ? "\n" : isPortableTextBlock(current) && isPortableTextBlock(next) && current.style === "blockquote" && next.style === "blockquote" ? "\n>\n" : "\n\n", DefaultHardBreakRenderer = () => " \n", DefaultListItemRenderer = ({ children, value, listIndex, listDepth }) => {
|
|
593
|
+
}, DefaultBlockSpacingRenderer = ({ current, next }) => isPortableTextListItemBlock(current) && isPortableTextListItemBlock(next) ? "\n" : isPortableTextBlock(current) && isPortableTextBlock(next) && current.style === "blockquote" && next.style === "blockquote" ? "\n>\n" : "\n\n", DefaultHardBreakRenderer = () => " \n", DefaultListItemRenderer = ({ children, value, listIndex, listDepth }) => {
|
|
186
594
|
let listStyle = value.listItem || "bullet", depth = listDepth ?? (value.level || 1) - 1, indent = " ".repeat(depth);
|
|
187
595
|
return listStyle === "number" ? `${indent}${listIndex ?? 1}. ${children}` : listStyle === "task" ? `${indent}- ${"checked" in value && typeof value.checked == "boolean" && value.checked ? "[x]" : "[ ]"} ${children}` : `${indent}- ${children}`;
|
|
188
596
|
}, DefaultUnknownListItemRenderer = ({ children }) => `- ${children}\n`;
|
|
@@ -208,32 +616,39 @@ function escapeImageAndLinkTitle(text) {
|
|
|
208
616
|
* Escapes characters that have special meaning at the row level of a GFM
|
|
209
617
|
* table cell.
|
|
210
618
|
*
|
|
211
|
-
* A literal `|` ends the cell, so
|
|
212
|
-
*
|
|
213
|
-
*
|
|
214
|
-
*
|
|
215
|
-
*
|
|
619
|
+
* A literal `|` ends the cell, so a pipe preceded by an even number of
|
|
620
|
+
* backslashes (including zero) gets one more: paired backslashes cancel
|
|
621
|
+
* out to a literal backslash and leave the pipe live, so parity, not mere
|
|
622
|
+
* presence, decides whether it is already escaped. Newlines end the row,
|
|
623
|
+
* so they are replaced with `<br>` to keep the visible line break inside
|
|
624
|
+
* the cell.
|
|
216
625
|
*
|
|
217
|
-
* Backslashes are
|
|
218
|
-
* already in the rendered cell (such as `\[` and `\]` in
|
|
219
|
-
*
|
|
626
|
+
* Backslashes themselves are left alone here; only the parity check reads
|
|
627
|
+
* them, so escapes already in the rendered cell (such as `\[` and `\]` in
|
|
628
|
+
* link text) survive the pass untouched.
|
|
220
629
|
*/
|
|
221
630
|
function escapeTableCell(text) {
|
|
222
|
-
return text.replace(
|
|
631
|
+
return text.replace(/(\\*)\|/g, (match, backslashes) => backslashes.length % 2 == 0 ? `${backslashes}\\|` : match).replace(/\n/g, "<br>");
|
|
632
|
+
}
|
|
633
|
+
/**
|
|
634
|
+
* @public
|
|
635
|
+
*/
|
|
636
|
+
const DefaultEmRenderer = ({ children }) => `_${children}_`, DefaultStrongRenderer = ({ children }) => `**${children}**`, DefaultCodeRenderer = ({ text }) => wrapInCodeSpan(text);
|
|
637
|
+
function wrapInCodeSpan(text) {
|
|
638
|
+
let fence = "`".repeat(longestBacktickRun(text) + 1), touchesBacktick = text.startsWith("`") || text.endsWith("`"), wouldBeStripped = text.startsWith(" ") && text.endsWith(" ") && text.trim() !== "", padding = touchesBacktick || wouldBeStripped ? " " : "";
|
|
639
|
+
return `${fence}${padding}${text}${padding}${fence}`;
|
|
640
|
+
}
|
|
641
|
+
function longestBacktickRun(text) {
|
|
642
|
+
let longest = 0;
|
|
643
|
+
for (let run of text.match(/`+/g) ?? []) longest = Math.max(longest, run.length);
|
|
644
|
+
return longest;
|
|
223
645
|
}
|
|
224
646
|
/**
|
|
225
647
|
* @public
|
|
226
648
|
*/
|
|
227
|
-
const
|
|
649
|
+
const DefaultUnderlineRenderer = ({ children }) => `<u>${children}</u>`, DefaultStrikeThroughRenderer = ({ children }) => `~~${children}~~`, DefaultLinkRenderer = ({ children, value }) => {
|
|
228
650
|
let href = value?.href || "", title = value?.title || "";
|
|
229
|
-
|
|
230
|
-
if (/["'][^"']*[<>]|[<>][^<>]*["']/.test(href)) {
|
|
231
|
-
let encodedHref = href.replace(/["<>() ]/g, (char) => `%${char.charCodeAt(0).toString(16).toUpperCase()}`);
|
|
232
|
-
return `[${escapeImageAndLinkText(children)}](${encodedHref})`;
|
|
233
|
-
}
|
|
234
|
-
return `[${escapeImageAndLinkText(children)}](${href}${title ? ` "${escapeImageAndLinkTitle(title)}"` : ""})`;
|
|
235
|
-
}
|
|
236
|
-
return children;
|
|
651
|
+
return uriLooksSafe(href) ? /["'][^"']*[<>]|[<>][^<>]*["']/.test(href) ? `[${children}](${href.replace(/["<>() ]/g, (char) => `%${char.charCodeAt(0).toString(16).toUpperCase()}`)})` : `[${children}](${href}${title ? ` "${escapeImageAndLinkTitle(title)}"` : ""})` : children;
|
|
237
652
|
};
|
|
238
653
|
function uriLooksSafe(uri) {
|
|
239
654
|
let url = (uri || "").trim(), first = url.charAt(0);
|
|
@@ -267,7 +682,7 @@ function isCodeShaped(value) {
|
|
|
267
682
|
* as absent instead of guarded.
|
|
268
683
|
*/
|
|
269
684
|
function normalizeLanguage(language) {
|
|
270
|
-
return typeof language != "string" || language.includes("\n") ? "" : language;
|
|
685
|
+
return typeof language != "string" || language.includes("\n") || language === "json:object" ? "" : language;
|
|
271
686
|
}
|
|
272
687
|
/**
|
|
273
688
|
* @public
|
|
@@ -280,10 +695,10 @@ function isHtmlShaped(value) {
|
|
|
280
695
|
* @public
|
|
281
696
|
*/
|
|
282
697
|
const DefaultImageRenderer = (options) => {
|
|
283
|
-
if (!isImageShaped(options.value)) return DefaultUnknownTypeRenderer(options);
|
|
698
|
+
if (!isImageShaped(options.value) || !linkValidator(options.value.src)) return DefaultUnknownTypeRenderer(options);
|
|
284
699
|
let alt = escapeImageAndLinkText(options.value.alt ?? ""), title = options.value.title ? ` "${escapeImageAndLinkTitle(options.value.title)}"` : "";
|
|
285
700
|
return ``;
|
|
286
|
-
};
|
|
701
|
+
}, linkValidator = new markdownit().validateLink;
|
|
287
702
|
function isImageShaped(value) {
|
|
288
703
|
let image = value;
|
|
289
704
|
return typeof image?.src == "string" && (image.alt == null || typeof image.alt == "string") && (image.title == null || typeof image.title == "string");
|
|
@@ -325,12 +740,23 @@ const DefaultTableRenderer = (options) => {
|
|
|
325
740
|
function renderTable(value, renderNode) {
|
|
326
741
|
let rows = value.rows, alignment = Array.isArray(value.alignment) ? value.alignment : void 0, headerRow = rows.at(0);
|
|
327
742
|
if (!headerRow) return "";
|
|
328
|
-
let getCellText = (cellBlocks) => cellBlocks.map((block, index) =>
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
743
|
+
let getCellText = (cellBlocks) => cellBlocks.map((block, index) => {
|
|
744
|
+
let rendered = renderNode({
|
|
745
|
+
node: block,
|
|
746
|
+
index,
|
|
747
|
+
isInline: !1,
|
|
748
|
+
renderNode
|
|
749
|
+
}), rendererOptions = {
|
|
750
|
+
value: block,
|
|
751
|
+
isInline: !1,
|
|
752
|
+
index,
|
|
753
|
+
renderNode
|
|
754
|
+
};
|
|
755
|
+
return rendered === DefaultUnknownTypeRenderer(rendererOptions) ? DefaultUnknownTypeRenderer({
|
|
756
|
+
...rendererOptions,
|
|
757
|
+
isInline: !0
|
|
758
|
+
}) : rendered;
|
|
759
|
+
}).join(" ").trim(), lines = [], columnCount = rows.reduce((max, row) => Math.max(max, row.cells.length), 0), renderCells = (texts) => {
|
|
334
760
|
let padded = [...texts];
|
|
335
761
|
for (; padded.length < columnCount;) padded.push("");
|
|
336
762
|
return `| ${padded.join(" | ")} |`;
|
|
@@ -363,7 +789,7 @@ const DefaultCalloutRenderer = (options) => {
|
|
|
363
789
|
index,
|
|
364
790
|
isInline: !1,
|
|
365
791
|
renderNode
|
|
366
|
-
})).join("\n\n").split("\n").map((line) => line === "" ? ">" : `> ${line}`).join("\n");
|
|
792
|
+
})).filter((rendered) => rendered !== "").join("\n\n").split("\n").map((line) => line === "" ? ">" : `> ${line}`).join("\n");
|
|
367
793
|
return `> [!${options.value.tone.toUpperCase()}]\n${prefixed}`;
|
|
368
794
|
};
|
|
369
795
|
function isCalloutShaped(value) {
|
|
@@ -390,20 +816,29 @@ const DefaultBlockquoteObjectRenderer = ({ value, renderNode }) => value.content
|
|
|
390
816
|
index,
|
|
391
817
|
isInline: !1,
|
|
392
818
|
renderNode
|
|
393
|
-
})).join("\n\n").split("\n").map((line) => line === "" ? ">" : `> ${line}`).join("\n"), DefaultListRenderer = ({ value, renderNode }) => {
|
|
394
|
-
let
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
isNestedList
|
|
398
|
-
|
|
819
|
+
})).filter((rendered) => rendered !== "").join("\n\n").split("\n").map((line) => line === "" ? ">" : `> ${line}`).join("\n"), DefaultListRenderer = ({ value, renderNode }) => {
|
|
820
|
+
let renderedItems = value.items.map((item) => {
|
|
821
|
+
let markerLineSettled = !1;
|
|
822
|
+
return item.content.map((block, blockIndex) => {
|
|
823
|
+
let isNestedList = block._type === "list", isTextBlock = !isNestedList && isPortableTextBlock(block);
|
|
824
|
+
!markerLineSettled && isTextBlock && markListItemFirstBlock(block);
|
|
825
|
+
let text = renderNode({
|
|
399
826
|
node: block,
|
|
400
827
|
index: blockIndex,
|
|
401
828
|
isInline: !1,
|
|
402
829
|
renderNode
|
|
403
|
-
})
|
|
404
|
-
|
|
830
|
+
});
|
|
831
|
+
return text !== "" && (markerLineSettled = !0), {
|
|
832
|
+
isNestedList,
|
|
833
|
+
isTextBlock,
|
|
834
|
+
text
|
|
835
|
+
};
|
|
836
|
+
});
|
|
837
|
+
}), itemSeparator = renderedItems.some((renderedBlocks) => renderedBlocks.filter((rendered) => !rendered.isNestedList && rendered.text !== "").length > 1) ? "\n\n" : "\n";
|
|
838
|
+
return value.items.map((item, itemIndex) => {
|
|
839
|
+
let marker = getListMarker(value.kind, itemIndex, item.checked), indentWidth = value.kind === "task" ? 2 : marker.length, indent = " ".repeat(indentWidth), indentLines = (text) => text.split("\n").map((line) => line === "" ? "" : `${indent}${line}`).join("\n"), nonEmptyBlocks = (renderedItems[itemIndex] ?? []).filter((rendered) => rendered.text !== ""), markerLineCandidate = nonEmptyBlocks[0], promoted = markerLineCandidate && !markerLineCandidate.isNestedList && (value.kind !== "task" || markerLineCandidate.isTextBlock) ? markerLineCandidate : void 0, rest = promoted ? nonEmptyBlocks.slice(1) : nonEmptyBlocks, [promotedFirstLine = "", ...promotedRestLines] = (promoted?.text ?? "").split("\n"), head = [`${marker}${promotedFirstLine}`, ...promotedRestLines.length > 0 ? [indentLines(promotedRestLines.join("\n"))] : []].join("\n").trimEnd();
|
|
405
840
|
return rest.length === 0 ? head : `${head}${rest.map((rendered) => {
|
|
406
|
-
let indented = rendered.text
|
|
841
|
+
let indented = indentLines(rendered.text);
|
|
407
842
|
return rendered.isNestedList ? `\n${indented}` : `\n\n${indented}`;
|
|
408
843
|
}).join("")}`;
|
|
409
844
|
}).join(itemSeparator);
|
|
@@ -414,10 +849,7 @@ function getListMarker(kind, itemIndex, checked) {
|
|
|
414
849
|
/**
|
|
415
850
|
* @public
|
|
416
851
|
*/
|
|
417
|
-
const DefaultUnknownTypeRenderer = ({ value, isInline }) => {
|
|
418
|
-
let json = `\`\`\`json\n${JSON.stringify(value, null, 2)}\n\`\`\``;
|
|
419
|
-
return isInline ? `\n${json}\n` : json;
|
|
420
|
-
}, defaultRenderers = {
|
|
852
|
+
const DefaultUnknownTypeRenderer = ({ value, isInline }) => isInline ? `json:object${wrapInCodeSpan(JSON.stringify(value))}` : `\`\`\`json:object\n${JSON.stringify(value, null, 2)}\n\`\`\``, defaultRenderers = {
|
|
421
853
|
types: {
|
|
422
854
|
callout: DefaultCalloutRenderer,
|
|
423
855
|
code: DefaultCodeBlockRenderer,
|
|
@@ -466,7 +898,7 @@ function portableTextToMarkdown(blocks, options = {}) {
|
|
|
466
898
|
...options.marks
|
|
467
899
|
},
|
|
468
900
|
types: {
|
|
469
|
-
...defaultRenderers.types,
|
|
901
|
+
...gateDefaultTypeRenderers(defaultRenderers.types, options.schema, options.unknownType ?? defaultRenderers.unknownType),
|
|
470
902
|
...options.types
|
|
471
903
|
},
|
|
472
904
|
hardBreak: options.hardBreak ?? defaultRenderers.hardBreak,
|
|
@@ -474,22 +906,26 @@ function portableTextToMarkdown(blocks, options = {}) {
|
|
|
474
906
|
unknownBlockStyle: options.unknownBlockStyle ?? defaultRenderers.unknownBlockStyle,
|
|
475
907
|
unknownListItem: options.unknownListItem ?? defaultRenderers.unknownListItem,
|
|
476
908
|
unknownMark: options.unknownMark ?? defaultRenderers.unknownMark
|
|
477
|
-
}, renderBlockSpacing = options.blockSpacing ?? DefaultBlockSpacingRenderer, { listIndexMap, listDepthMap } = buildListIndexMap(blocks), renderNode = createRenderNode(renderers, listIndexMap, listDepthMap)
|
|
478
|
-
|
|
479
|
-
|
|
909
|
+
}, renderBlockSpacing = options.blockSpacing ?? DefaultBlockSpacingRenderer, { listIndexMap, listDepthMap } = buildListIndexMap(blocks), renderNode = createRenderNode(renderers, listIndexMap, listDepthMap), renderedBlocks = blocks.map((node, index) => ({
|
|
910
|
+
node,
|
|
911
|
+
rendered: renderNode({
|
|
480
912
|
node,
|
|
481
913
|
index,
|
|
482
914
|
isInline: !1,
|
|
483
915
|
renderNode
|
|
484
|
-
})
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
916
|
+
})
|
|
917
|
+
})).filter(({ rendered }) => rendered !== "");
|
|
918
|
+
return renderedBlocks.map(({ node, rendered }, index) => {
|
|
919
|
+
let nextBlock = renderedBlocks.at(index + 1);
|
|
920
|
+
return nextBlock ? `${rendered}${renderBlockSpacing({
|
|
488
921
|
current: node,
|
|
489
|
-
next:
|
|
490
|
-
}) ?? "\n\n"}` :
|
|
922
|
+
next: nextBlock.node
|
|
923
|
+
}) ?? "\n\n"}` : rendered;
|
|
491
924
|
}).join("");
|
|
492
925
|
}
|
|
926
|
+
function gateDefaultTypeRenderers(defaultTypeRenderers, schema, resolvedUnknownType) {
|
|
927
|
+
return schema ? Object.fromEntries(Object.entries(defaultTypeRenderers).map(([typeName, renderer]) => [typeName, (rendererOptions) => (rendererOptions.isInline ? schema.inlineObjects : schema.blockObjects).some((item) => item.name === typeName) && renderer ? renderer(rendererOptions) : resolvedUnknownType(rendererOptions)])) : defaultTypeRenderers;
|
|
928
|
+
}
|
|
493
929
|
/********************
|
|
494
930
|
* Default style definitions
|
|
495
931
|
********************/
|
|
@@ -611,7 +1047,66 @@ const normalStyleDefinition = { name: "normal" }, h1StyleDefinition = { name: "h
|
|
|
611
1047
|
defaultTableObjectDefinition
|
|
612
1048
|
],
|
|
613
1049
|
inlineObjects: [defaultImageObjectDefinition]
|
|
614
|
-
}))
|
|
1050
|
+
})), degradationMessage = {
|
|
1051
|
+
"decorator-dropped": (decorator) => {
|
|
1052
|
+
switch (decorator) {
|
|
1053
|
+
case "code": return "Removed inline-code formatting, kept the text: the schema has no `code` decorator";
|
|
1054
|
+
case "strong": return "Removed bold formatting, kept the text: the schema has no `strong` decorator";
|
|
1055
|
+
case "em": return "Removed italic formatting, kept the text: the schema has no `em` decorator";
|
|
1056
|
+
case "strikeThrough": return "Removed strikethrough formatting, kept the text: the schema has no `strike-through` decorator";
|
|
1057
|
+
}
|
|
1058
|
+
},
|
|
1059
|
+
"annotation-dropped": (cause) => cause === "missing-url" ? "Removed a link that has no URL, kept its text" : "Removed the link, kept its text: the schema has no `link` annotation",
|
|
1060
|
+
"style-fallback": (name) => /^h[1-6]$/.test(name) ? `\`${"#".repeat(Number(name.slice(1)))}\` heading became a normal paragraph: the schema has no \`${name}\` style` : name === "blockquote" ? "Blockquote became normal paragraphs: the schema has no `blockquote` style" : `Fell back to \`normal\` style: \`${name}\` not in schema`,
|
|
1061
|
+
"list-flattened": (kind) => `${kind === "number" ? "Numbered" : "Bullet"} list became plain paragraphs: the schema has no \`${kind}\` list`,
|
|
1062
|
+
"task-checkbox-stripped": (checked) => `Removed the \`${checked ? "[x]" : "[ ]"}\` checkbox, kept a plain list item: the schema has no \`task\` list`,
|
|
1063
|
+
"table-flattened": "Table became plain text blocks, rows and columns lost: the schema has no `table` block object",
|
|
1064
|
+
"code-block-to-text": (language) => language ? `\`${language}\` code block became plain text: the schema has no \`code\` block object` : "Code block became plain text: the schema has no `code` block object",
|
|
1065
|
+
"horizontal-rule-to-text": "Horizontal rule became the text `---`: the schema has no `horizontal-rule` block object",
|
|
1066
|
+
"html-block-to-text": "HTML block became plain text: the schema has no `html` block object",
|
|
1067
|
+
"inline-html-dropped": "Removed inline HTML tags, kept nothing: `html.inline` is `skip` (the default)",
|
|
1068
|
+
"image-block-to-inline": (cause) => cause === "table-cell" ? "The image became inline: a table cell can't hold a block-level `image`" : "The image became inline: the schema has no block-level `image`",
|
|
1069
|
+
"image-inline-to-block": "The image became its own block, splitting the paragraph: the schema has no inline `image`",
|
|
1070
|
+
"image-to-text": "Image became its markdown source as plain text: the schema has no `image` object",
|
|
1071
|
+
"callout-fallback": (calloutType, style) => `\`[!${calloutType.toUpperCase()}]\` callout became ${style}-styled text: the schema has no \`callout\` block object`,
|
|
1072
|
+
"fields-dropped": (names, construct) => `Dropped ${names} from \`${construct}\`: not in the schema's \`${construct}\` fields`,
|
|
1073
|
+
"object-carrier-invalid": (kind, payload) => kind === "fence" ? `\`json:object\` fence fell back to a code block: ${describeObjectCarrierFailure(payload)}` : `\`json:object\`-tagged code span fell back to a plain code span: ${describeObjectCarrierFailure(payload)}`
|
|
1074
|
+
};
|
|
1075
|
+
/**
|
|
1076
|
+
* Names why a `json:object` payload failed to parse as an object carrier:
|
|
1077
|
+
* a payload that isn't a JSON object at all reads differently from one
|
|
1078
|
+
* that is but has no usable `_type`.
|
|
1079
|
+
*/
|
|
1080
|
+
function describeObjectCarrierFailure(payload) {
|
|
1081
|
+
let parsed;
|
|
1082
|
+
try {
|
|
1083
|
+
parsed = JSON.parse(payload);
|
|
1084
|
+
} catch {
|
|
1085
|
+
return "the payload is not valid JSON";
|
|
1086
|
+
}
|
|
1087
|
+
return typeof parsed != "object" || !parsed || Array.isArray(parsed) ? "the payload is not a JSON object" : "the payload has no string `_type`";
|
|
1088
|
+
}
|
|
1089
|
+
const droppedFieldsTag = Symbol("droppedFields");
|
|
1090
|
+
function readDroppedFields(object) {
|
|
1091
|
+
if (object) return object[droppedFieldsTag];
|
|
1092
|
+
}
|
|
1093
|
+
function buildFilteredObject(schemaDefinition, value, keyGenerator) {
|
|
1094
|
+
let filteredValue = schemaDefinition.fields.reduce((filteredValue, field) => {
|
|
1095
|
+
let fieldValue = value[field.name];
|
|
1096
|
+
return fieldValue !== void 0 && (filteredValue[field.name] = fieldValue), filteredValue;
|
|
1097
|
+
}, {}), object = {
|
|
1098
|
+
_key: keyGenerator(),
|
|
1099
|
+
_type: schemaDefinition.name,
|
|
1100
|
+
...filteredValue
|
|
1101
|
+
}, droppedKeys = Object.entries(value).filter(([, fieldValue]) => fieldValue !== void 0).map(([key]) => key).filter((key) => !(key in filteredValue));
|
|
1102
|
+
return droppedKeys.length > 0 && Object.defineProperty(object, droppedFieldsTag, {
|
|
1103
|
+
value: {
|
|
1104
|
+
construct: schemaDefinition.name,
|
|
1105
|
+
keys: droppedKeys
|
|
1106
|
+
},
|
|
1107
|
+
enumerable: !1
|
|
1108
|
+
}), object;
|
|
1109
|
+
}
|
|
615
1110
|
function buildStyleMatcher(definition) {
|
|
616
1111
|
return ({ context }) => {
|
|
617
1112
|
let schemaDefinition = context.schema.styles.find((item) => item.name === definition.name);
|
|
@@ -633,31 +1128,13 @@ function buildDecoratorMatcher(definition) {
|
|
|
633
1128
|
function buildAnnotationMatcher(definition) {
|
|
634
1129
|
return ({ context, value }) => {
|
|
635
1130
|
let schemaDefinition = context.schema.annotations.find((item) => item.name === definition.name);
|
|
636
|
-
if (
|
|
637
|
-
let filteredValue = schemaDefinition.fields.reduce((filteredValue, field) => {
|
|
638
|
-
let fieldValue = value[field.name];
|
|
639
|
-
return fieldValue !== void 0 && (filteredValue[field.name] = fieldValue), filteredValue;
|
|
640
|
-
}, {});
|
|
641
|
-
return {
|
|
642
|
-
_key: context.keyGenerator(),
|
|
643
|
-
_type: schemaDefinition.name,
|
|
644
|
-
...filteredValue
|
|
645
|
-
};
|
|
1131
|
+
if (schemaDefinition) return buildFilteredObject(schemaDefinition, value, context.keyGenerator);
|
|
646
1132
|
};
|
|
647
1133
|
}
|
|
648
1134
|
function buildObjectMatcher(definition) {
|
|
649
1135
|
return ({ context, value, isInline }) => {
|
|
650
1136
|
let schemaDefinition = (isInline ? context.schema.inlineObjects : context.schema.blockObjects).find((item) => item.name === definition.name);
|
|
651
|
-
if (
|
|
652
|
-
let filteredValue = schemaDefinition.fields.reduce((filteredValue, field) => {
|
|
653
|
-
let fieldValue = value[field.name];
|
|
654
|
-
return fieldValue !== void 0 && (filteredValue[field.name] = fieldValue), filteredValue;
|
|
655
|
-
}, {});
|
|
656
|
-
return {
|
|
657
|
-
_key: context.keyGenerator(),
|
|
658
|
-
_type: schemaDefinition.name,
|
|
659
|
-
...filteredValue
|
|
660
|
-
};
|
|
1137
|
+
if (schemaDefinition) return buildFilteredObject(schemaDefinition, value, context.keyGenerator);
|
|
661
1138
|
};
|
|
662
1139
|
}
|
|
663
1140
|
const codeBlockMatcher = ({ context, value, isInline }) => {
|
|
@@ -742,6 +1219,83 @@ function flattenTable(table, portableText) {
|
|
|
742
1219
|
for (let row of table.rows) for (let cell of row.cells) for (let block of cell.value) portableText.push(block);
|
|
743
1220
|
}
|
|
744
1221
|
/**
|
|
1222
|
+
* Truncates a degradation message's snippet to keep the thrown/reported
|
|
1223
|
+
* message readable. Truncates on character count, not word boundaries: the
|
|
1224
|
+
* snippet is a diagnostic pointer back to the source, not prose. Undefined
|
|
1225
|
+
* for empty input, so a construct with nothing to quote (an empty link's
|
|
1226
|
+
* text, say) omits `snippet` entirely instead of reporting `""`. Backs the
|
|
1227
|
+
* cut off by one unit when it would land on a lead surrogate, so a snippet
|
|
1228
|
+
* ending mid-emoji doesn't produce an unpaired surrogate. A literal newline
|
|
1229
|
+
* surviving into the snippet is escaped to `\n`, since the reported message
|
|
1230
|
+
* is one line per finding.
|
|
1231
|
+
*/
|
|
1232
|
+
function truncateSnippet(text, maxLength = 40) {
|
|
1233
|
+
if (text.length === 0) return;
|
|
1234
|
+
if (text.length <= maxLength) return text.replace(/\n/g, "\\n");
|
|
1235
|
+
let cut = maxLength, codeUnit = text.charCodeAt(cut - 1);
|
|
1236
|
+
return codeUnit >= 55296 && codeUnit <= 56319 && --cut, `${text.slice(0, cut).replace(/\n/g, "\\n")}...`;
|
|
1237
|
+
}
|
|
1238
|
+
/**
|
|
1239
|
+
* Concatenates the plain text between an inline open token (`strong_open`,
|
|
1240
|
+
* `em_open`, `s_open`, `link_open`) and its matching close, for use as a
|
|
1241
|
+
* degradation message snippet. Tracks nesting depth so a same-type token
|
|
1242
|
+
* nested inside itself doesn't stop the scan at the wrong close.
|
|
1243
|
+
*/
|
|
1244
|
+
function collectInlineText(children, openIndex, openType, closeType) {
|
|
1245
|
+
let depth = 1, text = "";
|
|
1246
|
+
for (let i = openIndex + 1; i < children.length; i++) {
|
|
1247
|
+
let child = children[i];
|
|
1248
|
+
if (child) {
|
|
1249
|
+
if (child.type === openType) depth++;
|
|
1250
|
+
else if (child.type === closeType) {
|
|
1251
|
+
if (depth--, depth === 0) break;
|
|
1252
|
+
} else child.type === "text" ? text += child.content : child.type === "softbreak" ? text += " " : child.type === "hardbreak" && (text += "\n");
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1255
|
+
return text;
|
|
1256
|
+
}
|
|
1257
|
+
function capList(values) {
|
|
1258
|
+
if (values.length <= 5) return values.join(", ");
|
|
1259
|
+
let shown = values.slice(0, 5), more = values.length - 5;
|
|
1260
|
+
return `${shown.join(", ")}, and ${more} more`;
|
|
1261
|
+
}
|
|
1262
|
+
/**
|
|
1263
|
+
* Builds the canonical grouped message reported alongside a non-empty
|
|
1264
|
+
* `degradations` array: identical (`type`, `message`) pairs collapse into one
|
|
1265
|
+
* line, so a document with the same missing decorator on three spans
|
|
1266
|
+
* doesn't repeat the same sentence three times. Groups sort by their
|
|
1267
|
+
* earliest-lined entry so the message reads top-to-bottom regardless of walk
|
|
1268
|
+
* order: nested constructs (a blockquote inside a blockquote, say) report
|
|
1269
|
+
* the innermost closing first even though it opened last, and that line may
|
|
1270
|
+
* arrive after another entry already in the group. Groups without any lined
|
|
1271
|
+
* entry sort last, in their relative encounter order.
|
|
1272
|
+
*/
|
|
1273
|
+
function buildDegradationMessage(degradations) {
|
|
1274
|
+
let groups = [], groupIndexByKey = /* @__PURE__ */ new Map();
|
|
1275
|
+
for (let degradation of degradations) {
|
|
1276
|
+
let key = `${degradation.type}\u0000${degradation.message}`, groupIndex = groupIndexByKey.get(key);
|
|
1277
|
+
groupIndex === void 0 && (groupIndex = groups.length, groupIndexByKey.set(key, groupIndex), groups.push({
|
|
1278
|
+
base: degradation.message,
|
|
1279
|
+
entries: []
|
|
1280
|
+
})), groups[groupIndex].entries.push(degradation);
|
|
1281
|
+
}
|
|
1282
|
+
let minLine = (entries) => {
|
|
1283
|
+
let definedLines = entries.map((entry) => entry.line).filter((line) => line !== void 0);
|
|
1284
|
+
return definedLines.length > 0 ? Math.min(...definedLines) : void 0;
|
|
1285
|
+
};
|
|
1286
|
+
return ["Markdown could not be converted without loss:", ...[...groups].sort((a, b) => {
|
|
1287
|
+
let lineA = minLine(a.entries), lineB = minLine(b.entries);
|
|
1288
|
+
return lineA === void 0 ? lineB === void 0 ? 0 : 1 : lineB === void 0 ? -1 : lineA - lineB;
|
|
1289
|
+
}).map((group) => {
|
|
1290
|
+
if (group.entries.length === 1) {
|
|
1291
|
+
let event = group.entries[0], snippetPart = event.snippet === void 0 ? "" : ` ("${event.snippet}")`;
|
|
1292
|
+
return event.line === void 0 ? `- ${event.message}${snippetPart}` : `- line ${event.line}: ${event.message}${snippetPart}`;
|
|
1293
|
+
}
|
|
1294
|
+
let snippets = group.entries.map((entry) => entry.snippet).filter((snippet) => snippet !== void 0), linesAscending = [...new Set(group.entries.map((entry) => entry.line).filter((line) => line !== void 0).sort((a, b) => a - b))], count = group.entries.length, suffix = snippets.length === count ? `(${count}\u00d7: ${capList(snippets.map((snippet) => `"${snippet}"`))})` : linesAscending.length > 0 ? `(${count}\u00d7: lines ${capList(linesAscending.map(String))})` : `(${count}\u00d7)`;
|
|
1295
|
+
return `- ${group.base} ${suffix}`;
|
|
1296
|
+
})].join("\n");
|
|
1297
|
+
}
|
|
1298
|
+
/**
|
|
745
1299
|
* Converts a markdown string to an array of Portable Text blocks.
|
|
746
1300
|
*
|
|
747
1301
|
* @public
|
|
@@ -749,7 +1303,7 @@ function flattenTable(table, portableText) {
|
|
|
749
1303
|
function markdownToPortableText(markdown, options) {
|
|
750
1304
|
let consolidatedOptions = {
|
|
751
1305
|
schema: options?.schema ?? defaultSchema,
|
|
752
|
-
keyGenerator: options?.keyGenerator ?? defaultKeyGenerator,
|
|
1306
|
+
keyGenerator: uniqueKeyGenerator(options?.keyGenerator ?? defaultKeyGenerator),
|
|
753
1307
|
html: { inline: options?.html?.inline ?? "skip" },
|
|
754
1308
|
marks: {
|
|
755
1309
|
...defaultOptions.marks,
|
|
@@ -767,11 +1321,46 @@ function markdownToPortableText(markdown, options) {
|
|
|
767
1321
|
...defaultOptions.types,
|
|
768
1322
|
...options?.types
|
|
769
1323
|
}
|
|
1324
|
+
}, degradationEvents = [], report = (event) => {
|
|
1325
|
+
degradationEvents.push(event);
|
|
1326
|
+
}, lineOf = (candidateToken) => candidateToken?.map ? candidateToken.map[0] + 1 : void 0, reportStyleFallback = (name, line, snippet) => {
|
|
1327
|
+
if (/^h[1-6]$/.test(name)) {
|
|
1328
|
+
let truncated = snippet === void 0 ? void 0 : truncateSnippet(snippet);
|
|
1329
|
+
report({
|
|
1330
|
+
type: "style-fallback",
|
|
1331
|
+
message: degradationMessage["style-fallback"](name),
|
|
1332
|
+
line,
|
|
1333
|
+
snippet: truncated
|
|
1334
|
+
});
|
|
1335
|
+
return;
|
|
1336
|
+
}
|
|
1337
|
+
if (name === "blockquote") {
|
|
1338
|
+
report({
|
|
1339
|
+
type: "style-fallback",
|
|
1340
|
+
message: degradationMessage["style-fallback"](name),
|
|
1341
|
+
line
|
|
1342
|
+
});
|
|
1343
|
+
return;
|
|
1344
|
+
}
|
|
1345
|
+
report({
|
|
1346
|
+
type: "style-fallback",
|
|
1347
|
+
message: degradationMessage["style-fallback"](name),
|
|
1348
|
+
line
|
|
1349
|
+
});
|
|
1350
|
+
}, reportFieldsDropped = (object, line) => {
|
|
1351
|
+
let dropped = readDroppedFields(object);
|
|
1352
|
+
if (!dropped) return;
|
|
1353
|
+
let names = dropped.keys.map((key) => `\`${key}\``).join(", ");
|
|
1354
|
+
report({
|
|
1355
|
+
type: "fields-dropped",
|
|
1356
|
+
message: degradationMessage["fields-dropped"](names, dropped.construct),
|
|
1357
|
+
line
|
|
1358
|
+
});
|
|
770
1359
|
}, tokens = markdownit({
|
|
771
1360
|
html: !0,
|
|
772
1361
|
linkify: !0,
|
|
773
1362
|
typographer: !1
|
|
774
|
-
}).enable(["strikethrough", "table"]).use(alert).parse(markdown, {}), taskCheckedByListItemIndex = /* @__PURE__ */ new Map();
|
|
1363
|
+
}).enable(["strikethrough", "table"]).use(alert).parse(markdown, {}), taskCheckedByListItemIndex = /* @__PURE__ */ new Map(), taskItemTextByListItemIndex = /* @__PURE__ */ new Map();
|
|
775
1364
|
for (let i = 0; i < tokens.length; i++) {
|
|
776
1365
|
if (tokens[i]?.type !== "list_item_open") continue;
|
|
777
1366
|
let inlineIndex = -1;
|
|
@@ -791,11 +1380,11 @@ function markdownToPortableText(markdown, options) {
|
|
|
791
1380
|
let match = inlineToken.content.match(/^\[([ xX])\] /);
|
|
792
1381
|
if (!match) continue;
|
|
793
1382
|
let checked = match[1] !== " ";
|
|
794
|
-
taskCheckedByListItemIndex.set(i, checked), inlineToken.content = inlineToken.content.slice(match[0].length);
|
|
1383
|
+
taskCheckedByListItemIndex.set(i, checked), inlineToken.content = inlineToken.content.slice(match[0].length), taskItemTextByListItemIndex.set(i, inlineToken.content);
|
|
795
1384
|
let firstChild = inlineToken.children?.[0];
|
|
796
1385
|
firstChild && typeof firstChild.content == "string" && (firstChild.content = firstChild.content.slice(match[0].length));
|
|
797
1386
|
}
|
|
798
|
-
let portableText = [], currentBlock = null, currentListStack = [], markDefRefs = [], currentMarkDefs = [], currentBlockquoteStyle = null, inListItem = !1, calloutStartIndex = null, calloutStartTarget = null, calloutType = null, blockquoteStack = [], currentTable = null, currentTableRow = null, inTableHead = !1, listContainerStack = [], blockTarget = () => {
|
|
1387
|
+
let portableText = [], currentBlock = null, currentListStack = [], markDefRefs = [], currentMarkDefs = [], currentBlockquoteStyle = null, inListItem = !1, currentBlockTookBlockquoteStyle = !1, currentBlockIsPlainParagraph = !1, plainParagraphBlocks = /* @__PURE__ */ new WeakSet(), calloutStartIndex = null, calloutStartTarget = null, calloutType = null, calloutStartLine, calloutPendingStyleFallbacks = [], blockquoteStack = [], currentTable = null, currentTableRow = null, inTableHead = !1, demotedTableImages = /* @__PURE__ */ new WeakMap(), listContainerStack = [], pendingListFlattenedStack = [], taskInfoByListItem = /* @__PURE__ */ new WeakMap(), blockTarget = () => {
|
|
799
1388
|
for (let i = listContainerStack.length - 1; i >= 0; i--) {
|
|
800
1389
|
let frame = listContainerStack[i];
|
|
801
1390
|
if (frame && frame.currentItem) return frame.currentItem.content;
|
|
@@ -803,26 +1392,35 @@ function markdownToPortableText(markdown, options) {
|
|
|
803
1392
|
return portableText;
|
|
804
1393
|
}, pushBlock = (block) => {
|
|
805
1394
|
blockTarget().push(block);
|
|
806
|
-
}, startBlock = (style) => {
|
|
1395
|
+
}, startBlock = (style, provenance) => {
|
|
807
1396
|
flushBlock(), currentBlock = {
|
|
808
1397
|
_type: "block",
|
|
809
1398
|
style,
|
|
810
1399
|
children: [],
|
|
811
1400
|
_key: consolidatedOptions.keyGenerator(),
|
|
812
1401
|
markDefs: []
|
|
813
|
-
}, currentMarkDefs = [];
|
|
1402
|
+
}, currentMarkDefs = [], currentBlockTookBlockquoteStyle = provenance?.tookBlockquoteStyle ?? !1, currentBlockIsPlainParagraph = provenance?.isPlainParagraph ?? !1;
|
|
814
1403
|
}, flushBlock = () => {
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
1404
|
+
if (currentBlock) {
|
|
1405
|
+
if (calloutPendingStyleFallbacks.length > 0 && currentBlockTookBlockquoteStyle) {
|
|
1406
|
+
for (let name of calloutPendingStyleFallbacks) reportStyleFallback(name, calloutStartLine);
|
|
1407
|
+
calloutPendingStyleFallbacks = [];
|
|
1408
|
+
}
|
|
1409
|
+
currentBlock.children.length === 0 && currentBlock.children.push({
|
|
1410
|
+
_type: consolidatedOptions.schema.span.name,
|
|
1411
|
+
_key: consolidatedOptions.keyGenerator(),
|
|
1412
|
+
text: "",
|
|
1413
|
+
marks: []
|
|
1414
|
+
}), currentBlock.markDefs = currentMarkDefs, currentBlockIsPlainParagraph && plainParagraphBlocks.add(currentBlock), pushBlock(currentBlock), currentBlock = null, currentMarkDefs = [];
|
|
1415
|
+
}
|
|
821
1416
|
}, addSpan = (text) => {
|
|
822
1417
|
if (text.length === 0) return;
|
|
823
1418
|
if (!currentBlock) {
|
|
824
1419
|
let style = currentBlockquoteStyle ?? consolidatedOptions.block.normal({ context: { schema: consolidatedOptions.schema } });
|
|
825
|
-
style ? startBlock(style
|
|
1420
|
+
style ? startBlock(style, {
|
|
1421
|
+
tookBlockquoteStyle: currentBlockquoteStyle !== null,
|
|
1422
|
+
isPlainParagraph: !0
|
|
1423
|
+
}) : (reportStyleFallback("normal"), startBlock("normal", { isPlainParagraph: !0 }));
|
|
826
1424
|
}
|
|
827
1425
|
if (!currentBlock) throw Error("Expected current block");
|
|
828
1426
|
let lastChild = currentBlock.children.at(-1);
|
|
@@ -835,7 +1433,7 @@ function markdownToPortableText(markdown, options) {
|
|
|
835
1433
|
}, listLevel = () => currentListStack.length, ensureListBlock = (listItem, checked) => {
|
|
836
1434
|
if (!currentBlock) {
|
|
837
1435
|
let style = currentBlockquoteStyle ?? consolidatedOptions.block.normal({ context: { schema: consolidatedOptions.schema } });
|
|
838
|
-
style ? startBlock(style
|
|
1436
|
+
style ? startBlock(style, { tookBlockquoteStyle: currentBlockquoteStyle !== null }) : (reportStyleFallback("normal"), startBlock("normal"));
|
|
839
1437
|
}
|
|
840
1438
|
if (!currentBlock) throw Error("Expected current block");
|
|
841
1439
|
(currentBlock.listItem !== listItem || currentBlock.level !== listLevel()) && (currentBlock.listItem = listItem, currentBlock.level = listLevel()), checked !== void 0 && (currentBlock.checked = checked);
|
|
@@ -846,7 +1444,13 @@ function markdownToPortableText(markdown, options) {
|
|
|
846
1444
|
case "paragraph_open": {
|
|
847
1445
|
if (inListItem) {
|
|
848
1446
|
if (listContainerStack.at(-1)) {
|
|
849
|
-
|
|
1447
|
+
if (!currentBlock) {
|
|
1448
|
+
let style = currentBlockquoteStyle ?? consolidatedOptions.block.normal({ context: { schema: consolidatedOptions.schema } });
|
|
1449
|
+
style || reportStyleFallback("normal", lineOf(token)), startBlock(style ?? "normal", {
|
|
1450
|
+
tookBlockquoteStyle: currentBlockquoteStyle !== null,
|
|
1451
|
+
isPlainParagraph: !0
|
|
1452
|
+
});
|
|
1453
|
+
}
|
|
850
1454
|
break;
|
|
851
1455
|
}
|
|
852
1456
|
if (!currentBlock) {
|
|
@@ -857,10 +1461,13 @@ function markdownToPortableText(markdown, options) {
|
|
|
857
1461
|
}
|
|
858
1462
|
let style = currentBlockquoteStyle ?? consolidatedOptions.block.normal({ context: { schema: consolidatedOptions.schema } });
|
|
859
1463
|
if (!style) {
|
|
860
|
-
|
|
1464
|
+
reportStyleFallback("normal", lineOf(token)), startBlock("normal", { isPlainParagraph: !0 });
|
|
861
1465
|
break;
|
|
862
1466
|
}
|
|
863
|
-
startBlock(style
|
|
1467
|
+
startBlock(style, {
|
|
1468
|
+
tookBlockquoteStyle: currentBlockquoteStyle !== null,
|
|
1469
|
+
isPlainParagraph: !0
|
|
1470
|
+
});
|
|
864
1471
|
break;
|
|
865
1472
|
}
|
|
866
1473
|
case "paragraph_close":
|
|
@@ -878,9 +1485,11 @@ function markdownToPortableText(markdown, options) {
|
|
|
878
1485
|
4: consolidatedOptions.block.h4,
|
|
879
1486
|
5: consolidatedOptions.block.h5,
|
|
880
1487
|
6: consolidatedOptions.block.h6
|
|
881
|
-
}[level],
|
|
1488
|
+
}[level], headingStyle = headingMatcher?.({ context: { schema: consolidatedOptions.schema } });
|
|
1489
|
+
headingStyle || reportStyleFallback(`h${level}`, lineOf(token), tokens[tokenIndex + 1]?.content);
|
|
1490
|
+
let style = headingStyle ?? consolidatedOptions.block.normal({ context: { schema: consolidatedOptions.schema } });
|
|
882
1491
|
if (!style) {
|
|
883
|
-
|
|
1492
|
+
reportStyleFallback("normal", lineOf(token)), startBlock("normal");
|
|
884
1493
|
break;
|
|
885
1494
|
}
|
|
886
1495
|
startBlock(style);
|
|
@@ -889,17 +1498,22 @@ function markdownToPortableText(markdown, options) {
|
|
|
889
1498
|
case "heading_close":
|
|
890
1499
|
flushBlock();
|
|
891
1500
|
break;
|
|
892
|
-
case "blockquote_open":
|
|
1501
|
+
case "blockquote_open": {
|
|
893
1502
|
if (flushBlock(), consolidatedOptions.types.blockquote) {
|
|
894
1503
|
let startTarget = blockTarget();
|
|
895
1504
|
blockquoteStack.push({
|
|
896
1505
|
startTarget,
|
|
897
|
-
startIndex: startTarget.length
|
|
1506
|
+
startIndex: startTarget.length,
|
|
1507
|
+
line: lineOf(token)
|
|
898
1508
|
});
|
|
899
1509
|
break;
|
|
900
1510
|
}
|
|
901
|
-
|
|
1511
|
+
let blockquoteStyle = consolidatedOptions.block.blockquote({ context: { schema: consolidatedOptions.schema } });
|
|
1512
|
+
blockquoteStyle || reportStyleFallback("blockquote", lineOf(token));
|
|
1513
|
+
let style = blockquoteStyle ?? consolidatedOptions.block.normal({ context: { schema: consolidatedOptions.schema } });
|
|
1514
|
+
style || reportStyleFallback("normal", lineOf(token)), currentBlockquoteStyle = style ?? "normal";
|
|
902
1515
|
break;
|
|
1516
|
+
}
|
|
903
1517
|
case "blockquote_close":
|
|
904
1518
|
if (flushBlock(), consolidatedOptions.types.blockquote && blockquoteStack.length > 0) {
|
|
905
1519
|
let frame = blockquoteStack.pop();
|
|
@@ -914,11 +1528,18 @@ function markdownToPortableText(markdown, options) {
|
|
|
914
1528
|
});
|
|
915
1529
|
if (blockquoteObject) pushBlock(blockquoteObject);
|
|
916
1530
|
else {
|
|
917
|
-
let blockquoteStyle = consolidatedOptions.block.blockquote({ context: { schema: consolidatedOptions.schema } })
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
1531
|
+
let blockquoteStyle = consolidatedOptions.block.blockquote({ context: { schema: consolidatedOptions.schema } });
|
|
1532
|
+
blockquoteStyle || reportStyleFallback("blockquote", frame.line);
|
|
1533
|
+
let resolvedStyle = blockquoteStyle ?? consolidatedOptions.block.normal({ context: { schema: consolidatedOptions.schema } });
|
|
1534
|
+
resolvedStyle || reportStyleFallback("normal", frame.line);
|
|
1535
|
+
let fallbackStyle = resolvedStyle ?? "blockquote";
|
|
1536
|
+
for (let block of contentBlocks) if (block._type === "block" && plainParagraphBlocks.has(block)) {
|
|
1537
|
+
let restyledBlock = {
|
|
1538
|
+
...block,
|
|
1539
|
+
style: fallbackStyle
|
|
1540
|
+
};
|
|
1541
|
+
plainParagraphBlocks.add(restyledBlock), pushBlock(restyledBlock);
|
|
1542
|
+
} else pushBlock(block);
|
|
922
1543
|
}
|
|
923
1544
|
}
|
|
924
1545
|
break;
|
|
@@ -930,16 +1551,21 @@ function markdownToPortableText(markdown, options) {
|
|
|
930
1551
|
listContainerStack.push({
|
|
931
1552
|
kind: "bullet",
|
|
932
1553
|
items: [],
|
|
933
|
-
currentItem: null
|
|
934
|
-
|
|
1554
|
+
currentItem: null,
|
|
1555
|
+
line: lineOf(token)
|
|
1556
|
+
}), currentListStack.push(null), pendingListFlattenedStack.push(null);
|
|
935
1557
|
break;
|
|
936
1558
|
}
|
|
937
1559
|
let listItem = consolidatedOptions.listItem.bullet({ context: { schema: consolidatedOptions.schema } });
|
|
938
1560
|
if (listContainerStack.push(null), !listItem) {
|
|
939
|
-
|
|
1561
|
+
pendingListFlattenedStack.push({
|
|
1562
|
+
line: lineOf(token),
|
|
1563
|
+
kindName: "bullet",
|
|
1564
|
+
reported: !1
|
|
1565
|
+
}), currentListStack.push(null);
|
|
940
1566
|
break;
|
|
941
1567
|
}
|
|
942
|
-
currentListStack.push(listItem);
|
|
1568
|
+
pendingListFlattenedStack.push(null), currentListStack.push(listItem);
|
|
943
1569
|
break;
|
|
944
1570
|
}
|
|
945
1571
|
case "ordered_list_open": {
|
|
@@ -947,22 +1573,27 @@ function markdownToPortableText(markdown, options) {
|
|
|
947
1573
|
listContainerStack.push({
|
|
948
1574
|
kind: "number",
|
|
949
1575
|
items: [],
|
|
950
|
-
currentItem: null
|
|
951
|
-
|
|
1576
|
+
currentItem: null,
|
|
1577
|
+
line: lineOf(token)
|
|
1578
|
+
}), currentListStack.push(null), pendingListFlattenedStack.push(null);
|
|
952
1579
|
break;
|
|
953
1580
|
}
|
|
954
1581
|
let listItem = consolidatedOptions.listItem.number({ context: { schema: consolidatedOptions.schema } });
|
|
955
1582
|
if (listContainerStack.push(null), !listItem) {
|
|
956
|
-
|
|
1583
|
+
pendingListFlattenedStack.push({
|
|
1584
|
+
line: lineOf(token),
|
|
1585
|
+
kindName: "number",
|
|
1586
|
+
reported: !1
|
|
1587
|
+
}), currentListStack.push(null);
|
|
957
1588
|
break;
|
|
958
1589
|
}
|
|
959
|
-
currentListStack.push(listItem);
|
|
1590
|
+
pendingListFlattenedStack.push(null), currentListStack.push(listItem);
|
|
960
1591
|
break;
|
|
961
1592
|
}
|
|
962
1593
|
case "bullet_list_close":
|
|
963
1594
|
case "ordered_list_close": {
|
|
964
1595
|
let frame = listContainerStack.pop();
|
|
965
|
-
if (currentListStack.pop(), frame && consolidatedOptions.types.list) {
|
|
1596
|
+
if (currentListStack.pop(), pendingListFlattenedStack.pop(), frame && consolidatedOptions.types.list) {
|
|
966
1597
|
let kind = frame.items.some((item) => "checked" in item) ? "task" : frame.kind, listObject = consolidatedOptions.types.list({
|
|
967
1598
|
context: {
|
|
968
1599
|
schema: consolidatedOptions.schema,
|
|
@@ -976,13 +1607,45 @@ function markdownToPortableText(markdown, options) {
|
|
|
976
1607
|
});
|
|
977
1608
|
if (listObject) pushBlock(listObject);
|
|
978
1609
|
else {
|
|
979
|
-
let
|
|
980
|
-
for (let item of frame.items)
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
1610
|
+
let kindListItem = (frame.kind === "number" ? consolidatedOptions.listItem.number : consolidatedOptions.listItem.bullet)({ context: { schema: consolidatedOptions.schema } }) ?? null, taskListItemType = consolidatedOptions.listItem.task?.({ context: { schema: consolidatedOptions.schema } }) ?? null, kindName = frame.kind === "number" ? "number" : "bullet", level = listContainerStack.length + 1, flattenedReported = !1;
|
|
1611
|
+
for (let item of frame.items) {
|
|
1612
|
+
let itemListType = kindListItem, itemChecked;
|
|
1613
|
+
if (item.checked !== void 0) {
|
|
1614
|
+
if (taskListItemType) itemListType = taskListItemType, itemChecked = item.checked;
|
|
1615
|
+
else if (kindListItem !== null) {
|
|
1616
|
+
let info = taskInfoByListItem.get(item);
|
|
1617
|
+
report({
|
|
1618
|
+
type: "task-checkbox-stripped",
|
|
1619
|
+
message: degradationMessage["task-checkbox-stripped"](item.checked),
|
|
1620
|
+
line: info?.line,
|
|
1621
|
+
snippet: info?.snippet
|
|
1622
|
+
});
|
|
1623
|
+
}
|
|
1624
|
+
}
|
|
1625
|
+
itemListType === null && !flattenedReported && (report({
|
|
1626
|
+
type: "list-flattened",
|
|
1627
|
+
message: degradationMessage["list-flattened"](kindName),
|
|
1628
|
+
line: frame.line
|
|
1629
|
+
}), flattenedReported = !0);
|
|
1630
|
+
let mergeTarget = null;
|
|
1631
|
+
for (let block of item.content) {
|
|
1632
|
+
if (!(itemListType !== null && block._type === "block" && !("listItem" in block) && !/^h[1-6]$/.test(block.style ?? "") && plainParagraphBlocks.has(block))) {
|
|
1633
|
+
mergeTarget = null, pushBlock(block);
|
|
1634
|
+
continue;
|
|
1635
|
+
}
|
|
1636
|
+
let textBlock = block;
|
|
1637
|
+
if (mergeTarget && mergeTarget.style === textBlock.style) {
|
|
1638
|
+
mergeTarget.children.push(...textBlock.children), mergeTarget.markDefs = [...mergeTarget.markDefs ?? [], ...textBlock.markDefs ?? []];
|
|
1639
|
+
continue;
|
|
1640
|
+
}
|
|
1641
|
+
mergeTarget = {
|
|
1642
|
+
...textBlock,
|
|
1643
|
+
listItem: itemListType,
|
|
1644
|
+
level,
|
|
1645
|
+
...itemChecked === void 0 ? {} : { checked: itemChecked }
|
|
1646
|
+
}, pushBlock(mergeTarget);
|
|
1647
|
+
}
|
|
1648
|
+
}
|
|
986
1649
|
}
|
|
987
1650
|
}
|
|
988
1651
|
break;
|
|
@@ -996,7 +1659,10 @@ function markdownToPortableText(markdown, options) {
|
|
|
996
1659
|
_key: consolidatedOptions.keyGenerator(),
|
|
997
1660
|
...taskChecked === void 0 ? {} : { checked: taskChecked },
|
|
998
1661
|
content: []
|
|
999
|
-
},
|
|
1662
|
+
}, taskChecked !== void 0 && taskInfoByListItem.set(frame.currentItem, {
|
|
1663
|
+
line: lineOf(token),
|
|
1664
|
+
snippet: truncateSnippet(taskItemTextByListItemIndex.get(tokenIndex) ?? "")
|
|
1665
|
+
}), inListItem = !0;
|
|
1000
1666
|
break;
|
|
1001
1667
|
}
|
|
1002
1668
|
let baseListType = currentListStack.at(-1);
|
|
@@ -1007,10 +1673,25 @@ function markdownToPortableText(markdown, options) {
|
|
|
1007
1673
|
taskListType && (listType = taskListType, checked = taskChecked);
|
|
1008
1674
|
}
|
|
1009
1675
|
if (listType === null) {
|
|
1676
|
+
let pendingFlattened = pendingListFlattenedStack.at(-1);
|
|
1677
|
+
pendingFlattened && !pendingFlattened.reported && (report({
|
|
1678
|
+
type: "list-flattened",
|
|
1679
|
+
message: degradationMessage["list-flattened"](pendingFlattened.kindName),
|
|
1680
|
+
line: pendingFlattened.line
|
|
1681
|
+
}), pendingFlattened.reported = !0);
|
|
1010
1682
|
let style = currentBlockquoteStyle ?? consolidatedOptions.block.normal({ context: { schema: consolidatedOptions.schema } });
|
|
1011
|
-
style ? startBlock(style
|
|
1683
|
+
style ? startBlock(style, { tookBlockquoteStyle: currentBlockquoteStyle !== null }) : (reportStyleFallback("normal", lineOf(token)), startBlock("normal")), inListItem = !0;
|
|
1012
1684
|
break;
|
|
1013
1685
|
}
|
|
1686
|
+
if (taskChecked !== void 0 && checked === void 0) {
|
|
1687
|
+
let snippet = truncateSnippet(taskItemTextByListItemIndex.get(tokenIndex) ?? "");
|
|
1688
|
+
report({
|
|
1689
|
+
type: "task-checkbox-stripped",
|
|
1690
|
+
message: degradationMessage["task-checkbox-stripped"](taskChecked),
|
|
1691
|
+
line: lineOf(token),
|
|
1692
|
+
snippet
|
|
1693
|
+
});
|
|
1694
|
+
}
|
|
1014
1695
|
ensureListBlock(listType, checked), inListItem = !0;
|
|
1015
1696
|
break;
|
|
1016
1697
|
}
|
|
@@ -1025,7 +1706,21 @@ function markdownToPortableText(markdown, options) {
|
|
|
1025
1706
|
}
|
|
1026
1707
|
case "fence": {
|
|
1027
1708
|
flushBlock();
|
|
1028
|
-
let language = token.info.trim() || void 0, code = token.content.replace(/\n$/, "")
|
|
1709
|
+
let language = token.info.trim() || void 0, code = token.content.replace(/\n$/, "");
|
|
1710
|
+
if (language === "json:object") {
|
|
1711
|
+
let objectValue = parseJsonObjectFence(code);
|
|
1712
|
+
if (objectValue) {
|
|
1713
|
+
pushBlock(objectValue);
|
|
1714
|
+
break;
|
|
1715
|
+
}
|
|
1716
|
+
report({
|
|
1717
|
+
type: "object-carrier-invalid",
|
|
1718
|
+
message: degradationMessage["object-carrier-invalid"]("fence", code),
|
|
1719
|
+
line: lineOf(token),
|
|
1720
|
+
snippet: truncateSnippet(code)
|
|
1721
|
+
});
|
|
1722
|
+
}
|
|
1723
|
+
let codeObject = consolidatedOptions.types.code({
|
|
1029
1724
|
context: {
|
|
1030
1725
|
schema: consolidatedOptions.schema,
|
|
1031
1726
|
keyGenerator: consolidatedOptions.keyGenerator
|
|
@@ -1037,11 +1732,18 @@ function markdownToPortableText(markdown, options) {
|
|
|
1037
1732
|
isInline: !1
|
|
1038
1733
|
});
|
|
1039
1734
|
if (!codeObject) {
|
|
1735
|
+
let snippet = truncateSnippet(code.split("\n")[0] ?? "");
|
|
1736
|
+
report({
|
|
1737
|
+
type: "code-block-to-text",
|
|
1738
|
+
message: degradationMessage["code-block-to-text"](language),
|
|
1739
|
+
line: lineOf(token),
|
|
1740
|
+
snippet
|
|
1741
|
+
});
|
|
1040
1742
|
let style = consolidatedOptions.block.normal({ context: { schema: consolidatedOptions.schema } });
|
|
1041
|
-
style ? startBlock(style) : (
|
|
1743
|
+
style ? startBlock(style) : (reportStyleFallback("normal", lineOf(token)), startBlock("normal")), addSpan(code), flushBlock();
|
|
1042
1744
|
break;
|
|
1043
1745
|
}
|
|
1044
|
-
pushBlock(codeObject);
|
|
1746
|
+
reportFieldsDropped(codeObject, lineOf(token)), pushBlock(codeObject);
|
|
1045
1747
|
break;
|
|
1046
1748
|
}
|
|
1047
1749
|
case "hr": {
|
|
@@ -1055,8 +1757,13 @@ function markdownToPortableText(markdown, options) {
|
|
|
1055
1757
|
isInline: !1
|
|
1056
1758
|
});
|
|
1057
1759
|
if (!hrObject) {
|
|
1760
|
+
report({
|
|
1761
|
+
type: "horizontal-rule-to-text",
|
|
1762
|
+
message: degradationMessage["horizontal-rule-to-text"],
|
|
1763
|
+
line: lineOf(token)
|
|
1764
|
+
});
|
|
1058
1765
|
let style = consolidatedOptions.block.normal({ context: { schema: consolidatedOptions.schema } });
|
|
1059
|
-
style ? startBlock(style) : (
|
|
1766
|
+
style ? startBlock(style) : (reportStyleFallback("normal", lineOf(token)), startBlock("normal")), addSpan("---"), flushBlock();
|
|
1060
1767
|
break;
|
|
1061
1768
|
}
|
|
1062
1769
|
pushBlock(hrObject);
|
|
@@ -1075,11 +1782,18 @@ function markdownToPortableText(markdown, options) {
|
|
|
1075
1782
|
isInline: !1
|
|
1076
1783
|
});
|
|
1077
1784
|
if (!htmlObject) {
|
|
1785
|
+
let snippet = truncateSnippet(htmlContent);
|
|
1786
|
+
report({
|
|
1787
|
+
type: "html-block-to-text",
|
|
1788
|
+
message: degradationMessage["html-block-to-text"],
|
|
1789
|
+
line: lineOf(token),
|
|
1790
|
+
snippet
|
|
1791
|
+
});
|
|
1078
1792
|
let style = consolidatedOptions.block.normal({ context: { schema: consolidatedOptions.schema } });
|
|
1079
|
-
style ? startBlock(style) : (
|
|
1793
|
+
style ? startBlock(style) : (reportStyleFallback("normal", lineOf(token)), startBlock("normal")), addSpan(htmlContent), flushBlock();
|
|
1080
1794
|
break;
|
|
1081
1795
|
}
|
|
1082
|
-
pushBlock(htmlObject);
|
|
1796
|
+
reportFieldsDropped(htmlObject, lineOf(token)), pushBlock(htmlObject);
|
|
1083
1797
|
break;
|
|
1084
1798
|
}
|
|
1085
1799
|
case "code_block": {
|
|
@@ -1095,10 +1809,17 @@ function markdownToPortableText(markdown, options) {
|
|
|
1095
1809
|
},
|
|
1096
1810
|
isInline: !1
|
|
1097
1811
|
});
|
|
1098
|
-
if (codeObject) pushBlock(codeObject);
|
|
1812
|
+
if (codeObject) reportFieldsDropped(codeObject, lineOf(token)), pushBlock(codeObject);
|
|
1099
1813
|
else {
|
|
1814
|
+
let snippet = truncateSnippet(code.split("\n")[0] ?? "");
|
|
1815
|
+
report({
|
|
1816
|
+
type: "code-block-to-text",
|
|
1817
|
+
message: degradationMessage["code-block-to-text"](void 0),
|
|
1818
|
+
line: lineOf(token),
|
|
1819
|
+
snippet
|
|
1820
|
+
});
|
|
1100
1821
|
let style = consolidatedOptions.block.normal({ context: { schema: consolidatedOptions.schema } });
|
|
1101
|
-
style ? startBlock(style) : (
|
|
1822
|
+
style ? startBlock(style) : (reportStyleFallback("normal", lineOf(token)), startBlock("normal")), addSpan(code), flushBlock();
|
|
1102
1823
|
}
|
|
1103
1824
|
break;
|
|
1104
1825
|
}
|
|
@@ -1107,7 +1828,8 @@ function markdownToPortableText(markdown, options) {
|
|
|
1107
1828
|
rows: [],
|
|
1108
1829
|
headerRows: 0,
|
|
1109
1830
|
emptyHeaderDropped: !1,
|
|
1110
|
-
alignment: []
|
|
1831
|
+
alignment: [],
|
|
1832
|
+
line: lineOf(token)
|
|
1111
1833
|
};
|
|
1112
1834
|
break;
|
|
1113
1835
|
case "table_close":
|
|
@@ -1125,8 +1847,16 @@ function markdownToPortableText(markdown, options) {
|
|
|
1125
1847
|
},
|
|
1126
1848
|
isInline: !1
|
|
1127
1849
|
});
|
|
1128
|
-
tableObject ? pushBlock(tableObject) :
|
|
1129
|
-
|
|
1850
|
+
tableObject ? (reportFieldsDropped(tableObject, currentTable.line), pushBlock(tableObject)) : (report({
|
|
1851
|
+
type: "table-flattened",
|
|
1852
|
+
message: degradationMessage["table-flattened"],
|
|
1853
|
+
line: currentTable.line
|
|
1854
|
+
}), flattenTable(currentTable, blockTarget()));
|
|
1855
|
+
} else report({
|
|
1856
|
+
type: "table-flattened",
|
|
1857
|
+
message: degradationMessage["table-flattened"],
|
|
1858
|
+
line: currentTable.line
|
|
1859
|
+
}), flattenTable(currentTable, blockTarget());
|
|
1130
1860
|
currentTable = null;
|
|
1131
1861
|
break;
|
|
1132
1862
|
case "thead_open":
|
|
@@ -1151,7 +1881,7 @@ function markdownToPortableText(markdown, options) {
|
|
|
1151
1881
|
case "td_open": {
|
|
1152
1882
|
currentTable && inTableHead && token.type === "th_open" && currentTable.alignment.push(extractAlignmentFromStyleAttr(token.attrGet("style")));
|
|
1153
1883
|
let style = consolidatedOptions.block.normal({ context: { schema: consolidatedOptions.schema } });
|
|
1154
|
-
style ? startBlock(style) : (
|
|
1884
|
+
style ? startBlock(style) : (reportStyleFallback("normal", currentTable?.line), startBlock("normal"));
|
|
1155
1885
|
break;
|
|
1156
1886
|
}
|
|
1157
1887
|
case "th_close":
|
|
@@ -1174,10 +1904,24 @@ function markdownToPortableText(markdown, options) {
|
|
|
1174
1904
|
_key: consolidatedOptions.keyGenerator(),
|
|
1175
1905
|
markDefs: []
|
|
1176
1906
|
});
|
|
1177
|
-
let
|
|
1907
|
+
let demotedInlineImages = [];
|
|
1908
|
+
for (let block of cellBlocks) if (block._type === "block" && "children" in block && Array.isArray(block.children)) for (let child of block.children) typeof child == "object" && child && demotedTableImages.has(child) && demotedInlineImages.push(child);
|
|
1909
|
+
let firstBlock = cellBlocks[0], liftedObject;
|
|
1178
1910
|
if (cellBlocks.length === 1 && firstBlock && firstBlock._type === "block" && "children" in firstBlock && Array.isArray(firstBlock.children) && firstBlock.children.length === 1) {
|
|
1179
1911
|
let onlyChild = firstBlock.children[0];
|
|
1180
|
-
typeof onlyChild == "object" && onlyChild && "_type" in onlyChild && onlyChild._type !== consolidatedOptions.schema.span.name
|
|
1912
|
+
if (typeof onlyChild == "object" && onlyChild && "_type" in onlyChild && onlyChild._type !== consolidatedOptions.schema.span.name) {
|
|
1913
|
+
let declaredInline = consolidatedOptions.schema.inlineObjects.some((inlineObject) => inlineObject.name === onlyChild._type), declaredBlock = consolidatedOptions.schema.blockObjects.some((blockObject) => blockObject.name === onlyChild._type);
|
|
1914
|
+
declaredInline && !declaredBlock || (cellBlocks[0] = onlyChild, liftedObject = onlyChild);
|
|
1915
|
+
}
|
|
1916
|
+
}
|
|
1917
|
+
for (let demotedImage of demotedInlineImages) if (demotedImage !== liftedObject) {
|
|
1918
|
+
let { alt, src } = demotedImage;
|
|
1919
|
+
report({
|
|
1920
|
+
type: "image-block-to-inline",
|
|
1921
|
+
message: degradationMessage["image-block-to-inline"](demotedTableImages.get(demotedImage) ?? "table-cell"),
|
|
1922
|
+
line: currentTable?.line,
|
|
1923
|
+
snippet: truncateSnippet(alt || src || "")
|
|
1924
|
+
});
|
|
1181
1925
|
}
|
|
1182
1926
|
currentTableRow !== null && currentTableRow.push({
|
|
1183
1927
|
_type: "cell",
|
|
@@ -1187,7 +1931,7 @@ function markdownToPortableText(markdown, options) {
|
|
|
1187
1931
|
break;
|
|
1188
1932
|
}
|
|
1189
1933
|
case "inline": {
|
|
1190
|
-
let inTableCell = currentTableRow !== null;
|
|
1934
|
+
let inTableCell = currentTableRow !== null, inlineLine = () => lineOf(token) ?? currentTable?.line;
|
|
1191
1935
|
if (token.children?.length === 1 && token.children[0]?.type === "image") {
|
|
1192
1936
|
let imageToken = token.children[0];
|
|
1193
1937
|
if (!imageToken) break;
|
|
@@ -1204,7 +1948,7 @@ function markdownToPortableText(markdown, options) {
|
|
|
1204
1948
|
isInline: !1
|
|
1205
1949
|
});
|
|
1206
1950
|
if (blockImageObject) {
|
|
1207
|
-
inTableCell ? currentBlock && "children" in currentBlock && currentBlock.children.push(blockImageObject) : (currentBlock && "children" in currentBlock && currentBlock.children.length > 0 ? flushBlock() : (currentBlock = null, currentMarkDefs = []), pushBlock(blockImageObject));
|
|
1951
|
+
reportFieldsDropped(blockImageObject, inlineLine()), inTableCell ? (demotedTableImages.set(blockImageObject, "table-cell"), currentBlock && "children" in currentBlock && currentBlock.children.push(blockImageObject)) : (currentBlock && "children" in currentBlock && currentBlock.children.length > 0 ? flushBlock() : (currentBlock = null, currentMarkDefs = []), pushBlock(blockImageObject));
|
|
1208
1952
|
break;
|
|
1209
1953
|
}
|
|
1210
1954
|
let inlineImageObject = consolidatedOptions.types.image({
|
|
@@ -1220,165 +1964,281 @@ function markdownToPortableText(markdown, options) {
|
|
|
1220
1964
|
isInline: !0
|
|
1221
1965
|
});
|
|
1222
1966
|
if (inlineImageObject) {
|
|
1223
|
-
if (
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1967
|
+
if (reportFieldsDropped(inlineImageObject, inlineLine()), inTableCell ? demotedTableImages.set(inlineImageObject, "no-block-image") : report({
|
|
1968
|
+
type: "image-block-to-inline",
|
|
1969
|
+
message: degradationMessage["image-block-to-inline"]("no-block-image"),
|
|
1970
|
+
line: inlineLine(),
|
|
1971
|
+
snippet: truncateSnippet(alt || src)
|
|
1972
|
+
}), !currentBlock) {
|
|
1973
|
+
if (inListItem) {
|
|
1974
|
+
if (listContainerStack.at(-1)) {
|
|
1975
|
+
let style = consolidatedOptions.block.normal({ context: { schema: consolidatedOptions.schema } });
|
|
1976
|
+
style || reportStyleFallback("normal", inlineLine()), startBlock(style ?? "normal");
|
|
1977
|
+
} else {
|
|
1978
|
+
let listType = currentListStack.at(-1);
|
|
1979
|
+
listType && ensureListBlock(listType);
|
|
1980
|
+
}
|
|
1981
|
+
} else {
|
|
1982
|
+
let style = consolidatedOptions.block.normal({ context: { schema: consolidatedOptions.schema } });
|
|
1983
|
+
style && startBlock(style);
|
|
1984
|
+
}
|
|
1231
1985
|
}
|
|
1232
1986
|
currentBlock && "children" in currentBlock && currentBlock.children.push(inlineImageObject);
|
|
1233
1987
|
break;
|
|
1234
1988
|
}
|
|
1235
|
-
|
|
1989
|
+
let standaloneImageSnippet = truncateSnippet(alt || src);
|
|
1990
|
+
report({
|
|
1991
|
+
type: "image-to-text",
|
|
1992
|
+
message: degradationMessage["image-to-text"],
|
|
1993
|
+
line: inlineLine(),
|
|
1994
|
+
snippet: standaloneImageSnippet
|
|
1995
|
+
}), addSpan(``);
|
|
1236
1996
|
break;
|
|
1237
1997
|
}
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1998
|
+
let inlineChildren = token.children ?? [];
|
|
1999
|
+
for (let childIndex = 0; childIndex < inlineChildren.length; childIndex++) {
|
|
2000
|
+
let childToken = inlineChildren[childIndex];
|
|
2001
|
+
if (childToken) switch (childToken.type) {
|
|
2002
|
+
case "text": {
|
|
2003
|
+
let nextToken = inlineChildren[childIndex + 1];
|
|
2004
|
+
if (childToken.content.endsWith("json:object") && nextToken?.type === "code_inline" && currentBlock && "children" in currentBlock) {
|
|
2005
|
+
let objectValue = parseJsonObjectFence(nextToken.content);
|
|
2006
|
+
if (objectValue) {
|
|
2007
|
+
let prefix = childToken.content.slice(0, -11);
|
|
2008
|
+
prefix.length > 0 && addSpan(prefix), currentBlock.children.push(objectValue), childIndex++;
|
|
2009
|
+
break;
|
|
2010
|
+
}
|
|
2011
|
+
report({
|
|
2012
|
+
type: "object-carrier-invalid",
|
|
2013
|
+
message: degradationMessage["object-carrier-invalid"]("code-span", nextToken.content),
|
|
2014
|
+
line: inlineLine(),
|
|
2015
|
+
snippet: truncateSnippet(nextToken.content)
|
|
2016
|
+
});
|
|
2017
|
+
}
|
|
1249
2018
|
addSpan(childToken.content);
|
|
1250
2019
|
break;
|
|
1251
2020
|
}
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
}
|
|
1270
|
-
case "em_open": {
|
|
1271
|
-
let decorator = consolidatedOptions.marks.em({ context: { schema: consolidatedOptions.schema } });
|
|
1272
|
-
if (!decorator) break;
|
|
1273
|
-
markDefRefs.push(decorator);
|
|
1274
|
-
break;
|
|
1275
|
-
}
|
|
1276
|
-
case "em_close": {
|
|
1277
|
-
let decorator = consolidatedOptions.marks.em({ context: { schema: consolidatedOptions.schema } });
|
|
1278
|
-
if (!decorator) break;
|
|
1279
|
-
let index = markDefRefs.lastIndexOf(decorator);
|
|
1280
|
-
index !== -1 && markDefRefs.splice(index, 1);
|
|
1281
|
-
break;
|
|
1282
|
-
}
|
|
1283
|
-
case "s_open": {
|
|
1284
|
-
let decorator = consolidatedOptions.marks.strikeThrough({ context: { schema: consolidatedOptions.schema } });
|
|
1285
|
-
if (!decorator) break;
|
|
1286
|
-
markDefRefs.push(decorator);
|
|
1287
|
-
break;
|
|
1288
|
-
}
|
|
1289
|
-
case "s_close": {
|
|
1290
|
-
let decorator = consolidatedOptions.marks.strikeThrough({ context: { schema: consolidatedOptions.schema } });
|
|
1291
|
-
if (!decorator) break;
|
|
1292
|
-
let index = markDefRefs.lastIndexOf(decorator);
|
|
1293
|
-
index !== -1 && markDefRefs.splice(index, 1);
|
|
1294
|
-
break;
|
|
1295
|
-
}
|
|
1296
|
-
case "link_open": {
|
|
1297
|
-
let href = childToken.attrs?.find(([name]) => name === "href")?.at(1);
|
|
1298
|
-
if (!href) break;
|
|
1299
|
-
let title = childToken.attrs?.find(([name]) => name === "title")?.at(1), linkObject = consolidatedOptions.marks.link({
|
|
1300
|
-
context: {
|
|
1301
|
-
schema: consolidatedOptions.schema,
|
|
1302
|
-
keyGenerator: consolidatedOptions.keyGenerator
|
|
1303
|
-
},
|
|
1304
|
-
value: {
|
|
1305
|
-
href,
|
|
1306
|
-
title
|
|
2021
|
+
case "softbreak":
|
|
2022
|
+
addSpan(" ");
|
|
2023
|
+
break;
|
|
2024
|
+
case "hardbreak":
|
|
2025
|
+
addSpan("\n");
|
|
2026
|
+
break;
|
|
2027
|
+
case "code_inline": {
|
|
2028
|
+
let decorator = consolidatedOptions.marks.code({ context: { schema: consolidatedOptions.schema } });
|
|
2029
|
+
if (!decorator) {
|
|
2030
|
+
let codeSnippet = truncateSnippet(childToken.content);
|
|
2031
|
+
report({
|
|
2032
|
+
type: "decorator-dropped",
|
|
2033
|
+
message: degradationMessage["decorator-dropped"]("code"),
|
|
2034
|
+
line: inlineLine(),
|
|
2035
|
+
snippet: codeSnippet
|
|
2036
|
+
}), addSpan(childToken.content);
|
|
2037
|
+
break;
|
|
1307
2038
|
}
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
2039
|
+
markDefRefs.push(decorator), addSpan(childToken.content);
|
|
2040
|
+
let index = markDefRefs.lastIndexOf(decorator);
|
|
2041
|
+
index !== -1 && markDefRefs.splice(index, 1);
|
|
2042
|
+
break;
|
|
2043
|
+
}
|
|
2044
|
+
case "strong_open": {
|
|
2045
|
+
let decorator = consolidatedOptions.marks.strong({ context: { schema: consolidatedOptions.schema } });
|
|
2046
|
+
if (!decorator) {
|
|
2047
|
+
let strongSnippet = truncateSnippet(collectInlineText(inlineChildren, childIndex, "strong_open", "strong_close"));
|
|
2048
|
+
report({
|
|
2049
|
+
type: "decorator-dropped",
|
|
2050
|
+
message: degradationMessage["decorator-dropped"]("strong"),
|
|
2051
|
+
line: inlineLine(),
|
|
2052
|
+
snippet: strongSnippet
|
|
2053
|
+
});
|
|
2054
|
+
break;
|
|
2055
|
+
}
|
|
2056
|
+
markDefRefs.push(decorator);
|
|
1317
2057
|
break;
|
|
1318
2058
|
}
|
|
1319
|
-
|
|
1320
|
-
let
|
|
1321
|
-
|
|
2059
|
+
case "strong_close": {
|
|
2060
|
+
let decorator = consolidatedOptions.marks.strong({ context: { schema: consolidatedOptions.schema } });
|
|
2061
|
+
if (!decorator) break;
|
|
2062
|
+
let index = markDefRefs.lastIndexOf(decorator);
|
|
2063
|
+
index !== -1 && markDefRefs.splice(index, 1);
|
|
2064
|
+
break;
|
|
1322
2065
|
}
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
title: void 0
|
|
1335
|
-
},
|
|
1336
|
-
isInline: !0
|
|
1337
|
-
});
|
|
1338
|
-
if (inlineImageObject) {
|
|
1339
|
-
if (!currentBlock) {
|
|
1340
|
-
let style = consolidatedOptions.block.normal({ context: { schema: consolidatedOptions.schema } });
|
|
1341
|
-
style ? startBlock(style) : (console.warn("No default style found, using \"normal\""), startBlock("normal"));
|
|
2066
|
+
case "em_open": {
|
|
2067
|
+
let decorator = consolidatedOptions.marks.em({ context: { schema: consolidatedOptions.schema } });
|
|
2068
|
+
if (!decorator) {
|
|
2069
|
+
let emSnippet = truncateSnippet(collectInlineText(inlineChildren, childIndex, "em_open", "em_close"));
|
|
2070
|
+
report({
|
|
2071
|
+
type: "decorator-dropped",
|
|
2072
|
+
message: degradationMessage["decorator-dropped"]("em"),
|
|
2073
|
+
line: inlineLine(),
|
|
2074
|
+
snippet: emSnippet
|
|
2075
|
+
});
|
|
2076
|
+
break;
|
|
1342
2077
|
}
|
|
1343
|
-
|
|
1344
|
-
currentBlock.children.push(inlineImageObject);
|
|
2078
|
+
markDefRefs.push(decorator);
|
|
1345
2079
|
break;
|
|
1346
2080
|
}
|
|
1347
|
-
|
|
1348
|
-
context: {
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
value: {
|
|
1353
|
-
src,
|
|
1354
|
-
alt,
|
|
1355
|
-
title: void 0
|
|
1356
|
-
},
|
|
1357
|
-
isInline: !1
|
|
1358
|
-
});
|
|
1359
|
-
if (!blockImageObject) {
|
|
1360
|
-
addSpan(``);
|
|
2081
|
+
case "em_close": {
|
|
2082
|
+
let decorator = consolidatedOptions.marks.em({ context: { schema: consolidatedOptions.schema } });
|
|
2083
|
+
if (!decorator) break;
|
|
2084
|
+
let index = markDefRefs.lastIndexOf(decorator);
|
|
2085
|
+
index !== -1 && markDefRefs.splice(index, 1);
|
|
1361
2086
|
break;
|
|
1362
2087
|
}
|
|
1363
|
-
|
|
1364
|
-
|
|
2088
|
+
case "s_open": {
|
|
2089
|
+
let decorator = consolidatedOptions.marks.strikeThrough({ context: { schema: consolidatedOptions.schema } });
|
|
2090
|
+
if (!decorator) {
|
|
2091
|
+
let strikeSnippet = truncateSnippet(collectInlineText(inlineChildren, childIndex, "s_open", "s_close"));
|
|
2092
|
+
report({
|
|
2093
|
+
type: "decorator-dropped",
|
|
2094
|
+
message: degradationMessage["decorator-dropped"]("strikeThrough"),
|
|
2095
|
+
line: inlineLine(),
|
|
2096
|
+
snippet: strikeSnippet
|
|
2097
|
+
});
|
|
2098
|
+
break;
|
|
2099
|
+
}
|
|
2100
|
+
markDefRefs.push(decorator);
|
|
1365
2101
|
break;
|
|
1366
2102
|
}
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
2103
|
+
case "s_close": {
|
|
2104
|
+
let decorator = consolidatedOptions.marks.strikeThrough({ context: { schema: consolidatedOptions.schema } });
|
|
2105
|
+
if (!decorator) break;
|
|
2106
|
+
let index = markDefRefs.lastIndexOf(decorator);
|
|
2107
|
+
index !== -1 && markDefRefs.splice(index, 1);
|
|
2108
|
+
break;
|
|
2109
|
+
}
|
|
2110
|
+
case "link_open": {
|
|
2111
|
+
let href = childToken.attrs?.find(([name]) => name === "href")?.at(1);
|
|
2112
|
+
if (!href) {
|
|
2113
|
+
let missingHrefSnippet = truncateSnippet(collectInlineText(inlineChildren, childIndex, "link_open", "link_close"));
|
|
2114
|
+
report({
|
|
2115
|
+
type: "annotation-dropped",
|
|
2116
|
+
message: degradationMessage["annotation-dropped"]("missing-url"),
|
|
2117
|
+
line: inlineLine(),
|
|
2118
|
+
snippet: missingHrefSnippet
|
|
2119
|
+
});
|
|
2120
|
+
break;
|
|
2121
|
+
}
|
|
2122
|
+
let title = childToken.attrs?.find(([name]) => name === "title")?.at(1), linkObject = consolidatedOptions.marks.link({
|
|
2123
|
+
context: {
|
|
2124
|
+
schema: consolidatedOptions.schema,
|
|
2125
|
+
keyGenerator: consolidatedOptions.keyGenerator
|
|
2126
|
+
},
|
|
2127
|
+
value: {
|
|
2128
|
+
href,
|
|
2129
|
+
title
|
|
2130
|
+
}
|
|
2131
|
+
});
|
|
2132
|
+
if (!linkObject) {
|
|
2133
|
+
let linkSnippet = truncateSnippet(collectInlineText(inlineChildren, childIndex, "link_open", "link_close"));
|
|
2134
|
+
report({
|
|
2135
|
+
type: "annotation-dropped",
|
|
2136
|
+
message: degradationMessage["annotation-dropped"]("no-annotation"),
|
|
2137
|
+
line: inlineLine(),
|
|
2138
|
+
snippet: linkSnippet
|
|
2139
|
+
});
|
|
2140
|
+
break;
|
|
2141
|
+
}
|
|
2142
|
+
reportFieldsDropped(linkObject, inlineLine()), currentMarkDefs.push(linkObject), markDefRefs.push(linkObject._key);
|
|
2143
|
+
break;
|
|
2144
|
+
}
|
|
2145
|
+
case "link_close": {
|
|
2146
|
+
let markDefKeys = new Set(currentMarkDefs.map((d) => d._key)), lastLinkIndex;
|
|
2147
|
+
for (let markDefRef of markDefRefs.reverse()) if (markDefKeys.has(markDefRef)) {
|
|
2148
|
+
lastLinkIndex = markDefRefs.indexOf(markDefRef);
|
|
2149
|
+
break;
|
|
2150
|
+
}
|
|
2151
|
+
if (lastLinkIndex !== void 0) {
|
|
2152
|
+
let realIndex = markDefRefs.length - 1 - lastLinkIndex;
|
|
2153
|
+
markDefRefs.splice(realIndex, 1);
|
|
2154
|
+
}
|
|
2155
|
+
break;
|
|
2156
|
+
}
|
|
2157
|
+
case "image": {
|
|
2158
|
+
let src = childToken.attrs?.find(([name]) => name === "src")?.at(1) || "", alt = unescapeImageAndLinkText(childToken.content || ""), inlineImageObject = consolidatedOptions.types.image({
|
|
2159
|
+
context: {
|
|
2160
|
+
schema: consolidatedOptions.schema,
|
|
2161
|
+
keyGenerator: consolidatedOptions.keyGenerator
|
|
2162
|
+
},
|
|
2163
|
+
value: {
|
|
2164
|
+
src,
|
|
2165
|
+
alt,
|
|
2166
|
+
title: void 0
|
|
2167
|
+
},
|
|
2168
|
+
isInline: !0
|
|
2169
|
+
});
|
|
2170
|
+
if (inlineImageObject) {
|
|
2171
|
+
if (reportFieldsDropped(inlineImageObject, inlineLine()), !currentBlock) {
|
|
2172
|
+
let style = consolidatedOptions.block.normal({ context: { schema: consolidatedOptions.schema } });
|
|
2173
|
+
style ? startBlock(style) : (reportStyleFallback("normal", inlineLine()), startBlock("normal"));
|
|
2174
|
+
}
|
|
2175
|
+
if (!currentBlock) throw Error("Expected current block after startBlock");
|
|
2176
|
+
currentBlock.children.push(inlineImageObject);
|
|
2177
|
+
break;
|
|
2178
|
+
}
|
|
2179
|
+
let blockImageObject = consolidatedOptions.types.image({
|
|
2180
|
+
context: {
|
|
2181
|
+
schema: consolidatedOptions.schema,
|
|
2182
|
+
keyGenerator: consolidatedOptions.keyGenerator
|
|
2183
|
+
},
|
|
2184
|
+
value: {
|
|
2185
|
+
src,
|
|
2186
|
+
alt,
|
|
2187
|
+
title: void 0
|
|
2188
|
+
},
|
|
2189
|
+
isInline: !1
|
|
2190
|
+
});
|
|
2191
|
+
if (!blockImageObject) {
|
|
2192
|
+
let inlineImageSnippet = truncateSnippet(alt || src);
|
|
2193
|
+
report({
|
|
2194
|
+
type: "image-to-text",
|
|
2195
|
+
message: degradationMessage["image-to-text"],
|
|
2196
|
+
line: inlineLine(),
|
|
2197
|
+
snippet: inlineImageSnippet
|
|
2198
|
+
}), addSpan(``);
|
|
2199
|
+
break;
|
|
2200
|
+
}
|
|
2201
|
+
if (inTableCell) {
|
|
2202
|
+
reportFieldsDropped(blockImageObject, inlineLine()), demotedTableImages.set(blockImageObject, "table-cell"), currentBlock && "children" in currentBlock && currentBlock.children.push(blockImageObject);
|
|
2203
|
+
break;
|
|
2204
|
+
}
|
|
2205
|
+
reportFieldsDropped(blockImageObject, inlineLine()), report({
|
|
2206
|
+
type: "image-inline-to-block",
|
|
2207
|
+
message: degradationMessage["image-inline-to-block"],
|
|
2208
|
+
line: inlineLine(),
|
|
2209
|
+
snippet: truncateSnippet(alt || src)
|
|
2210
|
+
}), flushBlock(), pushBlock(blockImageObject);
|
|
2211
|
+
let style = consolidatedOptions.block.normal({ context: { schema: consolidatedOptions.schema } });
|
|
2212
|
+
style && startBlock(style);
|
|
2213
|
+
break;
|
|
2214
|
+
}
|
|
2215
|
+
case "html_inline": if (consolidatedOptions.html.inline === "text") addSpan(childToken.content);
|
|
2216
|
+
else if (childToken.content) {
|
|
2217
|
+
let htmlInlineSnippet = truncateSnippet(childToken.content);
|
|
2218
|
+
report({
|
|
2219
|
+
type: "inline-html-dropped",
|
|
2220
|
+
message: degradationMessage["inline-html-dropped"],
|
|
2221
|
+
line: inlineLine(),
|
|
2222
|
+
snippet: htmlInlineSnippet
|
|
2223
|
+
});
|
|
2224
|
+
}
|
|
1371
2225
|
}
|
|
1372
|
-
case "html_inline": consolidatedOptions.html.inline === "text" && addSpan(childToken.content);
|
|
1373
2226
|
}
|
|
1374
2227
|
break;
|
|
1375
2228
|
}
|
|
1376
|
-
case "alert_open":
|
|
1377
|
-
flushBlock(), calloutStartTarget = blockTarget(), calloutStartIndex = calloutStartTarget.length, calloutType = token.markup,
|
|
2229
|
+
case "alert_open": {
|
|
2230
|
+
flushBlock(), calloutStartTarget = blockTarget(), calloutStartIndex = calloutStartTarget.length, calloutType = token.markup, calloutStartLine = lineOf(token);
|
|
2231
|
+
let blockquoteStyle = consolidatedOptions.block.blockquote({ context: { schema: consolidatedOptions.schema } });
|
|
2232
|
+
calloutPendingStyleFallbacks = [], blockquoteStyle || calloutPendingStyleFallbacks.push("blockquote");
|
|
2233
|
+
let style = blockquoteStyle ?? consolidatedOptions.block.normal({ context: { schema: consolidatedOptions.schema } });
|
|
2234
|
+
style || calloutPendingStyleFallbacks.push("normal"), currentBlockquoteStyle = style ?? "normal";
|
|
2235
|
+
break;
|
|
2236
|
+
}
|
|
2237
|
+
case "alert_title":
|
|
2238
|
+
calloutStartLine = lineOf(token);
|
|
1378
2239
|
break;
|
|
1379
|
-
case "alert_title": break;
|
|
1380
2240
|
case "alert_close":
|
|
1381
|
-
if (flushBlock(), calloutStartIndex !== null && calloutType !== null && calloutStartTarget !== null) {
|
|
2241
|
+
if (flushBlock(), calloutPendingStyleFallbacks = [], calloutStartIndex !== null && calloutType !== null && calloutStartTarget !== null) {
|
|
1382
2242
|
let contentBlocks = calloutStartTarget.splice(calloutStartIndex), calloutObject = consolidatedOptions.types.callout?.({
|
|
1383
2243
|
context: {
|
|
1384
2244
|
schema: consolidatedOptions.schema,
|
|
@@ -1390,14 +2250,864 @@ function markdownToPortableText(markdown, options) {
|
|
|
1390
2250
|
},
|
|
1391
2251
|
isInline: !1
|
|
1392
2252
|
});
|
|
1393
|
-
if (calloutObject) pushBlock(calloutObject);
|
|
1394
|
-
else
|
|
2253
|
+
if (calloutObject) reportFieldsDropped(calloutObject, calloutStartLine), pushBlock(calloutObject);
|
|
2254
|
+
else {
|
|
2255
|
+
report({
|
|
2256
|
+
type: "callout-fallback",
|
|
2257
|
+
message: degradationMessage["callout-fallback"](calloutType, currentBlockquoteStyle ?? "normal"),
|
|
2258
|
+
line: calloutStartLine
|
|
2259
|
+
});
|
|
2260
|
+
for (let block of contentBlocks) pushBlock(block);
|
|
2261
|
+
}
|
|
1395
2262
|
}
|
|
1396
|
-
calloutStartIndex = null, calloutStartTarget = null, calloutType = null, currentBlockquoteStyle = null;
|
|
2263
|
+
calloutStartIndex = null, calloutStartTarget = null, calloutType = null, calloutStartLine = void 0, currentBlockquoteStyle = null;
|
|
2264
|
+
}
|
|
2265
|
+
}
|
|
2266
|
+
return flushBlock(), degradationEvents.length > 0 && options?.onDegradation?.({
|
|
2267
|
+
degradations: degradationEvents,
|
|
2268
|
+
message: buildDegradationMessage(degradationEvents)
|
|
2269
|
+
}), portableText;
|
|
2270
|
+
}
|
|
2271
|
+
/**
|
|
2272
|
+
* A `json:object` fence always reconstructs its object, schema or no
|
|
2273
|
+
* schema: the fence carries its own `_type`, and degrading it to a code
|
|
2274
|
+
* block would reintroduce the loss the syntax exists to remove. Returns
|
|
2275
|
+
* `undefined` instead of throwing, so an unusable fence falls through to
|
|
2276
|
+
* the regular code path.
|
|
2277
|
+
*/
|
|
2278
|
+
function parseJsonObjectFence(code) {
|
|
2279
|
+
let parsed;
|
|
2280
|
+
try {
|
|
2281
|
+
parsed = JSON.parse(code);
|
|
2282
|
+
} catch {
|
|
2283
|
+
return;
|
|
2284
|
+
}
|
|
2285
|
+
if (typeof parsed != "object" || !parsed || Array.isArray(parsed)) return;
|
|
2286
|
+
let objectValue = parsed;
|
|
2287
|
+
if (typeof objectValue._type == "string" && objectValue._type.length !== 0) return objectValue;
|
|
2288
|
+
}
|
|
2289
|
+
/**
|
|
2290
|
+
* Deliberately high pending calibration against real agent edit
|
|
2291
|
+
* traces: a wrong match moves anchors onto unrelated text, a fresh
|
|
2292
|
+
* key resets one block.
|
|
2293
|
+
*/
|
|
2294
|
+
const MIN_BLOCK_SIMILARITY = .8, MAX_SIMILARITY_PAIRS = 2500;
|
|
2295
|
+
/**
|
|
2296
|
+
* Converts edited markdown to Portable Text, restores stored keys, and
|
|
2297
|
+
* restores fields the markdown dialect cannot express (dropped by
|
|
2298
|
+
* serialization, so the edit could not have touched them); a field
|
|
2299
|
+
* markdown does express follows the edit. The same rule covers a
|
|
2300
|
+
* custom style or decorator markdown has no syntax for, coerced to a
|
|
2301
|
+
* built-in on the round trip. An empty or whitespace-only text block
|
|
2302
|
+
* is the same case taken to the whole block: markdown has no form for
|
|
2303
|
+
* either, so it is restored next to its surviving neighbor and
|
|
2304
|
+
* dropped along with that neighbor if the neighbor does not survive.
|
|
2305
|
+
* Keys aim for what the
|
|
2306
|
+
* same edit would have produced in an editor:
|
|
2307
|
+
* unchanged, moved, and rewritten-in-place content keeps its keys
|
|
2308
|
+
* (rewriting a paragraph in place keeps its identity, like typing over
|
|
2309
|
+
* it), a split keeps the key on its first non-empty fragment, a merge
|
|
2310
|
+
* keeps the first source block's key, and a `json:object` payload keeps
|
|
2311
|
+
* the key it carries, unless reconciliation matches it to stored
|
|
2312
|
+
* content, which takes the stored key even over a differing key in the
|
|
2313
|
+
* payload. When an insertion or deletion makes positions ambiguous,
|
|
2314
|
+
* only clear similarity evidence adopts a key and everything else gets
|
|
2315
|
+
* a new one; gathering that evidence is time-capped, so on very large
|
|
2316
|
+
* ambiguous edits the set of adopted keys can differ across machine
|
|
2317
|
+
* speeds, degrading toward fresh keys.
|
|
2318
|
+
* Output keys are unique among siblings. The function does not mutate
|
|
2319
|
+
* `storedPortableText` and returns a value, not patches. The `schema`
|
|
2320
|
+
* is taken once and governs both directions; pass the same `serialize`
|
|
2321
|
+
* options that produced the markdown that was edited. A throwing or
|
|
2322
|
+
* stateful custom matcher or renderer propagates or degrades matching
|
|
2323
|
+
* respectively.
|
|
2324
|
+
* Reconciliation never merges concurrent edits: compare the stored
|
|
2325
|
+
* field against the live document before writing the result back.
|
|
2326
|
+
* The trades in one line: same-position replacement inherits identity,
|
|
2327
|
+
* a count-preserving rewrite pairs positionally (block-level and
|
|
2328
|
+
* sibling-level alike), and evidence gathering is capped, degrading to
|
|
2329
|
+
* fresh keys.
|
|
2330
|
+
*
|
|
2331
|
+
* @public
|
|
2332
|
+
*/
|
|
2333
|
+
function applyMarkdownEdit(storedPortableText, editedMarkdown, options) {
|
|
2334
|
+
let onReconciliation = options?.onReconciliation, recorder = onReconciliation ? {
|
|
2335
|
+
preservationBasis: /* @__PURE__ */ new WeakMap(),
|
|
2336
|
+
renamePreviousKey: /* @__PURE__ */ new WeakMap(),
|
|
2337
|
+
annotationKeyConflicts: [],
|
|
2338
|
+
ambiguousRegionGroups: [],
|
|
2339
|
+
skipReason: void 0
|
|
2340
|
+
} : void 0, result = structuredClone(markdownToPortableText(editedMarkdown, {
|
|
2341
|
+
...options?.deserialize,
|
|
2342
|
+
schema: options?.schema
|
|
2343
|
+
})), canonical = canonicalizeStored(storedPortableText, options), originOf = traceOrigins(nonEmptyBlocks(storedPortableText, options), canonical, recorder), adoptedNodes = /* @__PURE__ */ new WeakSet();
|
|
2344
|
+
if (originOf) {
|
|
2345
|
+
let alignment = alignBlocks(canonical, result, recorder);
|
|
2346
|
+
if (alignment) {
|
|
2347
|
+
adoptAnchors(alignment.anchors, originOf, result, adoptedNodes, recorder);
|
|
2348
|
+
let gaps = adoptMoves(alignment.gaps, canonical, result, originOf, adoptedNodes, recorder);
|
|
2349
|
+
for (let gap of gaps) resolveGap(gap.storedIndexes, gap.editedIndexes, canonical, result, originOf, adoptedNodes, recorder);
|
|
2350
|
+
reinsertEmptyRuns(storedPortableText, result, adoptedNodes, options, recorder);
|
|
2351
|
+
}
|
|
2352
|
+
}
|
|
2353
|
+
return enforceSiblingKeyUniqueness(result, options?.deserialize?.keyGenerator ?? defaultKeyGenerator, adoptedNodes, recorder), recorder && onReconciliation && onReconciliation(buildReconciliationReport(result, recorder)), result;
|
|
2354
|
+
}
|
|
2355
|
+
/**
|
|
2356
|
+
* Re-expresses the stored value in the parser's dialect by serializing
|
|
2357
|
+
* it and parsing it right back: the parser merges same-mark sibling
|
|
2358
|
+
* spans, reorders marks, collapses list levels, and fills defaults, so
|
|
2359
|
+
* content the edit never touched only deep-equals its parsed
|
|
2360
|
+
* counterpart after both sides have been through the same round trip.
|
|
2361
|
+
* The replacement keys are positional (`__canonical_<n>`), so a match
|
|
2362
|
+
* against canonical node `n` can be traded back for stored node `n`'s
|
|
2363
|
+
* real `_key`.
|
|
2364
|
+
*/
|
|
2365
|
+
function canonicalizeStored(stored, options) {
|
|
2366
|
+
let storedMarkdown = portableTextToMarkdown(structuredClone(stored), {
|
|
2367
|
+
...options?.serialize,
|
|
2368
|
+
schema: options?.schema
|
|
2369
|
+
}), { onDegradation, ...canonicalDeserializeOptions } = options?.deserialize ?? {}, canonicalKeyCounter = 0;
|
|
2370
|
+
return markdownToPortableText(storedMarkdown, {
|
|
2371
|
+
...canonicalDeserializeOptions,
|
|
2372
|
+
schema: options?.schema,
|
|
2373
|
+
keyGenerator: () => `__canonical_${canonicalKeyCounter++}`
|
|
2374
|
+
});
|
|
2375
|
+
}
|
|
2376
|
+
/**
|
|
2377
|
+
* Positional pairing between `stored` and `canonical` is only
|
|
2378
|
+
* trustworthy when serialization preserved the node count and the
|
|
2379
|
+
* type sequence; when it did not (heading hard-break splits, lossy
|
|
2380
|
+
* table normalization), no key can be traced back to its owner, so
|
|
2381
|
+
* nothing adopts. `stored` is already the non-empty subsequence: empty
|
|
2382
|
+
* text blocks have no markdown form, so `canonical` never carries them
|
|
2383
|
+
* either, and `reinsertEmptyRuns` restores them afterward.
|
|
2384
|
+
*/
|
|
2385
|
+
function traceOrigins(stored, canonical, recorder) {
|
|
2386
|
+
if (canonical.length !== stored.length) {
|
|
2387
|
+
recorder && (recorder.skipReason = "round-trip-mismatch");
|
|
2388
|
+
return;
|
|
2389
|
+
}
|
|
2390
|
+
for (let index = 0; index < stored.length; index++) if (stored[index]._type !== canonical[index]?._type) {
|
|
2391
|
+
recorder && (recorder.skipReason = "round-trip-mismatch");
|
|
2392
|
+
return;
|
|
2393
|
+
}
|
|
2394
|
+
for (let index = 0; index < stored.length; index++) {
|
|
2395
|
+
let storedNode = stored[index], canonicalNode = canonical[index];
|
|
2396
|
+
if (isTextBlock$1(storedNode) && isTextBlock$1(canonicalNode) && blockText(storedNode).trim() !== blockText(canonicalNode).trim()) {
|
|
2397
|
+
recorder && (recorder.skipReason = "round-trip-mismatch");
|
|
2398
|
+
return;
|
|
2399
|
+
}
|
|
2400
|
+
}
|
|
2401
|
+
return (canonicalIndex) => stored[canonicalIndex];
|
|
2402
|
+
}
|
|
2403
|
+
/**
|
|
2404
|
+
* Equal runs become anchor pairs (so repeated content pairs
|
|
2405
|
+
* first-to-first when adopted), and the delete/insert runs between
|
|
2406
|
+
* them form the gaps.
|
|
2407
|
+
*/
|
|
2408
|
+
function alignBlocks(canonical, result, recorder) {
|
|
2409
|
+
let tokenByNeutral = /* @__PURE__ */ new Map(), tokenOf = (node) => {
|
|
2410
|
+
let neutral = neutralForm(node), token = tokenByNeutral.get(neutral);
|
|
2411
|
+
return token === void 0 && (token = String.fromCharCode(tokenByNeutral.size + 1), tokenByNeutral.set(neutral, token)), token;
|
|
2412
|
+
}, canonicalTokens = canonical.map(tokenOf).join(""), resultTokens = result.map(tokenOf).join("");
|
|
2413
|
+
if (tokenByNeutral.size > 55e3) {
|
|
2414
|
+
recorder && (recorder.skipReason = "document-too-large");
|
|
2415
|
+
return;
|
|
2416
|
+
}
|
|
2417
|
+
let diffs = makeDiff(canonicalTokens, resultTokens, { checkLines: !1 }), anchors = [], gaps = [], gap = {
|
|
2418
|
+
storedIndexes: [],
|
|
2419
|
+
editedIndexes: []
|
|
2420
|
+
}, flushGap = () => {
|
|
2421
|
+
(gap.storedIndexes.length > 0 || gap.editedIndexes.length > 0) && (gaps.push(gap), gap = {
|
|
2422
|
+
storedIndexes: [],
|
|
2423
|
+
editedIndexes: []
|
|
2424
|
+
});
|
|
2425
|
+
}, canonicalIndex = 0, resultIndex = 0;
|
|
2426
|
+
for (let [operation, text] of diffs) if (operation === 0) {
|
|
2427
|
+
flushGap();
|
|
2428
|
+
for (let offset = 0; offset < text.length; offset++) anchors.push({
|
|
2429
|
+
canonicalIndex,
|
|
2430
|
+
resultIndex
|
|
2431
|
+
}), canonicalIndex++, resultIndex++;
|
|
2432
|
+
} else if (operation === -1) for (let offset = 0; offset < text.length; offset++) gap.storedIndexes.push(canonicalIndex++);
|
|
2433
|
+
else for (let offset = 0; offset < text.length; offset++) gap.editedIndexes.push(resultIndex++);
|
|
2434
|
+
return flushGap(), {
|
|
2435
|
+
anchors,
|
|
2436
|
+
gaps
|
|
2437
|
+
};
|
|
2438
|
+
}
|
|
2439
|
+
function adoptAnchors(anchors, originOf, result, adoptedNodes, recorder) {
|
|
2440
|
+
for (let anchor of anchors) result[anchor.resultIndex] = adoptVerbatim(originOf(anchor.canonicalIndex), result[anchor.resultIndex], adoptedNodes, "content-unchanged", recorder);
|
|
2441
|
+
}
|
|
2442
|
+
/**
|
|
2443
|
+
* An anchor or a unique exact leftover pairs a canonical block against
|
|
2444
|
+
* a parsed one that share the same neutral form (that is what put them
|
|
2445
|
+
* in the same equal-diff run or the same neutral-form bucket), so
|
|
2446
|
+
* everything the dialect can express is untouched and everything it
|
|
2447
|
+
* cannot express was invisible to the edit. The stored subtree is the
|
|
2448
|
+
* truth at every depth, spans, marks, markDefs, and any field the
|
|
2449
|
+
* dialect drops, so adoption replaces the whole node rather than
|
|
2450
|
+
* reconciling into the parsed shape (which would otherwise, for
|
|
2451
|
+
* instance, keep a parser-side span merge that collapsed an
|
|
2452
|
+
* unmappable mark boundary the edit never touched). `restoreFields`
|
|
2453
|
+
* and per-child reconciliation are skipped entirely: a verbatim clone
|
|
2454
|
+
* of the stored node is already complete.
|
|
2455
|
+
*/
|
|
2456
|
+
function adoptVerbatim(original, target, adoptedNodes, basis, recorder) {
|
|
2457
|
+
let clone = structuredClone(original);
|
|
2458
|
+
return fillMissingKeysFromTarget(clone, target), markSubtreeAdopted(clone, adoptedNodes), recorder && (tagSubtreePreserved(clone, recorder), basis !== "content-unchanged" && typeof clone._key == "string" && recorder.preservationBasis.set(clone, basis)), clone;
|
|
2459
|
+
}
|
|
2460
|
+
/**
|
|
2461
|
+
* A stored node practically always carries its own `_key`; when it
|
|
2462
|
+
* genuinely does not, there is nothing to adopt, so the clone keeps
|
|
2463
|
+
* whatever key the plain parse already minted at the corresponding
|
|
2464
|
+
* position, the same key adoption would have left in place. The walk
|
|
2465
|
+
* follows both trees positionally (not by content matching, which is
|
|
2466
|
+
* exactly what an exact-signature match already guarantees agrees at
|
|
2467
|
+
* every position the two sides both still have).
|
|
2468
|
+
*/
|
|
2469
|
+
function fillMissingKeysFromTarget(clone, target) {
|
|
2470
|
+
typeof clone._key != "string" && typeof target._key == "string" && (clone._key = target._key);
|
|
2471
|
+
for (let field of Object.keys(clone)) {
|
|
2472
|
+
let cloneValue = clone[field], targetValue = target[field];
|
|
2473
|
+
if (isTypedObjectArray(cloneValue) && isTypedObjectArray(targetValue)) {
|
|
2474
|
+
let length = Math.min(cloneValue.length, targetValue.length);
|
|
2475
|
+
for (let index = 0; index < length; index++) fillMissingKeysFromTarget(cloneValue[index], targetValue[index]);
|
|
2476
|
+
} else typeof cloneValue == "object" && cloneValue && !Array.isArray(cloneValue) && typeof targetValue == "object" && targetValue && !Array.isArray(targetValue) && fillMissingKeysFromTarget(cloneValue, targetValue);
|
|
2477
|
+
}
|
|
2478
|
+
}
|
|
2479
|
+
/**
|
|
2480
|
+
* Every keyed node in a verbatim clone is adopted, not only its root:
|
|
2481
|
+
* the sibling-key-uniqueness pass recurses into every nested keyed
|
|
2482
|
+
* array, and an adopted-first ordering there only favors a clone's
|
|
2483
|
+
* descendants if they are themselves marked adopted.
|
|
2484
|
+
*/
|
|
2485
|
+
function markSubtreeAdopted(node, adoptedNodes) {
|
|
2486
|
+
adoptedNodes.add(node);
|
|
2487
|
+
for (let value of Object.values(node)) if (Array.isArray(value) && value.every((item) => typeof item == "object" && !!item) && value.length > 0) for (let child of value) markSubtreeAdopted(child, adoptedNodes);
|
|
2488
|
+
else typeof value == "object" && value && markSubtreeAdopted(value, adoptedNodes);
|
|
2489
|
+
}
|
|
2490
|
+
/**
|
|
2491
|
+
* Moves: content that left one gap and reappeared in another. Unique
|
|
2492
|
+
* exact pairs across all gaps adopt before any gap-local pairing can
|
|
2493
|
+
* consume the keys they need.
|
|
2494
|
+
*/
|
|
2495
|
+
function adoptMoves(gaps, canonical, result, originOf, adoptedNodes, recorder) {
|
|
2496
|
+
let consumedStored = /* @__PURE__ */ new Set(), consumedEdited = /* @__PURE__ */ new Set(), storedLeftovers = gaps.flatMap((g) => g.storedIndexes), editedLeftovers = gaps.flatMap((g) => g.editedIndexes), storedByNeutral = /* @__PURE__ */ new Map();
|
|
2497
|
+
for (let index of storedLeftovers) {
|
|
2498
|
+
let neutral = neutralForm(canonical[index]);
|
|
2499
|
+
storedByNeutral.set(neutral, [...storedByNeutral.get(neutral) ?? [], index]);
|
|
2500
|
+
}
|
|
2501
|
+
let editedByNeutral = /* @__PURE__ */ new Map();
|
|
2502
|
+
for (let index of editedLeftovers) {
|
|
2503
|
+
let neutral = neutralForm(result[index]);
|
|
2504
|
+
editedByNeutral.set(neutral, [...editedByNeutral.get(neutral) ?? [], index]);
|
|
2505
|
+
}
|
|
2506
|
+
for (let [neutral, storedIndexes] of storedByNeutral) {
|
|
2507
|
+
let editedIndexes = editedByNeutral.get(neutral);
|
|
2508
|
+
if (storedIndexes.length !== 1 || !editedIndexes || editedIndexes.length !== 1) continue;
|
|
2509
|
+
consumedStored.add(storedIndexes[0]), consumedEdited.add(editedIndexes[0]);
|
|
2510
|
+
let clone = adoptVerbatim(originOf(storedIndexes[0]), result[editedIndexes[0]], adoptedNodes, "content-moved", recorder);
|
|
2511
|
+
result[editedIndexes[0]] = clone;
|
|
2512
|
+
}
|
|
2513
|
+
return gaps.map((currentGap) => ({
|
|
2514
|
+
storedIndexes: currentGap.storedIndexes.filter((index) => !consumedStored.has(index)),
|
|
2515
|
+
editedIndexes: currentGap.editedIndexes.filter((index) => !consumedEdited.has(index))
|
|
2516
|
+
}));
|
|
2517
|
+
}
|
|
2518
|
+
/**
|
|
2519
|
+
* Gap policy, in order: split/merge survivor (the first fragment or
|
|
2520
|
+
* first source block keeps the key, matching what pressing enter or
|
|
2521
|
+
* backspace does in the editor), positional zip for equal counts
|
|
2522
|
+
* (typing over a paragraph keeps its identity), similarity for
|
|
2523
|
+
* unequal counts (an insertion or deletion shifted positions, so
|
|
2524
|
+
* position lies and only mutual unique best evidence adopts).
|
|
2525
|
+
*/
|
|
2526
|
+
function resolveGap(storedIndexes, editedIndexes, canonical, result, originOf, adoptedNodes, recorder) {
|
|
2527
|
+
let remainingStored = new Set(storedIndexes), remainingEdited = new Set(editedIndexes), withinConcatenationCap = storedIndexes.length * editedIndexes.length <= MAX_SIMILARITY_PAIRS;
|
|
2528
|
+
if (withinConcatenationCap) for (let storedIndex of storedIndexes) {
|
|
2529
|
+
if (!remainingStored.has(storedIndex)) continue;
|
|
2530
|
+
let storedBlock = canonical[storedIndex];
|
|
2531
|
+
if (!isTextBlock$1(storedBlock)) continue;
|
|
2532
|
+
let fragments = findConcatenation(blockText(storedBlock), editedIndexes.filter((index) => remainingEdited.has(index)), result);
|
|
2533
|
+
if (fragments) {
|
|
2534
|
+
remainingStored.delete(storedIndex);
|
|
2535
|
+
for (let fragment of fragments) remainingEdited.delete(fragment);
|
|
2536
|
+
let survivor = fragments.find((fragment) => blockText(result[fragment]).length > 0) ?? fragments[0];
|
|
2537
|
+
adoptNode(originOf(storedIndex), canonical[storedIndex], result[survivor], adoptedNodes, "content-split", recorder);
|
|
2538
|
+
}
|
|
2539
|
+
}
|
|
2540
|
+
if (withinConcatenationCap) for (let editedIndex of editedIndexes) {
|
|
2541
|
+
if (!remainingEdited.has(editedIndex)) continue;
|
|
2542
|
+
let editedBlock = result[editedIndex];
|
|
2543
|
+
if (!isTextBlock$1(editedBlock)) continue;
|
|
2544
|
+
let sources = findConcatenation(blockText(editedBlock), storedIndexes.filter((index) => remainingStored.has(index)), canonical);
|
|
2545
|
+
if (sources) {
|
|
2546
|
+
remainingEdited.delete(editedIndex);
|
|
2547
|
+
for (let source of sources) remainingStored.delete(source);
|
|
2548
|
+
adoptNode(originOf(sources[0]), canonical[sources[0]], result[editedIndex], adoptedNodes, "content-merged", recorder);
|
|
1397
2549
|
}
|
|
1398
2550
|
}
|
|
1399
|
-
|
|
2551
|
+
let storedRest = [...remainingStored], editedRest = [...remainingEdited];
|
|
2552
|
+
if (storedRest.length === editedRest.length) {
|
|
2553
|
+
for (let offset = 0; offset < storedRest.length; offset++) {
|
|
2554
|
+
let storedBlock = canonical[storedRest[offset]], editedBlock = result[editedRest[offset]];
|
|
2555
|
+
storedBlock._type === editedBlock._type && adoptNode(originOf(storedRest[offset]), canonical[storedRest[offset]], editedBlock, adoptedNodes, "same-position", recorder);
|
|
2556
|
+
}
|
|
2557
|
+
return;
|
|
2558
|
+
}
|
|
2559
|
+
if (storedRest.length * editedRest.length > MAX_SIMILARITY_PAIRS) {
|
|
2560
|
+
recorder && recorder.ambiguousRegionGroups.push(editedRest.map((editedIndex) => result[editedIndex]));
|
|
2561
|
+
return;
|
|
2562
|
+
}
|
|
2563
|
+
let scores = /* @__PURE__ */ new Map();
|
|
2564
|
+
for (let storedIndex of storedRest) for (let editedIndex of editedRest) {
|
|
2565
|
+
let score = blockSimilarity(canonical[storedIndex], result[editedIndex]);
|
|
2566
|
+
score >= MIN_BLOCK_SIMILARITY && scores.set(`${storedIndex}:${editedIndex}`, score);
|
|
2567
|
+
}
|
|
2568
|
+
for (let storedIndex of storedRest) {
|
|
2569
|
+
let best = uniqueBest(editedRest, (editedIndex) => scores.get(`${storedIndex}:${editedIndex}`));
|
|
2570
|
+
best !== void 0 && uniqueBest(storedRest, (otherStoredIndex) => scores.get(`${otherStoredIndex}:${best}`)) === storedIndex && adoptNode(originOf(storedIndex), canonical[storedIndex], result[best], adoptedNodes, "similar-content", recorder);
|
|
2571
|
+
}
|
|
2572
|
+
}
|
|
2573
|
+
/**
|
|
2574
|
+
* Fragments join with nothing or a single space, since a markdown
|
|
2575
|
+
* merge is often a soft-wrap join that inserts one ("alpha\nbeta"
|
|
2576
|
+
* parses to "alpha beta").
|
|
2577
|
+
*/
|
|
2578
|
+
function findConcatenation(wholeText, candidateIndexes, nodes) {
|
|
2579
|
+
if (wholeText.length !== 0) for (let joiner of ["", " "]) for (let start = 0; start < candidateIndexes.length; start++) {
|
|
2580
|
+
let concatenated = "", used = [];
|
|
2581
|
+
for (let position = start; position < candidateIndexes.length; position++) {
|
|
2582
|
+
let index = candidateIndexes[position];
|
|
2583
|
+
if (position > start && candidateIndexes[position - 1] !== index - 1 || !isTextBlock$1(nodes[index]) || (concatenated = used.length === 0 ? blockText(nodes[index]) : concatenated + joiner + blockText(nodes[index]), used.push(index), concatenated.length > wholeText.length)) break;
|
|
2584
|
+
if (concatenated === wholeText && used.length > 1) return used;
|
|
2585
|
+
}
|
|
2586
|
+
}
|
|
2587
|
+
}
|
|
2588
|
+
/**
|
|
2589
|
+
* Adopts the original node's `_key`, its markdown-inexpressible
|
|
2590
|
+
* fields, then its `markDefs` before its other keyed children, since
|
|
2591
|
+
* `span.marks` references need the adopted `markDefs` keys already in
|
|
2592
|
+
* place.
|
|
2593
|
+
*/
|
|
2594
|
+
function adoptNode(original, canonicalCounterpart, target, adoptedNodes, basis, recorder) {
|
|
2595
|
+
adoptedNodes.add(target), typeof original._key == "string" && (target._key = original._key, recorder?.preservationBasis.set(target, basis)), restoreFields(original, canonicalCounterpart, target), rewriteMarkReferences(target, adoptMarkDefs(original, canonicalCounterpart, target, recorder));
|
|
2596
|
+
for (let field of Object.keys(target)) {
|
|
2597
|
+
if (field === "markDefs") continue;
|
|
2598
|
+
let originalChildren = original[field], targetChildren = target[field];
|
|
2599
|
+
if (!isTypedObjectArray(originalChildren) || !isTypedObjectArray(targetChildren)) continue;
|
|
2600
|
+
let canonicalChildren = canonicalChildArray(canonicalCounterpart, field, originalChildren), matchedOriginal = /* @__PURE__ */ new Set(), matchedTarget = /* @__PURE__ */ new Set(), originalGroups = groupByNeutralForm(originalChildren, matchedOriginal, buildAliasMap(original)), targetGroups = groupByNeutralForm(targetChildren, matchedTarget, buildAliasMap(target));
|
|
2601
|
+
for (let [neutral, originalIndexes] of originalGroups) {
|
|
2602
|
+
let targetIndexes = targetGroups.get(neutral);
|
|
2603
|
+
originalIndexes.length !== 1 || !targetIndexes || targetIndexes.length !== 1 || (matchedOriginal.add(originalIndexes[0]), matchedTarget.add(targetIndexes[0]), adoptNode(originalChildren[originalIndexes[0]], canonicalChildren?.[originalIndexes[0]], targetChildren[targetIndexes[0]], adoptedNodes, "content-unchanged", recorder));
|
|
2604
|
+
}
|
|
2605
|
+
field === "children" && originalChildren.length * targetChildren.length <= MAX_SIMILARITY_PAIRS && adoptMergedSpans(originalChildren, canonicalChildren, matchedOriginal, targetChildren, matchedTarget, adoptedNodes, recorder), adoptResidualZip(originalChildren, canonicalChildren, matchedOriginal, targetChildren, matchedTarget, adoptedNodes, recorder);
|
|
2606
|
+
}
|
|
2607
|
+
}
|
|
2608
|
+
/**
|
|
2609
|
+
* The container-level counterpart of `traceOrigins`'s guard: a
|
|
2610
|
+
* child's canonical form is only trustworthy when the canonical
|
|
2611
|
+
* container holds the same field as the same typed object array,
|
|
2612
|
+
* equal in length and `_type` sequence to the original's, so pairing
|
|
2613
|
+
* by index (original child `i` to canonical child `i`) means the same
|
|
2614
|
+
* content on both sides.
|
|
2615
|
+
*/
|
|
2616
|
+
function canonicalChildArray(canonicalCounterpart, field, originalChildren) {
|
|
2617
|
+
if (!canonicalCounterpart) return;
|
|
2618
|
+
let candidate = canonicalCounterpart[field];
|
|
2619
|
+
if (!(!isTypedObjectArray(candidate) || candidate.length !== originalChildren.length)) {
|
|
2620
|
+
for (let index = 0; index < originalChildren.length; index++) if (originalChildren[index]._type !== candidate[index]._type) return;
|
|
2621
|
+
return candidate;
|
|
2622
|
+
}
|
|
2623
|
+
}
|
|
2624
|
+
/**
|
|
2625
|
+
* Restores fields the markdown dialect dropped or altered: present on
|
|
2626
|
+
* the original, unchanged by the edit (the target still agrees with
|
|
2627
|
+
* the original's own canonical round trip), and different on that
|
|
2628
|
+
* round trip from the original (so the parse, left to itself, could
|
|
2629
|
+
* never have produced the original's value). Absence counts as a
|
|
2630
|
+
* value under both comparisons, which is what folds a wholly dropped
|
|
2631
|
+
* field (no markdown form at all) and a coerced one (a custom style
|
|
2632
|
+
* or decorator markdown silently maps to its closest built-in) into
|
|
2633
|
+
* one rule. Structural child arrays (`children`, `markDefs`, and
|
|
2634
|
+
* typed object arrays generally) are excluded: their elements adopt
|
|
2635
|
+
* individually through the recursive per-child walk instead, except
|
|
2636
|
+
* when the target has no such element to walk at all: a typed-object
|
|
2637
|
+
* array field the dialect drops entirely leaves nothing on the target
|
|
2638
|
+
* side for that walk to reconcile, so it falls through to the same
|
|
2639
|
+
* absent-on-target, absent-on-canonical oracle as every scalar field,
|
|
2640
|
+
* restored verbatim rather than left missing. Restored values are
|
|
2641
|
+
* cloned, since the sibling-key-uniqueness pass may rewrite `_key`s
|
|
2642
|
+
* inside a restored array of objects, and the original must stay
|
|
2643
|
+
* untouched.
|
|
2644
|
+
*/
|
|
2645
|
+
function restoreFields(original, canonicalCounterpart, target) {
|
|
2646
|
+
if (canonicalCounterpart) for (let field of Object.keys(original)) {
|
|
2647
|
+
if (field === "_key" || field === "_type" || field === "markDefs" || field === "children") continue;
|
|
2648
|
+
if (isTypedObjectArray(original[field])) {
|
|
2649
|
+
target[field] === void 0 && canonicalCounterpart[field] === void 0 && (target[field] = structuredClone(original[field]));
|
|
2650
|
+
continue;
|
|
2651
|
+
}
|
|
2652
|
+
let canonicalValue = canonicalCounterpart[field];
|
|
2653
|
+
valuesEqual(target[field], canonicalValue) && (valuesEqual(canonicalValue, original[field]) || (target[field] = structuredClone(original[field])));
|
|
2654
|
+
}
|
|
2655
|
+
}
|
|
2656
|
+
/**
|
|
2657
|
+
* Deep equality for restoration's before/after comparison: the
|
|
2658
|
+
* `encodeNeutral` encoding without alias rewriting, so it compares
|
|
2659
|
+
* `marks` arrays (and any other array or nested object) by value
|
|
2660
|
+
* rather than by identity. Absent (`undefined`) encodes to the same
|
|
2661
|
+
* string on both sides, so two absent fields count as equal.
|
|
2662
|
+
*/
|
|
2663
|
+
function valuesEqual(a, b) {
|
|
2664
|
+
return encodeNeutral(a, void 0) === encodeNeutral(b, void 0);
|
|
2665
|
+
}
|
|
2666
|
+
/**
|
|
2667
|
+
* A span in the edited output can be the merge of several stored
|
|
2668
|
+
* spans: the parser merges adjacent same-mark spans, and an edit that
|
|
2669
|
+
* removes formatting merges across the old mark boundary too. Matching
|
|
2670
|
+
* is by text alone, and the first contributor's key survives, matching
|
|
2671
|
+
* the editor's own span-merge normalization.
|
|
2672
|
+
*/
|
|
2673
|
+
function adoptMergedSpans(originalChildren, canonicalChildren, matchedOriginal, targetChildren, matchedTarget, adoptedNodes, recorder) {
|
|
2674
|
+
for (let targetIndex = 0; targetIndex < targetChildren.length; targetIndex++) {
|
|
2675
|
+
if (matchedTarget.has(targetIndex)) continue;
|
|
2676
|
+
let targetSpan = targetChildren[targetIndex], targetText = targetSpan.text;
|
|
2677
|
+
if (typeof targetText == "string") for (let start = 0; start < originalChildren.length; start++) {
|
|
2678
|
+
if (matchedOriginal.has(start)) continue;
|
|
2679
|
+
let concatenated = "", used = [];
|
|
2680
|
+
for (let index = start; index < originalChildren.length && !matchedOriginal.has(index); index++) {
|
|
2681
|
+
let originalSpan = originalChildren[index];
|
|
2682
|
+
if (typeof originalSpan.text != "string" || (concatenated += originalSpan.text, used.push(index), concatenated.length > targetText.length)) break;
|
|
2683
|
+
if (concatenated === targetText && used.length > 1) {
|
|
2684
|
+
matchedTarget.add(targetIndex);
|
|
2685
|
+
for (let usedIndex of used) matchedOriginal.add(usedIndex);
|
|
2686
|
+
adoptNode(originalChildren[used[0]], canonicalChildren?.[used[0]], targetSpan, adoptedNodes, "content-merged", recorder);
|
|
2687
|
+
break;
|
|
2688
|
+
}
|
|
2689
|
+
}
|
|
2690
|
+
if (matchedTarget.has(targetIndex)) break;
|
|
2691
|
+
}
|
|
2692
|
+
}
|
|
2693
|
+
}
|
|
2694
|
+
/**
|
|
2695
|
+
* The equal-count residual rule, mirroring `resolveGap`'s positional
|
|
2696
|
+
* zip: elements left unmatched after neutral-form (and, for
|
|
2697
|
+
* `children`, span-merge) matching are treated as in-place edits when
|
|
2698
|
+
* both sides leave the same count, position being the same evidence
|
|
2699
|
+
* the block-level zip already trusts, and the trade is the same too:
|
|
2700
|
+
* a reorder-plus-edit with balanced counts mispairs. Unequal counts
|
|
2701
|
+
* adopt nothing, since position no longer lines up.
|
|
2702
|
+
*/
|
|
2703
|
+
function adoptResidualZip(originalChildren, canonicalChildren, matchedOriginal, targetChildren, matchedTarget, adoptedNodes, recorder) {
|
|
2704
|
+
let originalRest = originalChildren.map((node, index) => ({
|
|
2705
|
+
node,
|
|
2706
|
+
index
|
|
2707
|
+
})).filter(({ index }) => !matchedOriginal.has(index)), targetRest = targetChildren.filter((_, index) => !matchedTarget.has(index));
|
|
2708
|
+
if (originalRest.length === targetRest.length) for (let offset = 0; offset < originalRest.length; offset++) {
|
|
2709
|
+
let originalNode = originalRest[offset].node, targetNode = targetRest[offset];
|
|
2710
|
+
originalNode._type === targetNode._type && adoptNode(originalNode, canonicalChildren?.[originalRest[offset].index], targetNode, adoptedNodes, "same-position", recorder);
|
|
2711
|
+
}
|
|
2712
|
+
}
|
|
2713
|
+
/**
|
|
2714
|
+
* Matches `markDefs` by definition content, `_key` excluded. Returns
|
|
2715
|
+
* the mapping from the target's fresh keys to the adopted stored
|
|
2716
|
+
* keys, for rewriting `span.marks` references.
|
|
2717
|
+
*/
|
|
2718
|
+
function adoptMarkDefs(original, canonicalCounterpart, target, recorder) {
|
|
2719
|
+
let keyMap = /* @__PURE__ */ new Map(), originalDefs = original.markDefs, targetDefs = target.markDefs;
|
|
2720
|
+
if (!isTypedObjectArray(originalDefs) || !isTypedObjectArray(targetDefs)) return keyMap;
|
|
2721
|
+
let canonicalDefs = canonicalChildArray(canonicalCounterpart, "markDefs", originalDefs), matchedOriginal = /* @__PURE__ */ new Set(), matchedTarget = /* @__PURE__ */ new Set(), originalGroups = groupByNeutralForm(originalDefs.map((def, index) => canonicalDefs?.[index] ?? def), matchedOriginal), targetGroups = groupByNeutralForm(targetDefs, matchedTarget), adoptDef = (originalDef, canonicalDef, targetDef, basis) => {
|
|
2722
|
+
if (restoreFields(originalDef, canonicalDef, targetDef), typeof originalDef._key == "string" && typeof targetDef._key == "string") {
|
|
2723
|
+
let adoptedKey = originalDef._key;
|
|
2724
|
+
if (targetDefs.some((def) => def !== targetDef && def._key === adoptedKey)) {
|
|
2725
|
+
recorder?.annotationKeyConflicts.push(targetDef);
|
|
2726
|
+
return;
|
|
2727
|
+
}
|
|
2728
|
+
keyMap.set(targetDef._key, adoptedKey), targetDef._key = adoptedKey, recorder?.preservationBasis.set(targetDef, basis);
|
|
2729
|
+
}
|
|
2730
|
+
};
|
|
2731
|
+
for (let [neutral, originalIndexes] of originalGroups) {
|
|
2732
|
+
let targetIndexes = targetGroups.get(neutral);
|
|
2733
|
+
if (!(!targetIndexes || targetIndexes.length !== originalIndexes.length)) for (let offset = 0; offset < originalIndexes.length; offset++) matchedOriginal.add(originalIndexes[offset]), matchedTarget.add(targetIndexes[offset]), adoptDef(originalDefs[originalIndexes[offset]], canonicalDefs?.[originalIndexes[offset]], targetDefs[targetIndexes[offset]], originalIndexes.length === 1 ? "content-unchanged" : "same-position");
|
|
2734
|
+
}
|
|
2735
|
+
let originalRest = originalDefs.map((node, index) => ({
|
|
2736
|
+
node,
|
|
2737
|
+
index
|
|
2738
|
+
})).filter(({ index }) => !matchedOriginal.has(index)), targetRest = targetDefs.filter((_, index) => !matchedTarget.has(index));
|
|
2739
|
+
return originalRest.length === 1 && targetRest.length === 1 && originalRest[0].node._type === targetRest[0]._type && adoptDef(originalRest[0].node, canonicalDefs?.[originalRest[0].index], targetRest[0], "same-position"), keyMap;
|
|
2740
|
+
}
|
|
2741
|
+
function rewriteMarkReferences(block, keyMap) {
|
|
2742
|
+
if (keyMap.size === 0) return;
|
|
2743
|
+
let children = block.children;
|
|
2744
|
+
if (isTypedObjectArray(children)) for (let child of children) {
|
|
2745
|
+
let marks = child.marks;
|
|
2746
|
+
Array.isArray(marks) && (child.marks = marks.map((mark) => typeof mark == "string" && keyMap.has(mark) ? keyMap.get(mark) : mark));
|
|
2747
|
+
}
|
|
2748
|
+
}
|
|
2749
|
+
function groupByNeutralForm(nodes, exclude, aliasByKey) {
|
|
2750
|
+
let groups = /* @__PURE__ */ new Map();
|
|
2751
|
+
for (let index = 0; index < nodes.length; index++) {
|
|
2752
|
+
if (exclude.has(index)) continue;
|
|
2753
|
+
let neutral = aliasByKey ? encodeNeutral(nodes[index], aliasByKey) : neutralForm(nodes[index]), group = groups.get(neutral);
|
|
2754
|
+
group ? group.push(index) : groups.set(neutral, [index]);
|
|
2755
|
+
}
|
|
2756
|
+
return groups;
|
|
2757
|
+
}
|
|
2758
|
+
/**
|
|
2759
|
+
* A canonical JSON encoding that erases identity: `_key` properties
|
|
2760
|
+
* are dropped, object properties are sorted, and annotation `_key`
|
|
2761
|
+
* references inside `span.marks` are rewritten to the definition's
|
|
2762
|
+
* position in `markDefs` (dropping `_key` alone would compare the
|
|
2763
|
+
* stored annotation key against the fresh one and reject an unchanged
|
|
2764
|
+
* link).
|
|
2765
|
+
*/
|
|
2766
|
+
function neutralForm(node) {
|
|
2767
|
+
return encodeNeutral(node, buildAliasMap(node));
|
|
2768
|
+
}
|
|
2769
|
+
/**
|
|
2770
|
+
* Aliases each `markDefs` key to a key-independent spelling of the
|
|
2771
|
+
* definition itself, so `span.marks` references compare by what the
|
|
2772
|
+
* annotation is rather than which key it carries. The spelling is the
|
|
2773
|
+
* definition's own neutral form, not its array position: aliasing by
|
|
2774
|
+
* position made every annotated sibling span's neutral form shift when
|
|
2775
|
+
* a definition was inserted or removed before its own, so adding one
|
|
2776
|
+
* link re-keyed unrelated annotated spans. Identical definitions (the
|
|
2777
|
+
* same link twice) are disambiguated by occurrence order among
|
|
2778
|
+
* identical forms only, which no unrelated insertion can shift.
|
|
2779
|
+
*/
|
|
2780
|
+
function buildAliasMap(node) {
|
|
2781
|
+
let markDefs = node.markDefs;
|
|
2782
|
+
if (!isTypedObjectArray(markDefs)) return;
|
|
2783
|
+
let aliasByKey = /* @__PURE__ */ new Map(), occurrenceByForm = /* @__PURE__ */ new Map();
|
|
2784
|
+
for (let definition of markDefs) {
|
|
2785
|
+
let key = definition._key;
|
|
2786
|
+
if (typeof key != "string") continue;
|
|
2787
|
+
let form = encodeNeutral(definition, void 0), occurrence = occurrenceByForm.get(form) ?? 0;
|
|
2788
|
+
occurrenceByForm.set(form, occurrence + 1), aliasByKey.set(key, `@annotation:${occurrence}:${form}`);
|
|
2789
|
+
}
|
|
2790
|
+
return aliasByKey;
|
|
2791
|
+
}
|
|
2792
|
+
function encodeNeutral(value, aliasByKey) {
|
|
2793
|
+
return Array.isArray(value) ? `[${value.map((item) => encodeNeutral(item, aliasByKey)).join(",")}]` : typeof value == "object" && value ? `{${Object.entries(value).filter(([field]) => field !== "_key").sort(([a], [b]) => a < b ? -1 : +(a > b)).map(([field, fieldValue]) => {
|
|
2794
|
+
if (field === "marks" && aliasByKey && Array.isArray(fieldValue)) {
|
|
2795
|
+
let aliased = fieldValue.map((mark) => typeof mark == "string" && aliasByKey.has(mark) ? aliasByKey.get(mark) : mark);
|
|
2796
|
+
return `${JSON.stringify(field)}:${JSON.stringify(aliased)}`;
|
|
2797
|
+
}
|
|
2798
|
+
return `${JSON.stringify(field)}:${encodeNeutral(fieldValue, aliasByKey)}`;
|
|
2799
|
+
}).join(",")}}` : JSON.stringify(value) ?? "undefined";
|
|
2800
|
+
}
|
|
2801
|
+
/**
|
|
2802
|
+
* The block's comparison text: concatenated span text with inline
|
|
2803
|
+
* objects as sentinels. Marks are ignored, since a formatting-only
|
|
2804
|
+
* edit does not change textual identity.
|
|
2805
|
+
*/
|
|
2806
|
+
function blockText(block) {
|
|
2807
|
+
let children = block.children;
|
|
2808
|
+
return isTypedObjectArray(children) ? children.map((child) => typeof child.text == "string" ? child.text : "").join("") : "";
|
|
2809
|
+
}
|
|
2810
|
+
/**
|
|
2811
|
+
* Text similarity in `[0, 1]`, gated to `0` unless the block shells
|
|
2812
|
+
* agree and the inline objects are compatible: however alike the
|
|
2813
|
+
* prose, blocks that disagree on structure are not the same block.
|
|
2814
|
+
* The length prescreen skips the diff when the size difference alone
|
|
2815
|
+
* puts the score under `MIN_BLOCK_SIMILARITY`.
|
|
2816
|
+
*/
|
|
2817
|
+
function blockSimilarity(canonicalBlock, resultBlock) {
|
|
2818
|
+
if (!shellEquals(canonicalBlock, resultBlock) || !inlineObjectsCompatible(canonicalBlock, resultBlock)) return 0;
|
|
2819
|
+
let canonicalText = blockText(canonicalBlock), resultText = blockText(resultBlock);
|
|
2820
|
+
if (canonicalText.length === 0 || resultText.length === 0) return 0;
|
|
2821
|
+
let longer = Math.max(canonicalText.length, resultText.length);
|
|
2822
|
+
return 1 - Math.abs(canonicalText.length - resultText.length) / longer < MIN_BLOCK_SIMILARITY ? 0 : 1 - levenshteinFromDiffs(cleanupEfficiency(makeDiff(canonicalText, resultText, { timeout: .05 }))) / longer;
|
|
2823
|
+
}
|
|
2824
|
+
/**
|
|
2825
|
+
* The distance derivation from diff runs: insertions and deletions
|
|
2826
|
+
* between equality runs accumulate as the larger of the two, so a
|
|
2827
|
+
* delete-plus-insert counts as one substitution.
|
|
2828
|
+
*/
|
|
2829
|
+
function levenshteinFromDiffs(diffs) {
|
|
2830
|
+
let distance = 0, insertions = 0, deletions = 0;
|
|
2831
|
+
for (let [operation, text] of diffs) operation === 1 ? insertions += text.length : operation === -1 ? deletions += text.length : (distance += Math.max(insertions, deletions), insertions = 0, deletions = 0);
|
|
2832
|
+
return distance + Math.max(insertions, deletions);
|
|
2833
|
+
}
|
|
2834
|
+
/**
|
|
2835
|
+
* The block minus its content: `_type`, `style`, `listItem`, `level`,
|
|
2836
|
+
* and any custom fields must agree before text similarity means
|
|
2837
|
+
* anything.
|
|
2838
|
+
*/
|
|
2839
|
+
function shellEquals(a, b) {
|
|
2840
|
+
let shellOf = (node) => encodeNeutral(Object.fromEntries(Object.entries(node).filter(([field]) => field !== "children" && field !== "markDefs")), void 0);
|
|
2841
|
+
return shellOf(a) === shellOf(b);
|
|
2842
|
+
}
|
|
2843
|
+
/**
|
|
2844
|
+
* Inline objects are opaque content: two blocks whose objects differ
|
|
2845
|
+
* are different blocks no matter how similar their prose is.
|
|
2846
|
+
*/
|
|
2847
|
+
function inlineObjectsCompatible(a, b) {
|
|
2848
|
+
let objectsOf = (node) => {
|
|
2849
|
+
let children = node.children;
|
|
2850
|
+
return isTypedObjectArray(children) ? children.filter((child) => typeof child.text != "string") : [];
|
|
2851
|
+
}, aObjects = objectsOf(a), bObjects = objectsOf(b);
|
|
2852
|
+
return aObjects.length === bObjects.length && aObjects.every((aObject, index) => {
|
|
2853
|
+
let bObject = bObjects[index];
|
|
2854
|
+
return aObject._type === bObject._type ? typeof aObject._key == "string" && aObject._key === bObject._key || neutralForm(aObject) === neutralForm(bObject) : !1;
|
|
2855
|
+
});
|
|
2856
|
+
}
|
|
2857
|
+
/**
|
|
2858
|
+
* The highest-scoring candidate, or `undefined` on a tie: a tie is
|
|
2859
|
+
* ambiguity, and ambiguity refuses adoption rather than guessing.
|
|
2860
|
+
*/
|
|
2861
|
+
function uniqueBest(candidates, scoreOf) {
|
|
2862
|
+
let best, bestScore = 0, tied = !1;
|
|
2863
|
+
for (let candidate of candidates) {
|
|
2864
|
+
let score = scoreOf(candidate);
|
|
2865
|
+
score !== void 0 && (score > bestScore ? (best = candidate, bestScore = score, tied = !1) : score === bestScore && best !== void 0 && (tied = !0));
|
|
2866
|
+
}
|
|
2867
|
+
return tied ? void 0 : best;
|
|
2868
|
+
}
|
|
2869
|
+
/**
|
|
2870
|
+
* Deliberately loose: any node with a `children` array reconciles like
|
|
2871
|
+
* a text block, custom block types included.
|
|
2872
|
+
*/
|
|
2873
|
+
function isTextBlock$1(node) {
|
|
2874
|
+
return Array.isArray(node.children);
|
|
2875
|
+
}
|
|
2876
|
+
/**
|
|
2877
|
+
* A text block the round trip drops. Whether a whitespace-only block
|
|
2878
|
+
* survives serialize→parse depends on the converters, not on
|
|
2879
|
+
* structure: a heading renders `## ` and survives, a custom style
|
|
2880
|
+
* falls back to a plain paragraph and vanishes, and a non-breaking
|
|
2881
|
+
* space renders "blank-looking" output the parser still keeps (JS
|
|
2882
|
+
* `trim` folds NBSP, CommonMark does not), so any string check here is
|
|
2883
|
+
* a hand-written mirror of one converter or the other that drifts. The
|
|
2884
|
+
* block's own render-then-reparse is the authority, the same round
|
|
2885
|
+
* trip `canonicalizeStored` performs, so this predicate cannot
|
|
2886
|
+
* disagree with the canonical node count. The JS-trim pre-check only
|
|
2887
|
+
* keeps the per-block round trip off paths that cannot qualify: it
|
|
2888
|
+
* over-admits candidates (NBSP text passes it), and the reparse then
|
|
2889
|
+
* decides.
|
|
2890
|
+
*/
|
|
2891
|
+
function isEmptyTextBlock(node, options) {
|
|
2892
|
+
if (!(isTextBlock$1(node) && blockText(node).trim() === "")) return !1;
|
|
2893
|
+
let rendered = portableTextToMarkdown([structuredClone(node)], {
|
|
2894
|
+
...options?.serialize,
|
|
2895
|
+
schema: options?.schema
|
|
2896
|
+
});
|
|
2897
|
+
if (rendered === "") return !0;
|
|
2898
|
+
let { onDegradation: _onDegradation, ...deserializeOptions } = options?.deserialize ?? {}, probeKeyCounter = 0;
|
|
2899
|
+
return markdownToPortableText(rendered, {
|
|
2900
|
+
...deserializeOptions,
|
|
2901
|
+
schema: options?.schema,
|
|
2902
|
+
keyGenerator: () => `empty-probe-${probeKeyCounter++}`
|
|
2903
|
+
}).length === 0;
|
|
2904
|
+
}
|
|
2905
|
+
/**
|
|
2906
|
+
* Blank lines are markdown's block separator, so an empty text block
|
|
2907
|
+
* has no serialized form: `canonicalizeStored`'s round trip drops it,
|
|
2908
|
+
* the same way `markdownToPortableText` would if it were parsed back
|
|
2909
|
+
* from `editedMarkdown`. Tracing origins and aligning blocks over this
|
|
2910
|
+
* subsequence keeps both sides the same length; `reinsertEmptyRuns`
|
|
2911
|
+
* restores the dropped blocks afterward.
|
|
2912
|
+
*/
|
|
2913
|
+
function nonEmptyBlocks(stored, options) {
|
|
2914
|
+
return stored.filter((node) => !isEmptyTextBlock(node, options));
|
|
2915
|
+
}
|
|
2916
|
+
function isTypedObjectArray(value) {
|
|
2917
|
+
return Array.isArray(value) && value.length > 0 && value.every((item) => typeof item == "object" && !!item && typeof item._type == "string");
|
|
2918
|
+
}
|
|
2919
|
+
/**
|
|
2920
|
+
* Maximal runs of empty text blocks, each paired with the surviving
|
|
2921
|
+
* neighbor its restoration hangs off: a run with a preceding block
|
|
2922
|
+
* anchors to it (insert after); a run at the document's start, with
|
|
2923
|
+
* none, anchors to the block that follows it (insert before). A run
|
|
2924
|
+
* with neither (the whole document is empty blocks) has nothing to
|
|
2925
|
+
* anchor to and is dropped.
|
|
2926
|
+
*/
|
|
2927
|
+
function findEmptyRuns(stored, options) {
|
|
2928
|
+
let runs = [], index = 0;
|
|
2929
|
+
for (; index < stored.length;) {
|
|
2930
|
+
if (!isEmptyTextBlock(stored[index], options)) {
|
|
2931
|
+
index++;
|
|
2932
|
+
continue;
|
|
2933
|
+
}
|
|
2934
|
+
let runStart = index;
|
|
2935
|
+
for (; index < stored.length && isEmptyTextBlock(stored[index], options);) index++;
|
|
2936
|
+
let precedingBlock = runStart > 0 ? stored[runStart - 1] : void 0, followingBlock = index < stored.length ? stored[index] : void 0, anchor = precedingBlock ?? followingBlock;
|
|
2937
|
+
anchor && typeof anchor._key == "string" && runs.push({
|
|
2938
|
+
anchorKey: anchor._key,
|
|
2939
|
+
insertAfter: precedingBlock !== void 0,
|
|
2940
|
+
blocks: stored.slice(runStart, index)
|
|
2941
|
+
});
|
|
2942
|
+
}
|
|
2943
|
+
return runs;
|
|
2944
|
+
}
|
|
2945
|
+
/**
|
|
2946
|
+
* Restores each empty run next to the result node that adopted its
|
|
2947
|
+
* anchor's key, found by `_key` since positions have already shifted
|
|
2948
|
+
* under insertion, deletion, and move. An anchor whose key did not
|
|
2949
|
+
* survive into `result` (its region was rewritten or deleted) drops
|
|
2950
|
+
* the run with it, consistent with rewrite semantics elsewhere in this
|
|
2951
|
+
* module. Runs are cloned and marked adopted, the same authoritative
|
|
2952
|
+
* status as every other restored key, so a collision resolves in
|
|
2953
|
+
* their favor like `enforceSiblingKeyUniqueness` already does for
|
|
2954
|
+
* `json:object` duplicates.
|
|
2955
|
+
*/
|
|
2956
|
+
function reinsertEmptyRuns(stored, result, adoptedNodes, options, recorder) {
|
|
2957
|
+
for (let run of findEmptyRuns(stored, options)) {
|
|
2958
|
+
let adoptedAnchorIndex = result.findIndex((node) => node._key === run.anchorKey && adoptedNodes.has(node)), anchorIndex = adoptedAnchorIndex === -1 ? result.findIndex((node) => node._key === run.anchorKey) : adoptedAnchorIndex;
|
|
2959
|
+
if (anchorIndex === -1) continue;
|
|
2960
|
+
let clones = run.blocks.map((block) => structuredClone(block));
|
|
2961
|
+
for (let clone of clones) adoptedNodes.add(clone), recorder && tagSubtreePreserved(clone, recorder);
|
|
2962
|
+
result.splice(run.insertAfter ? anchorIndex + 1 : anchorIndex, 0, ...clones);
|
|
2963
|
+
}
|
|
2964
|
+
}
|
|
2965
|
+
/**
|
|
2966
|
+
* Every keyed node in a reinserted empty run is the stored value
|
|
2967
|
+
* verbatim, at every depth, so key resolution reporting tags the
|
|
2968
|
+
* whole subtree `content-unchanged` rather than only the run's top
|
|
2969
|
+
* block.
|
|
2970
|
+
*/
|
|
2971
|
+
function tagSubtreePreserved(node, recorder) {
|
|
2972
|
+
typeof node._key == "string" && recorder.preservationBasis.set(node, "content-unchanged");
|
|
2973
|
+
for (let value of Object.values(node)) if (Array.isArray(value) && value.every((item) => typeof item == "object" && !!item) && value.length > 0) for (let child of value) tagSubtreePreserved(child, recorder);
|
|
2974
|
+
else typeof value == "object" && value && tagSubtreePreserved(value, recorder);
|
|
2975
|
+
}
|
|
2976
|
+
/**
|
|
2977
|
+
* `json:object` payloads transport their `_key` verbatim, so a
|
|
2978
|
+
* copy-pasted fence puts the same key on two siblings, and everything
|
|
2979
|
+
* downstream (patches, anchors, editor normalization) assumes sibling
|
|
2980
|
+
* keys are unique. Adopted keys are authoritative, so a duplicate that
|
|
2981
|
+
* was adopted from the stored value wins and the other occurrences are
|
|
2982
|
+
* regenerated.
|
|
2983
|
+
*/
|
|
2984
|
+
function enforceSiblingKeyUniqueness(nodes, keyGenerator, adoptedNodes, recorder, onKeyRewritten) {
|
|
2985
|
+
let usedKeys = /* @__PURE__ */ new Set(), claim = (node) => {
|
|
2986
|
+
let key = node._key;
|
|
2987
|
+
if (typeof key == "string") {
|
|
2988
|
+
if (usedKeys.has(key)) {
|
|
2989
|
+
let freshKey;
|
|
2990
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
2991
|
+
let candidate = keyGenerator();
|
|
2992
|
+
if (!usedKeys.has(candidate)) {
|
|
2993
|
+
freshKey = candidate;
|
|
2994
|
+
break;
|
|
2995
|
+
}
|
|
2996
|
+
}
|
|
2997
|
+
if (freshKey === void 0) {
|
|
2998
|
+
let suffix = 1, candidate = `${key}_${suffix}`;
|
|
2999
|
+
for (; usedKeys.has(candidate);) suffix++, candidate = `${key}_${suffix}`;
|
|
3000
|
+
freshKey = candidate;
|
|
3001
|
+
}
|
|
3002
|
+
node._key = freshKey, usedKeys.add(freshKey), onKeyRewritten?.(key, freshKey), recorder?.renamePreviousKey.set(node, key);
|
|
3003
|
+
return;
|
|
3004
|
+
}
|
|
3005
|
+
usedKeys.add(key);
|
|
3006
|
+
}
|
|
3007
|
+
}, adopted = nodes.filter((node) => adoptedNodes.has(node)), rest = nodes.filter((node) => !adoptedNodes.has(node));
|
|
3008
|
+
for (let node of [...adopted, ...rest]) claim(node);
|
|
3009
|
+
for (let node of nodes) enforceNestedSiblingKeyUniqueness(node, keyGenerator, adoptedNodes, recorder);
|
|
3010
|
+
}
|
|
3011
|
+
function enforceNestedSiblingKeyUniqueness(node, keyGenerator, adoptedNodes, recorder) {
|
|
3012
|
+
for (let [field, value] of Object.entries(node)) Array.isArray(value) && value.every((item) => typeof item == "object" && !!item) && value.length > 0 ? enforceSiblingKeyUniqueness(value, keyGenerator, adoptedNodes, recorder, field === "markDefs" ? buildMarkDefKeyRewriter(node) : void 0) : typeof value == "object" && value && enforceNestedSiblingKeyUniqueness(value, keyGenerator, adoptedNodes, recorder);
|
|
3013
|
+
}
|
|
3014
|
+
/**
|
|
3015
|
+
* A colliding `keyGenerator` can mint the identical string for two
|
|
3016
|
+
* `markDefs` entries, which means the block's spans already reference
|
|
3017
|
+
* that shared string ambiguously before any rewrite happens: a
|
|
3018
|
+
* blanket find-and-replace of the old key would move every span's
|
|
3019
|
+
* reference, including the one that was never renamed. The
|
|
3020
|
+
* `markDefs` array and each span's `marks` are walked in the same
|
|
3021
|
+
* fixed document order, so the Nth occurrence of a given key in one
|
|
3022
|
+
* lines up with the Nth occurrence in the other; the first occurrence
|
|
3023
|
+
* is always the survivor (`enforceSiblingKeyUniqueness` only renames
|
|
3024
|
+
* on collision, never the first sighting of a key), so each rename
|
|
3025
|
+
* event retargets the next occurrence in that shared order instead of
|
|
3026
|
+
* every occurrence.
|
|
3027
|
+
*/
|
|
3028
|
+
function buildMarkDefKeyRewriter(block) {
|
|
3029
|
+
let children = block.children;
|
|
3030
|
+
if (!isTypedObjectArray(children)) return () => {};
|
|
3031
|
+
let occurrencesByKey = /* @__PURE__ */ new Map();
|
|
3032
|
+
for (let child of children) {
|
|
3033
|
+
let marks = child.marks;
|
|
3034
|
+
if (Array.isArray(marks)) for (let markIndex = 0; markIndex < marks.length; markIndex++) {
|
|
3035
|
+
let mark = marks[markIndex];
|
|
3036
|
+
if (typeof mark != "string") continue;
|
|
3037
|
+
let occurrences = occurrencesByKey.get(mark) ?? [];
|
|
3038
|
+
occurrences.push({
|
|
3039
|
+
child,
|
|
3040
|
+
markIndex
|
|
3041
|
+
}), occurrencesByKey.set(mark, occurrences);
|
|
3042
|
+
}
|
|
3043
|
+
}
|
|
3044
|
+
let consumedByKey = /* @__PURE__ */ new Map();
|
|
3045
|
+
return (oldKey, newKey) => {
|
|
3046
|
+
let occurrences = occurrencesByKey.get(oldKey), index = consumedByKey.get(oldKey) ?? 1;
|
|
3047
|
+
consumedByKey.set(oldKey, index + 1);
|
|
3048
|
+
let target = occurrences?.[index];
|
|
3049
|
+
if (!target) return;
|
|
3050
|
+
let marks = target.child.marks;
|
|
3051
|
+
Array.isArray(marks) && (marks[target.markIndex] = newKey);
|
|
3052
|
+
};
|
|
3053
|
+
}
|
|
3054
|
+
/**
|
|
3055
|
+
* Materializes the public report from the recorder's node-identity
|
|
3056
|
+
* decisions, walking the settled result tree once so every `key` and
|
|
3057
|
+
* `path` matches the returned value exactly: key resolution records
|
|
3058
|
+
* decisions before the sibling-uniqueness pass can still rewrite a
|
|
3059
|
+
* key, so keys and paths are only trustworthy read back from the
|
|
3060
|
+
* final tree, not from the moment a decision was made.
|
|
3061
|
+
*/
|
|
3062
|
+
function buildReconciliationReport(result, recorder) {
|
|
3063
|
+
let preservedKeys = [], renamedKeys = [], pathByNode = /* @__PURE__ */ new WeakMap(), walk = (node, path) => {
|
|
3064
|
+
pathByNode.set(node, path);
|
|
3065
|
+
let key = node._key;
|
|
3066
|
+
if (typeof key == "string") {
|
|
3067
|
+
let basis = recorder.preservationBasis.get(node), previousKey = recorder.renamePreviousKey.get(node);
|
|
3068
|
+
basis && previousKey === void 0 && preservedKeys.push({
|
|
3069
|
+
basis,
|
|
3070
|
+
key,
|
|
3071
|
+
path
|
|
3072
|
+
}), previousKey !== void 0 && renamedKeys.push({
|
|
3073
|
+
previousKey,
|
|
3074
|
+
key,
|
|
3075
|
+
path
|
|
3076
|
+
});
|
|
3077
|
+
}
|
|
3078
|
+
for (let [field, value] of Object.entries(node)) Array.isArray(value) && value.every((item) => typeof item == "object" && !!item) && value.length > 0 ? value.forEach((child, index) => {
|
|
3079
|
+
let childKey = child._key;
|
|
3080
|
+
walk(child, [
|
|
3081
|
+
...path,
|
|
3082
|
+
field,
|
|
3083
|
+
typeof childKey == "string" ? { _key: childKey } : index
|
|
3084
|
+
]);
|
|
3085
|
+
}) : typeof value == "object" && value && walk(value, [...path, field]);
|
|
3086
|
+
};
|
|
3087
|
+
if (result.forEach((block, index) => {
|
|
3088
|
+
let key = block._key;
|
|
3089
|
+
walk(block, [typeof key == "string" ? { _key: key } : index]);
|
|
3090
|
+
}), recorder.skipReason) return {
|
|
3091
|
+
keyMatching: "skipped",
|
|
3092
|
+
reason: recorder.skipReason,
|
|
3093
|
+
renamedKeys
|
|
3094
|
+
};
|
|
3095
|
+
let keyFallbacks = [];
|
|
3096
|
+
for (let group of recorder.ambiguousRegionGroups) keyFallbacks.push({
|
|
3097
|
+
type: "ambiguous-region-too-large",
|
|
3098
|
+
keys: group.map((node) => node._key).filter((key) => typeof key == "string")
|
|
3099
|
+
});
|
|
3100
|
+
for (let node of recorder.annotationKeyConflicts) keyFallbacks.push({
|
|
3101
|
+
type: "annotation-key-conflict",
|
|
3102
|
+
path: pathByNode.get(node)
|
|
3103
|
+
});
|
|
3104
|
+
return {
|
|
3105
|
+
keyMatching: "performed",
|
|
3106
|
+
preservedKeys,
|
|
3107
|
+
keyFallbacks,
|
|
3108
|
+
renamedKeys
|
|
3109
|
+
};
|
|
1400
3110
|
}
|
|
1401
|
-
export { DefaultBlockSpacingRenderer, DefaultBlockquoteObjectRenderer, DefaultBlockquoteRenderer, DefaultCalloutRenderer, DefaultCodeBlockRenderer, DefaultCodeRenderer, DefaultEmRenderer, DefaultH1Renderer, DefaultH2Renderer, DefaultH3Renderer, DefaultH4Renderer, DefaultH5Renderer, DefaultH6Renderer, DefaultHardBreakRenderer, DefaultHorizontalRuleRenderer, DefaultHtmlRenderer, DefaultImageRenderer, DefaultLinkRenderer, DefaultListItemRenderer, DefaultListRenderer, DefaultNormalRenderer, DefaultStrikeThroughRenderer, DefaultStrongRenderer, DefaultTableRenderer, DefaultUnderlineRenderer, markdownToPortableText, portableTextToMarkdown };
|
|
3111
|
+
export { DefaultBlockSpacingRenderer, DefaultBlockquoteObjectRenderer, DefaultBlockquoteRenderer, DefaultCalloutRenderer, DefaultCodeBlockRenderer, DefaultCodeRenderer, DefaultEmRenderer, DefaultH1Renderer, DefaultH2Renderer, DefaultH3Renderer, DefaultH4Renderer, DefaultH5Renderer, DefaultH6Renderer, DefaultHardBreakRenderer, DefaultHorizontalRuleRenderer, DefaultHtmlRenderer, DefaultImageRenderer, DefaultLinkRenderer, DefaultListItemRenderer, DefaultListRenderer, DefaultNormalRenderer, DefaultStrikeThroughRenderer, DefaultStrongRenderer, DefaultTableRenderer, DefaultUnderlineRenderer, applyMarkdownEdit, markdownToPortableText, portableTextToMarkdown };
|
|
1402
3112
|
|
|
1403
3113
|
//# sourceMappingURL=index.js.map
|