@portabletext/markdown 1.5.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { compileSchema, defineSchema, isSpan, isTextBlock, isTypedObject } from "@portabletext/schema";
2
- import { buildMarksTree, isPortableTextBlock, isPortableTextListItemBlock, isPortableTextToolkitSpan, isPortableTextToolkitTextNode, spanToPlainText } from "@portabletext/toolkit";
2
+ import { buildMarksTree, isPortableTextBlock, isPortableTextListItemBlock, isPortableTextSpan, isPortableTextToolkitSpan, isPortableTextToolkitTextNode, spanToPlainText } from "@portabletext/toolkit";
3
+ import LinkifyIt from "linkify-it";
3
4
  import { alert } from "@mdit/plugin-alert";
4
5
  import markdownit from "markdown-it";
5
6
  function defaultKeyGenerator() {
@@ -91,27 +92,431 @@ function buildListIndexMap(blocks) {
91
92
  listDepthMap
92
93
  };
93
94
  }
94
- const createRenderNode = (renderers, listIndexMap, listDepthMap) => {
95
- function renderNode(options) {
96
- let { node, index, isInline } = options;
97
- return isPortableTextListItemBlock(node) ? renderListItem(node, index) : isPortableTextToolkitSpan(node) ? renderSpan(node) : isPortableTextBlock(node) ? renderBlock(node, index, isInline) : isPortableTextToolkitTextNode(node) ? renderText(node) : renderCustomBlock(node, index, isInline);
95
+ /**
96
+ * The CommonMark ASCII punctuation set. Only these characters can be
97
+ * backslash-escaped into a literal without changing the parsed text.
98
+ */
99
+ 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();
100
+ /**
101
+ * Plans the escaped replacement for every plain-text leaf a block's children
102
+ * will produce, in the exact left-to-right order `renderText` visits them
103
+ * (mirroring `buildMarksTree`'s own `text.split('\n')` leaf splitting).
104
+ *
105
+ * Escaping runs ahead of rendering, over the flat span sequence: some
106
+ * hazards only exist across a leaf boundary (an ordered-list marker, a
107
+ * ref-def label, an emphasis run) because an annotation or decorator mark
108
+ * that introduces no markup of its own splices its children in seamlessly.
109
+ * The plan works line by line (a block's children joined into text, split
110
+ * at hard breaks) rather than leaf by leaf: each line's leaves are joined
111
+ * into one string first, opaque children (inline objects, or leaves a
112
+ * custom renderer will replace) masked with a sentinel that can't match any
113
+ * hazard, and every hazard - inline and line-start alike - is detected once
114
+ * against that real, complete line, with true left/right context on both
115
+ * sides. Detected hazards become position-tracked edits against the line's
116
+ * raw text, which are then split back into each contributing leaf's own
117
+ * escaped text; only that composition step is leaf-scoped.
118
+ *
119
+ * A joined line's text that markdown-it's own linkify pass (bundled as
120
+ * `linkify-it`) would claim as a bare URL or email is masked from most
121
+ * edits: the linkify carve-out promises that substring round-trips
122
+ * byte-identical, gaining only a link mark, so escaping inside it would
123
+ * corrupt text linkify is about to claim as a link's visible text. An
124
+ * entity-reference or backtick escape is never masked (see `computeLinkifyMask`
125
+ * for why), and a claim spliced across a decorator boundary is never masked
126
+ * in the first place.
127
+ *
128
+ * `isHeading` is set for ATX headings: only the first joined line sits
129
+ * inside the `# ` prefix an ATX heading can never be reparsed as a block
130
+ * construct within, so line-leading hazards are skipped there; a hard
131
+ * break's later lines are ordinary markdown lines and get the full
132
+ * line-start battery. That first line carries a line-*end* hazard of its
133
+ * own instead: a trailing `#`-run reads back as the heading's own optional
134
+ * closing sequence.
135
+ *
136
+ * `isListItem` is set when the block renders as list-item content: a
137
+ * `[ ] `/`[x] `/`[X] ` at the very start of the first joined line reads
138
+ * back as a GFM task-list checkbox, regardless of the list's own item type.
139
+ *
140
+ * `hardBreakOutputHasNewline` says whether the renderer's actual hard-break
141
+ * output contains a newline. A custom `hardBreak` can render to something
142
+ * with no newline of its own (eg `() => '<br />'`), in which case the
143
+ * leaves on either side of it land on the same rendered line, not two: a
144
+ * hard break like that can't be planned as a line boundary, so it's walled
145
+ * off as an opaque segment instead, the same protection an inline object's
146
+ * unknown rendered text already gets.
147
+ */
148
+ function planLeafEscaping(children, markDefs, options) {
149
+ let linkMarkKeys = new Set(markDefs.filter((def) => def._type === "link").map((def) => def._key)), markDefKeys = new Set(markDefs.map((def) => def._key)), pieces = [];
150
+ for (let child of children) if (isPortableTextSpan(child)) {
151
+ let isLinkLabel = (child.marks ?? []).some((mark) => linkMarkKeys.has(mark)), markSignature = (child.marks ?? []).filter((mark) => !markDefKeys.has(mark)).sort().join(",");
152
+ child.text.split("\n").forEach((line, index) => {
153
+ index > 0 && pieces.push(options.hardBreakOutputHasNewline ? { kind: "hardBreak" } : { kind: "opaque" }), pieces.push({
154
+ kind: "text",
155
+ raw: line,
156
+ isLinkLabel,
157
+ markSignature
158
+ });
159
+ });
160
+ } else pieces.push({ kind: "opaque" });
161
+ let pieceOutputs = pieces.map(() => ""), lineIndex = 0, lineText = "", lineChars = [], lineIsLinkLabelChar = [], lineMarkSignature = [], flushLine = () => {
162
+ processLine({
163
+ text: lineText,
164
+ chars: lineChars,
165
+ isLinkLabelChar: lineIsLinkLabelChar,
166
+ markSignature: lineMarkSignature,
167
+ lineIndex,
168
+ isHeading: options.isHeading,
169
+ isListItem: options.isListItem,
170
+ pieceOutputs
171
+ }), lineIndex++, lineText = "", lineChars = [], lineIsLinkLabelChar = [], lineMarkSignature = [];
172
+ };
173
+ for (let pieceIndex = 0; pieceIndex < pieces.length; pieceIndex++) {
174
+ let piece = pieces[pieceIndex];
175
+ if (!piece || piece.kind === "hardBreak") {
176
+ flushLine();
177
+ continue;
178
+ }
179
+ if (piece.kind === "opaque") {
180
+ lineText += "\0", lineChars.push(null), lineIsLinkLabelChar.push(!1), lineMarkSignature.push("");
181
+ continue;
182
+ }
183
+ let prepared = piece.isLinkLabel ? escapeLinkLabelBrackets(piece.raw) : piece.raw;
184
+ for (let offset = 0; offset < prepared.length; offset++) lineText += prepared[offset], lineChars.push({
185
+ pieceIndex,
186
+ offset
187
+ }), lineIsLinkLabelChar.push(piece.isLinkLabel), lineMarkSignature.push(piece.markSignature);
98
188
  }
99
- function renderListItem(node, index) {
100
- let renderer = renderers.listItem, itemHandler = (typeof renderer == "function" ? renderer : renderer[node.listItem]) || renderers.unknownListItem, children = buildMarksTree(node).map((child, i) => renderNode({
189
+ flushLine();
190
+ let escaped = [];
191
+ return pieces.forEach((piece, index) => {
192
+ piece.kind === "text" && escaped.push(pieceOutputs[index] ?? "");
193
+ }), escaped;
194
+ }
195
+ function processLine(args) {
196
+ let { text, chars, isLinkLabelChar, markSignature, pieceOutputs } = args, linkifyMask = computeLinkifyMask(text, chars, isLinkLabelChar, markSignature);
197
+ applyEdits(text, chars, [...collectInlineEdits(text, isLinkLabelChar), ...collectLineStartEdits(text, args)].filter((edit) => edit.bypassLinkifyMask || !isMasked(edit, linkifyMask)), pieceOutputs);
198
+ }
199
+ /**
200
+ * Marks every character of this line that markdown-it's linkify pass would
201
+ * claim as part of a bare URL or email. Link-label and opaque characters
202
+ * are blanked out first: a link label's visible text sits inside `[...]`
203
+ * markup real linkify never reconsiders, and an opaque child's rendered
204
+ * text is unknown at plan time, so neither should join or seed a match.
205
+ *
206
+ * The probe only sees this line's raw, undecoded text, one hazard pass
207
+ * ahead of markdown-it's own pipeline: it runs linkify against inline
208
+ * tokenization and entity decoding, not before them. A claim survives only
209
+ * if it lies entirely inside one run of identical decorator marks: a
210
+ * decorator boundary crossing it splices that decorator's delimiters
211
+ * (`**`, `` ` ``, ...) into the middle of the range real linkify would see,
212
+ * which breaks the very claim being trusted. An annotation-only boundary
213
+ * (a link's own label text is already excluded above; any other
214
+ * annotation type falls back to rendering with no delimiters at all,
215
+ * same as an unregistered decorator) never splices, so it can't invalidate
216
+ * a claim either.
217
+ */
218
+ function computeLinkifyMask(text, chars, isLinkLabelChar, markSignature) {
219
+ let mask = Array(text.length).fill(!1);
220
+ if (!/[.:@]/.test(text)) return mask;
221
+ let probe = "";
222
+ for (let index = 0; index < text.length; index++) probe += chars[index] === null || isLinkLabelChar[index] ? " " : text[index];
223
+ let matches = linkify.match(probe) ?? [];
224
+ for (let match of matches) {
225
+ if (match.schema === "") continue;
226
+ let signature = markSignature[match.index], staysWithinOneMarkRun = !0;
227
+ for (let index = match.index; index < match.lastIndex; index++) if (markSignature[index] !== signature) {
228
+ staysWithinOneMarkRun = !1;
229
+ break;
230
+ }
231
+ if (staysWithinOneMarkRun) for (let index = match.index; index < match.lastIndex; index++) mask[index] = !0;
232
+ }
233
+ return mask;
234
+ }
235
+ function isMasked(edit, mask) {
236
+ let end = edit.at + Math.max(edit.deleteCount, 1);
237
+ for (let index = edit.at; index < end; index++) if (mask[index]) return !0;
238
+ return !1;
239
+ }
240
+ /** Rewrites a line's raw text into each contributing leaf's escaped text by
241
+ * walking it once, left to right, applying at most one edit per position.
242
+ * Every hazard is keyed off its own trigger character - a backslash, a
243
+ * tilde, a backtick, an `&`, a `<`, a `*`/`_`, a `]`, or (line-start only,
244
+ * one hazard per line) a `#`, `>`, `[`, `-`/`+`/`*`, the `.`/`)` after an
245
+ * ordered-list marker's digits, `=`, 4 spaces, or a tab - and no two of
246
+ * those characters coincide at one position, so two edits can never target
247
+ * the same position. */
248
+ function applyEdits(text, chars, edits, pieceOutputs) {
249
+ let editsByPosition = /* @__PURE__ */ new Map();
250
+ for (let edit of edits) {
251
+ if (editsByPosition.has(edit.at)) throw Error(`Two hazard edits targeted the same position (${edit.at}); hazard trigger characters are assumed disjoint by construction.`);
252
+ editsByPosition.set(edit.at, edit);
253
+ }
254
+ let index = 0;
255
+ for (; index < text.length;) {
256
+ let edit = editsByPosition.get(index), owner = chars[index];
257
+ if (edit && (owner && (pieceOutputs[owner.pieceIndex] = (pieceOutputs[owner.pieceIndex] ?? "") + edit.insert), edit.deleteCount > 0)) {
258
+ index += edit.deleteCount;
259
+ continue;
260
+ }
261
+ owner && (pieceOutputs[owner.pieceIndex] = (pieceOutputs[owner.pieceIndex] ?? "") + (text[index] ?? "")), index++;
262
+ }
263
+ }
264
+ /**
265
+ * Hazards that can appear anywhere on a line: emphasis/strikethrough runs,
266
+ * a backtick, an entity reference, an HTML/autolink-shaped `<`, a literal
267
+ * backslash before punctuation, and a `]` immediately before `(`/`[`
268
+ * (which would otherwise read back as a link/image open).
269
+ */
270
+ function collectInlineEdits(text, isLinkLabelChar) {
271
+ let edits = [];
272
+ for (let match of text.matchAll(BACKSLASH_BEFORE_PUNCTUATION)) {
273
+ let at = match.index ?? 0;
274
+ isLinkLabelChar[at] || edits.push({
275
+ at,
276
+ deleteCount: 0,
277
+ insert: "\\"
278
+ });
279
+ }
280
+ for (let match of text.matchAll(TILDE_RUN)) {
281
+ let start = match.index ?? 0;
282
+ for (let index = start; index < start + match[0].length; index++) edits.push({
283
+ at: index,
284
+ deleteCount: 0,
285
+ insert: "\\"
286
+ });
287
+ }
288
+ for (let index = 0; index < text.length; index++) text[index] === "`" && edits.push({
289
+ at: index,
290
+ deleteCount: 0,
291
+ insert: "\\",
292
+ bypassLinkifyMask: !0
293
+ });
294
+ for (let match of text.matchAll(ENTITY_REFERENCE)) edits.push({
295
+ at: match.index ?? 0,
296
+ deleteCount: 0,
297
+ insert: "\\",
298
+ bypassLinkifyMask: !0
299
+ });
300
+ for (let match of text.matchAll(HTML_LIKE_ANGLE_BRACKET)) edits.push({
301
+ at: match.index ?? 0,
302
+ deleteCount: 0,
303
+ insert: "\\"
304
+ });
305
+ edits.push(...collectEmphasisEdits(text));
306
+ for (let match of text.matchAll(BRACKET_BEFORE_LINK_OPEN)) {
307
+ let at = match.index ?? 0;
308
+ isLinkLabelChar[at] || edits.push({
309
+ at,
310
+ deleteCount: 0,
311
+ insert: "\\"
312
+ });
313
+ }
314
+ return edits;
315
+ }
316
+ function isWhitespace(char) {
317
+ return char === void 0 || /\s/.test(char);
318
+ }
319
+ function isPunctuation(char) {
320
+ return char !== void 0 && UNICODE_PUNCTUATION_OR_SYMBOL.test(char);
321
+ }
322
+ /**
323
+ * The full code point sitting immediately before `index`: two UTF-16 code
324
+ * units for an astral character (eg an emoji) whose low surrogate lands at
325
+ * `index - 1`, one otherwise.
326
+ */
327
+ function codePointBefore(text, index) {
328
+ if (!(index <= 0)) return index >= 2 && isLowSurrogate(text[index - 1]) && isHighSurrogate(text[index - 2]) ? text.slice(index - 2, index) : text[index - 1];
329
+ }
330
+ /**
331
+ * The full code point sitting immediately at `index`: two UTF-16 code units
332
+ * for an astral character whose high surrogate lands at `index`, one
333
+ * otherwise.
334
+ */
335
+ function codePointAt(text, index) {
336
+ if (!(index >= text.length)) return isHighSurrogate(text[index]) && isLowSurrogate(text[index + 1]) ? text.slice(index, index + 2) : text[index];
337
+ }
338
+ function isHighSurrogate(char) {
339
+ if (char === void 0) return !1;
340
+ let code = char.charCodeAt(0);
341
+ return code >= 55296 && code <= 56319;
342
+ }
343
+ function isLowSurrogate(char) {
344
+ if (char === void 0) return !1;
345
+ let code = char.charCodeAt(0);
346
+ return code >= 56320 && code <= 57343;
347
+ }
348
+ function isLeftFlanking(before, after) {
349
+ return isWhitespace(after) ? !1 : !isPunctuation(after) || isWhitespace(before) || isPunctuation(before);
350
+ }
351
+ function isRightFlanking(before, after) {
352
+ return isWhitespace(before) ? !1 : !isPunctuation(before) || isWhitespace(after) || isPunctuation(after);
353
+ }
354
+ /**
355
+ * Finds `*`/`_` runs CommonMark would treat as flanking delimiters, using
356
+ * each run's true neighbors on the joined line (the start/end of the line
357
+ * itself counts as whitespace, matching the spec's treatment of line
358
+ * boundaries).
359
+ */
360
+ function collectEmphasisEdits(text) {
361
+ let edits = [], index = 0;
362
+ for (; index < text.length;) {
363
+ let char = text[index];
364
+ if (char !== "*" && char !== "_") {
365
+ index++;
366
+ continue;
367
+ }
368
+ let end = index;
369
+ for (; end < text.length && text[end] === char;) end++;
370
+ 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;
371
+ if (canOpen || canClose) for (let position = index; position < end; position++) edits.push({
372
+ at: position,
373
+ deleteCount: 0,
374
+ insert: "\\"
375
+ });
376
+ index = end;
377
+ }
378
+ return edits;
379
+ }
380
+ /**
381
+ * Hazards that only matter at the start (or, for a handful of whole-line
382
+ * constructs, the start *and* end) of a line: headings, blockquotes, list
383
+ * markers, ref-defs, setext underlines, thematic breaks, indented code, and
384
+ * a list item's own GFM task-checkbox prefix. A fence needs no branch of
385
+ * its own here: the inline backtick/tilde escaping every line already
386
+ * neutralizes the run a fence needs, so it can never open one on reparse.
387
+ * The remaining branches are mutually exclusive by construction
388
+ * (each targets a disjoint leading character) and return as soon as one
389
+ * matches, mirroring how CommonMark itself commits to one block-start
390
+ * interpretation per line; the checkbox branch above is the one exception,
391
+ * since a list item's checkbox prefix and, say, its heading marker are two
392
+ * independent hazards that can both apply to the same first line.
393
+ */
394
+ function collectLineStartEdits(text, context) {
395
+ let edits = [], isFirstLine = context.lineIndex === 0, leadingSpaces = /^ {0,3}/.exec(text)?.[0].length ?? 0, rest = text.slice(leadingSpaces);
396
+ if (context.isListItem && isFirstLine && /^\[[ xX]\] /.test(rest) && edits.push({
397
+ at: leadingSpaces,
398
+ deleteCount: 0,
399
+ insert: "\\"
400
+ }), context.isHeading && isFirstLine) {
401
+ let closingSequence = /^(?:(.*[ \t]))?(#+[ \t]*)$/.exec(text);
402
+ return closingSequence && edits.push({
403
+ at: closingSequence[1]?.length ?? 0,
404
+ deleteCount: 0,
405
+ insert: "\\"
406
+ }), edits;
407
+ }
408
+ let orderedListMarker = /^ {0,3}(\d{1,9})([.)])(?=[ \t]|$)/.exec(text);
409
+ if (orderedListMarker) return edits.push({
410
+ at: orderedListMarker[0].length - 1,
411
+ deleteCount: 0,
412
+ insert: "\\"
413
+ }), edits;
414
+ 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({
415
+ at: leadingSpaces,
416
+ deleteCount: 0,
417
+ insert: "\\"
418
+ }), edits;
419
+ if (/^ {4}/.test(text)) return edits.push({
420
+ at: 0,
421
+ deleteCount: 1,
422
+ insert: "&#32;"
423
+ }), edits;
424
+ let tabIndent = /^ {0,3}\t/.exec(text);
425
+ return tabIndent && edits.push({
426
+ at: tabIndent[0].length - 1,
427
+ deleteCount: 1,
428
+ insert: "&#9;"
429
+ }), edits;
430
+ }
431
+ /**
432
+ * Text rendered inside a link label needs every `[`, `]` and `\` escaped
433
+ * unconditionally, on top of the general-purpose hazard escaping every
434
+ * line gets: a link label must stay bracket-balanced, and any literal
435
+ * backslash in it needs protecting regardless of what follows (unlike
436
+ * plain text, where only a backslash immediately before punctuation is a
437
+ * hazard).
438
+ */
439
+ function escapeLinkLabelBrackets(text) {
440
+ return text.replace(/[[\]\\]/g, (char) => `\\${char}`);
441
+ }
442
+ /**
443
+ * Blocks currently known to be a list item's first content block: it shares
444
+ * its first line with the list marker (and, for a task item, its GFM
445
+ * checkbox), which changes how `renderBlock` plans line-start hazard
446
+ * escaping. Internal to this package so the signal never reaches the
447
+ * public `Serializable`/`RenderNode` types a custom renderer's `.d.ts`
448
+ * would otherwise expose it through.
449
+ *
450
+ * A block is marked right before rendering it; the `renderNode` call that
451
+ * dispatches to `renderBlock` consumes the membership on the way past so a
452
+ * later, unrelated render of the same object (still possible - `renderNode`
453
+ * accepts any `TypedObject`) doesn't inherit a stale claim.
454
+ */
455
+ const listItemFirstBlocks = /* @__PURE__ */ new WeakSet();
456
+ function markListItemFirstBlock(block) {
457
+ listItemFirstBlocks.add(block);
458
+ }
459
+ function consumeListItemFirstBlock(block) {
460
+ let isListItemFirstBlock = listItemFirstBlocks.has(block);
461
+ return listItemFirstBlocks.delete(block), isListItemFirstBlock;
462
+ }
463
+ /**
464
+ * ATX headings are single-line, inline-only leaf blocks: an ATX heading's
465
+ * first line sits inside its `# ` prefix and can never be reparsed as a
466
+ * block construct, so line-leading hazards never apply there. A hard
467
+ * break's later lines are ordinary markdown lines outside that prefix and
468
+ * get the full line-start battery, same as any other block's continuation.
469
+ */
470
+ const HEADING_STYLES = /* @__PURE__ */ new Set([
471
+ "h1",
472
+ "h2",
473
+ "h3",
474
+ "h4",
475
+ "h5",
476
+ "h6"
477
+ ]), createRenderNode = (renderers, listIndexMap, listDepthMap) => {
478
+ let escapedTextByNode = /* @__PURE__ */ new WeakMap(), hardBreakOutputHasNewline = renderers.hardBreak().includes("\n");
479
+ function renderBlockChildren(node, isHeading, isListItem = !1) {
480
+ let chunks = planLeafEscaping(node.children ?? [], node.markDefs ?? [], {
481
+ isHeading,
482
+ isListItem,
483
+ hardBreakOutputHasNewline
484
+ }), tree = buildMarksTree(node);
485
+ return assignEscapedText(tree, chunks), tree.map((child, i) => renderNode({
101
486
  node: child,
102
487
  isInline: !0,
103
488
  index: i,
104
489
  renderNode
105
490
  })).join("");
491
+ }
492
+ function assignEscapedText(nodes, chunks) {
493
+ let pointer = 0, visit = (node) => {
494
+ if (isPortableTextToolkitTextNode(node)) {
495
+ if (node.text !== "\n") {
496
+ let escaped = chunks[pointer];
497
+ escaped !== void 0 && escapedTextByNode.set(node, escaped), pointer++;
498
+ }
499
+ return;
500
+ }
501
+ isPortableTextToolkitSpan(node) && node.children.forEach(visit);
502
+ };
503
+ nodes.forEach(visit);
504
+ }
505
+ function renderNode(options) {
506
+ let { node, index, isInline } = options;
507
+ 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);
508
+ }
509
+ function renderListItem(node, index) {
510
+ let renderer = renderers.listItem, itemHandler = (typeof renderer == "function" ? renderer : renderer[node.listItem]) || renderers.unknownListItem, children;
106
511
  if (node.style && node.style !== "normal") {
107
512
  let { listItem: _listItem, ...blockNode } = node;
108
- children = renderNode({
513
+ markListItemFirstBlock(blockNode), children = renderNode({
109
514
  node: blockNode,
110
515
  index,
111
516
  isInline: !1,
112
517
  renderNode
113
518
  }), children = children.replace(/\n+$/, "");
114
- }
519
+ } else children = renderBlockChildren(node, !1, !0);
115
520
  return itemHandler({
116
521
  value: node,
117
522
  index,
@@ -138,21 +543,18 @@ const createRenderNode = (renderers, listIndexMap, listDepthMap) => {
138
543
  children: children.join("")
139
544
  });
140
545
  }
141
- function renderBlock(node, index, isInline) {
142
- let { _key, ...props } = serializeBlock({
143
- node,
546
+ function renderBlock(node, index, isInline, isListItem) {
547
+ let style = node.style || "normal", children = renderBlockChildren(node, HEADING_STYLES.has(style), isListItem);
548
+ return ((typeof renderers.block == "function" ? renderers.block : renderers.block[style]) || renderers.unknownBlockStyle)({
144
549
  index,
145
550
  isInline,
146
- renderNode
147
- }), style = props.node.style || "normal";
148
- return ((typeof renderers.block == "function" ? renderers.block : renderers.block[style]) || renderers.unknownBlockStyle)({
149
- ...props,
150
- value: props.node,
551
+ children,
552
+ value: node,
151
553
  renderNode
152
554
  });
153
555
  }
154
556
  function renderText(node) {
155
- return node.text === "\n" ? renderers.hardBreak() : node.text;
557
+ return node.text === "\n" ? renderers.hardBreak() : escapedTextByNode.get(node) ?? node.text;
156
558
  }
157
559
  function renderCustomBlock(value, index, isInline) {
158
560
  return (renderers.types[value._type] ?? renderers.unknownType)({
@@ -163,26 +565,7 @@ const createRenderNode = (renderers, listIndexMap, listDepthMap) => {
163
565
  });
164
566
  }
165
567
  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 }) => {
568
+ }, 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
569
  let listStyle = value.listItem || "bullet", depth = listDepth ?? (value.level || 1) - 1, indent = " ".repeat(depth);
187
570
  return listStyle === "number" ? `${indent}${listIndex ?? 1}. ${children}` : listStyle === "task" ? `${indent}- ${"checked" in value && typeof value.checked == "boolean" && value.checked ? "[x]" : "[ ]"} ${children}` : `${indent}- ${children}`;
188
571
  }, DefaultUnknownListItemRenderer = ({ children }) => `- ${children}\n`;
@@ -208,32 +591,39 @@ function escapeImageAndLinkTitle(text) {
208
591
  * Escapes characters that have special meaning at the row level of a GFM
209
592
  * table cell.
210
593
  *
211
- * A literal `|` ends the cell, so unescaped pipes are replaced with `\|`.
212
- * Newlines end the row, so they are replaced with `<br>` to keep the
213
- * visible line break inside the cell. Already-escaped pipes (`\|`) are
214
- * left intact so that escapes introduced by mark renderers survive the
215
- * pass.
594
+ * A literal `|` ends the cell, so a pipe preceded by an even number of
595
+ * backslashes (including zero) gets one more: paired backslashes cancel
596
+ * out to a literal backslash and leave the pipe live, so parity, not mere
597
+ * presence, decides whether it is already escaped. Newlines end the row,
598
+ * so they are replaced with `<br>` to keep the visible line break inside
599
+ * the cell.
216
600
  *
217
- * Backslashes are intentionally not escaped here so that other escapes
218
- * already in the rendered cell (such as `\[` and `\]` in link text) are
219
- * not double-escaped.
601
+ * Backslashes themselves are left alone here; only the parity check reads
602
+ * them, so escapes already in the rendered cell (such as `\[` and `\]` in
603
+ * link text) survive the pass untouched.
220
604
  */
221
605
  function escapeTableCell(text) {
222
- return text.replace(RegExp("(?<!\\\\)\\|", "g"), "\\|").replace(/\n/g, "<br>");
606
+ return text.replace(/(\\*)\|/g, (match, backslashes) => backslashes.length % 2 == 0 ? `${backslashes}\\|` : match).replace(/\n/g, "<br>");
607
+ }
608
+ /**
609
+ * @public
610
+ */
611
+ const DefaultEmRenderer = ({ children }) => `_${children}_`, DefaultStrongRenderer = ({ children }) => `**${children}**`, DefaultCodeRenderer = ({ text }) => wrapInCodeSpan(text);
612
+ function wrapInCodeSpan(text) {
613
+ let fence = "`".repeat(longestBacktickRun(text) + 1), touchesBacktick = text.startsWith("`") || text.endsWith("`"), wouldBeStripped = text.startsWith(" ") && text.endsWith(" ") && text.trim() !== "", padding = touchesBacktick || wouldBeStripped ? " " : "";
614
+ return `${fence}${padding}${text}${padding}${fence}`;
615
+ }
616
+ function longestBacktickRun(text) {
617
+ let longest = 0;
618
+ for (let run of text.match(/`+/g) ?? []) longest = Math.max(longest, run.length);
619
+ return longest;
223
620
  }
224
621
  /**
225
622
  * @public
226
623
  */
227
- const DefaultEmRenderer = ({ children }) => `_${children}_`, DefaultStrongRenderer = ({ children }) => `**${children}**`, DefaultCodeRenderer = ({ children }) => `\`${children}\``, DefaultUnderlineRenderer = ({ children }) => `<u>${children}</u>`, DefaultStrikeThroughRenderer = ({ children }) => `~~${children}~~`, DefaultLinkRenderer = ({ children, value }) => {
624
+ const DefaultUnderlineRenderer = ({ children }) => `<u>${children}</u>`, DefaultStrikeThroughRenderer = ({ children }) => `~~${children}~~`, DefaultLinkRenderer = ({ children, value }) => {
228
625
  let href = value?.href || "", title = value?.title || "";
229
- if (uriLooksSafe(href)) {
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;
626
+ return uriLooksSafe(href) ? /["'][^"']*[<>]|[<>][^<>]*["']/.test(href) ? `[${children}](${href.replace(/["<>() ]/g, (char) => `%${char.charCodeAt(0).toString(16).toUpperCase()}`)})` : `[${children}](${href}${title ? ` "${escapeImageAndLinkTitle(title)}"` : ""})` : children;
237
627
  };
238
628
  function uriLooksSafe(uri) {
239
629
  let url = (uri || "").trim(), first = url.charAt(0);
@@ -267,7 +657,7 @@ function isCodeShaped(value) {
267
657
  * as absent instead of guarded.
268
658
  */
269
659
  function normalizeLanguage(language) {
270
- return typeof language != "string" || language.includes("\n") ? "" : language;
660
+ return typeof language != "string" || language.includes("\n") || language === "json:object" ? "" : language;
271
661
  }
272
662
  /**
273
663
  * @public
@@ -393,7 +783,7 @@ const DefaultBlockquoteObjectRenderer = ({ value, renderNode }) => value.content
393
783
  })).join("\n\n").split("\n").map((line) => line === "" ? ">" : `> ${line}`).join("\n"), DefaultListRenderer = ({ value, renderNode }) => {
394
784
  let itemSeparator = value.items.some((item) => item.content.filter((block) => block._type !== "list").length > 1) ? "\n\n" : "\n";
395
785
  return value.items.map((item, itemIndex) => {
396
- let marker = getListMarker(value.kind, itemIndex, item.checked), indentWidth = value.kind === "task" ? 2 : marker.length, indent = " ".repeat(indentWidth), [first, ...rest] = item.content.map((block, blockIndex) => ({
786
+ let marker = getListMarker(value.kind, itemIndex, item.checked), indentWidth = value.kind === "task" ? 2 : marker.length, indent = " ".repeat(indentWidth), [first, ...rest] = item.content.map((block, blockIndex) => (blockIndex === 0 && isPortableTextBlock(block) && markListItemFirstBlock(block), {
397
787
  isNestedList: block._type === "list",
398
788
  text: renderNode({
399
789
  node: block,
@@ -414,10 +804,7 @@ function getListMarker(kind, itemIndex, checked) {
414
804
  /**
415
805
  * @public
416
806
  */
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 = {
807
+ const DefaultUnknownTypeRenderer = ({ value, isInline }) => isInline ? `json:object${wrapInCodeSpan(JSON.stringify(value))}` : `\`\`\`json:object\n${JSON.stringify(value, null, 2)}\n\`\`\``, defaultRenderers = {
421
808
  types: {
422
809
  callout: DefaultCalloutRenderer,
423
810
  code: DefaultCodeBlockRenderer,
@@ -611,7 +998,66 @@ const normalStyleDefinition = { name: "normal" }, h1StyleDefinition = { name: "h
611
998
  defaultTableObjectDefinition
612
999
  ],
613
1000
  inlineObjects: [defaultImageObjectDefinition]
614
- }));
1001
+ })), degradationMessage = {
1002
+ "decorator-dropped": (decorator) => {
1003
+ switch (decorator) {
1004
+ case "code": return "Removed inline-code formatting, kept the text: the schema has no `code` decorator";
1005
+ case "strong": return "Removed bold formatting, kept the text: the schema has no `strong` decorator";
1006
+ case "em": return "Removed italic formatting, kept the text: the schema has no `em` decorator";
1007
+ case "strikeThrough": return "Removed strikethrough formatting, kept the text: the schema has no `strike-through` decorator";
1008
+ }
1009
+ },
1010
+ "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",
1011
+ "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`,
1012
+ "list-flattened": (kind) => `${kind === "number" ? "Numbered" : "Bullet"} list became plain paragraphs: the schema has no \`${kind}\` list`,
1013
+ "task-checkbox-stripped": (checked) => `Removed the \`${checked ? "[x]" : "[ ]"}\` checkbox, kept a plain list item: the schema has no \`task\` list`,
1014
+ "table-flattened": "Table became plain text blocks, rows and columns lost: the schema has no `table` block object",
1015
+ "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",
1016
+ "horizontal-rule-to-text": "Horizontal rule became the text `---`: the schema has no `horizontal-rule` block object",
1017
+ "html-block-to-text": "HTML block became plain text: the schema has no `html` block object",
1018
+ "inline-html-dropped": "Removed inline HTML tags, kept nothing: `html.inline` is `skip` (the default)",
1019
+ "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`",
1020
+ "image-inline-to-block": "The image became its own block, splitting the paragraph: the schema has no inline `image`",
1021
+ "image-to-text": "Image became its markdown source as plain text: the schema has no `image` object",
1022
+ "callout-fallback": (calloutType, style) => `\`[!${calloutType.toUpperCase()}]\` callout became ${style}-styled text: the schema has no \`callout\` block object`,
1023
+ "fields-dropped": (names, construct) => `Dropped ${names} from \`${construct}\`: not in the schema's \`${construct}\` fields`,
1024
+ "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)}`
1025
+ };
1026
+ /**
1027
+ * Names why a `json:object` payload failed to parse as an object carrier:
1028
+ * a payload that isn't a JSON object at all reads differently from one
1029
+ * that is but has no usable `_type`.
1030
+ */
1031
+ function describeObjectCarrierFailure(payload) {
1032
+ let parsed;
1033
+ try {
1034
+ parsed = JSON.parse(payload);
1035
+ } catch {
1036
+ return "the payload is not valid JSON";
1037
+ }
1038
+ return typeof parsed != "object" || !parsed || Array.isArray(parsed) ? "the payload is not a JSON object" : "the payload has no string `_type`";
1039
+ }
1040
+ const droppedFieldsTag = Symbol("droppedFields");
1041
+ function readDroppedFields(object) {
1042
+ if (object) return object[droppedFieldsTag];
1043
+ }
1044
+ function buildFilteredObject(schemaDefinition, value, keyGenerator) {
1045
+ let filteredValue = schemaDefinition.fields.reduce((filteredValue, field) => {
1046
+ let fieldValue = value[field.name];
1047
+ return fieldValue !== void 0 && (filteredValue[field.name] = fieldValue), filteredValue;
1048
+ }, {}), object = {
1049
+ _key: keyGenerator(),
1050
+ _type: schemaDefinition.name,
1051
+ ...filteredValue
1052
+ }, droppedKeys = Object.entries(value).filter(([, fieldValue]) => fieldValue !== void 0).map(([key]) => key).filter((key) => !(key in filteredValue));
1053
+ return droppedKeys.length > 0 && Object.defineProperty(object, droppedFieldsTag, {
1054
+ value: {
1055
+ construct: schemaDefinition.name,
1056
+ keys: droppedKeys
1057
+ },
1058
+ enumerable: !1
1059
+ }), object;
1060
+ }
615
1061
  function buildStyleMatcher(definition) {
616
1062
  return ({ context }) => {
617
1063
  let schemaDefinition = context.schema.styles.find((item) => item.name === definition.name);
@@ -633,31 +1079,13 @@ function buildDecoratorMatcher(definition) {
633
1079
  function buildAnnotationMatcher(definition) {
634
1080
  return ({ context, value }) => {
635
1081
  let schemaDefinition = context.schema.annotations.find((item) => item.name === definition.name);
636
- if (!schemaDefinition) return;
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
- };
1082
+ if (schemaDefinition) return buildFilteredObject(schemaDefinition, value, context.keyGenerator);
646
1083
  };
647
1084
  }
648
1085
  function buildObjectMatcher(definition) {
649
1086
  return ({ context, value, isInline }) => {
650
1087
  let schemaDefinition = (isInline ? context.schema.inlineObjects : context.schema.blockObjects).find((item) => item.name === definition.name);
651
- if (!schemaDefinition) return;
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
- };
1088
+ if (schemaDefinition) return buildFilteredObject(schemaDefinition, value, context.keyGenerator);
661
1089
  };
662
1090
  }
663
1091
  const codeBlockMatcher = ({ context, value, isInline }) => {
@@ -742,6 +1170,83 @@ function flattenTable(table, portableText) {
742
1170
  for (let row of table.rows) for (let cell of row.cells) for (let block of cell.value) portableText.push(block);
743
1171
  }
744
1172
  /**
1173
+ * Truncates a degradation message's snippet to keep the thrown/reported
1174
+ * message readable. Truncates on character count, not word boundaries: the
1175
+ * snippet is a diagnostic pointer back to the source, not prose. Undefined
1176
+ * for empty input, so a construct with nothing to quote (an empty link's
1177
+ * text, say) omits `snippet` entirely instead of reporting `""`. Backs the
1178
+ * cut off by one unit when it would land on a lead surrogate, so a snippet
1179
+ * ending mid-emoji doesn't produce an unpaired surrogate. A literal newline
1180
+ * surviving into the snippet is escaped to `\n`, since the reported message
1181
+ * is one line per finding.
1182
+ */
1183
+ function truncateSnippet(text, maxLength = 40) {
1184
+ if (text.length === 0) return;
1185
+ if (text.length <= maxLength) return text.replace(/\n/g, "\\n");
1186
+ let cut = maxLength, codeUnit = text.charCodeAt(cut - 1);
1187
+ return codeUnit >= 55296 && codeUnit <= 56319 && --cut, `${text.slice(0, cut).replace(/\n/g, "\\n")}...`;
1188
+ }
1189
+ /**
1190
+ * Concatenates the plain text between an inline open token (`strong_open`,
1191
+ * `em_open`, `s_open`, `link_open`) and its matching close, for use as a
1192
+ * degradation message snippet. Tracks nesting depth so a same-type token
1193
+ * nested inside itself doesn't stop the scan at the wrong close.
1194
+ */
1195
+ function collectInlineText(children, openIndex, openType, closeType) {
1196
+ let depth = 1, text = "";
1197
+ for (let i = openIndex + 1; i < children.length; i++) {
1198
+ let child = children[i];
1199
+ if (child) {
1200
+ if (child.type === openType) depth++;
1201
+ else if (child.type === closeType) {
1202
+ if (depth--, depth === 0) break;
1203
+ } else child.type === "text" ? text += child.content : child.type === "softbreak" ? text += " " : child.type === "hardbreak" && (text += "\n");
1204
+ }
1205
+ }
1206
+ return text;
1207
+ }
1208
+ function capList(values) {
1209
+ if (values.length <= 5) return values.join(", ");
1210
+ let shown = values.slice(0, 5), more = values.length - 5;
1211
+ return `${shown.join(", ")}, and ${more} more`;
1212
+ }
1213
+ /**
1214
+ * Builds the canonical grouped message reported alongside a non-empty
1215
+ * `degradations` array: identical (`type`, `message`) pairs collapse into one
1216
+ * line, so a document with the same missing decorator on three spans
1217
+ * doesn't repeat the same sentence three times. Groups sort by their
1218
+ * earliest-lined entry so the message reads top-to-bottom regardless of walk
1219
+ * order: nested constructs (a blockquote inside a blockquote, say) report
1220
+ * the innermost closing first even though it opened last, and that line may
1221
+ * arrive after another entry already in the group. Groups without any lined
1222
+ * entry sort last, in their relative encounter order.
1223
+ */
1224
+ function buildDegradationMessage(degradations) {
1225
+ let groups = [], groupIndexByKey = /* @__PURE__ */ new Map();
1226
+ for (let degradation of degradations) {
1227
+ let key = `${degradation.type}\u0000${degradation.message}`, groupIndex = groupIndexByKey.get(key);
1228
+ groupIndex === void 0 && (groupIndex = groups.length, groupIndexByKey.set(key, groupIndex), groups.push({
1229
+ base: degradation.message,
1230
+ entries: []
1231
+ })), groups[groupIndex].entries.push(degradation);
1232
+ }
1233
+ let minLine = (entries) => {
1234
+ let definedLines = entries.map((entry) => entry.line).filter((line) => line !== void 0);
1235
+ return definedLines.length > 0 ? Math.min(...definedLines) : void 0;
1236
+ };
1237
+ return ["Markdown could not be converted without loss:", ...[...groups].sort((a, b) => {
1238
+ let lineA = minLine(a.entries), lineB = minLine(b.entries);
1239
+ return lineA === void 0 ? lineB === void 0 ? 0 : 1 : lineB === void 0 ? -1 : lineA - lineB;
1240
+ }).map((group) => {
1241
+ if (group.entries.length === 1) {
1242
+ let event = group.entries[0], snippetPart = event.snippet === void 0 ? "" : ` ("${event.snippet}")`;
1243
+ return event.line === void 0 ? `- ${event.message}${snippetPart}` : `- line ${event.line}: ${event.message}${snippetPart}`;
1244
+ }
1245
+ 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)`;
1246
+ return `- ${group.base} ${suffix}`;
1247
+ })].join("\n");
1248
+ }
1249
+ /**
745
1250
  * Converts a markdown string to an array of Portable Text blocks.
746
1251
  *
747
1252
  * @public
@@ -767,11 +1272,46 @@ function markdownToPortableText(markdown, options) {
767
1272
  ...defaultOptions.types,
768
1273
  ...options?.types
769
1274
  }
1275
+ }, degradationEvents = [], report = (event) => {
1276
+ degradationEvents.push(event);
1277
+ }, lineOf = (candidateToken) => candidateToken?.map ? candidateToken.map[0] + 1 : void 0, reportStyleFallback = (name, line, snippet) => {
1278
+ if (/^h[1-6]$/.test(name)) {
1279
+ let truncated = snippet === void 0 ? void 0 : truncateSnippet(snippet);
1280
+ report({
1281
+ type: "style-fallback",
1282
+ message: degradationMessage["style-fallback"](name),
1283
+ line,
1284
+ snippet: truncated
1285
+ });
1286
+ return;
1287
+ }
1288
+ if (name === "blockquote") {
1289
+ report({
1290
+ type: "style-fallback",
1291
+ message: degradationMessage["style-fallback"](name),
1292
+ line
1293
+ });
1294
+ return;
1295
+ }
1296
+ report({
1297
+ type: "style-fallback",
1298
+ message: degradationMessage["style-fallback"](name),
1299
+ line
1300
+ });
1301
+ }, reportFieldsDropped = (object, line) => {
1302
+ let dropped = readDroppedFields(object);
1303
+ if (!dropped) return;
1304
+ let names = dropped.keys.map((key) => `\`${key}\``).join(", ");
1305
+ report({
1306
+ type: "fields-dropped",
1307
+ message: degradationMessage["fields-dropped"](names, dropped.construct),
1308
+ line
1309
+ });
770
1310
  }, tokens = markdownit({
771
1311
  html: !0,
772
1312
  linkify: !0,
773
1313
  typographer: !1
774
- }).enable(["strikethrough", "table"]).use(alert).parse(markdown, {}), taskCheckedByListItemIndex = /* @__PURE__ */ new Map();
1314
+ }).enable(["strikethrough", "table"]).use(alert).parse(markdown, {}), taskCheckedByListItemIndex = /* @__PURE__ */ new Map(), taskItemTextByListItemIndex = /* @__PURE__ */ new Map();
775
1315
  for (let i = 0; i < tokens.length; i++) {
776
1316
  if (tokens[i]?.type !== "list_item_open") continue;
777
1317
  let inlineIndex = -1;
@@ -791,11 +1331,11 @@ function markdownToPortableText(markdown, options) {
791
1331
  let match = inlineToken.content.match(/^\[([ xX])\] /);
792
1332
  if (!match) continue;
793
1333
  let checked = match[1] !== " ";
794
- taskCheckedByListItemIndex.set(i, checked), inlineToken.content = inlineToken.content.slice(match[0].length);
1334
+ taskCheckedByListItemIndex.set(i, checked), inlineToken.content = inlineToken.content.slice(match[0].length), taskItemTextByListItemIndex.set(i, inlineToken.content);
795
1335
  let firstChild = inlineToken.children?.[0];
796
1336
  firstChild && typeof firstChild.content == "string" && (firstChild.content = firstChild.content.slice(match[0].length));
797
1337
  }
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 = () => {
1338
+ 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
1339
  for (let i = listContainerStack.length - 1; i >= 0; i--) {
800
1340
  let frame = listContainerStack[i];
801
1341
  if (frame && frame.currentItem) return frame.currentItem.content;
@@ -803,26 +1343,35 @@ function markdownToPortableText(markdown, options) {
803
1343
  return portableText;
804
1344
  }, pushBlock = (block) => {
805
1345
  blockTarget().push(block);
806
- }, startBlock = (style) => {
1346
+ }, startBlock = (style, provenance) => {
807
1347
  flushBlock(), currentBlock = {
808
1348
  _type: "block",
809
1349
  style,
810
1350
  children: [],
811
1351
  _key: consolidatedOptions.keyGenerator(),
812
1352
  markDefs: []
813
- }, currentMarkDefs = [];
1353
+ }, currentMarkDefs = [], currentBlockTookBlockquoteStyle = provenance?.tookBlockquoteStyle ?? !1, currentBlockIsPlainParagraph = provenance?.isPlainParagraph ?? !1;
814
1354
  }, flushBlock = () => {
815
- currentBlock && (currentBlock.children.length === 0 && currentBlock.children.push({
816
- _type: consolidatedOptions.schema.span.name,
817
- _key: consolidatedOptions.keyGenerator(),
818
- text: "",
819
- marks: []
820
- }), currentBlock.markDefs = currentMarkDefs, pushBlock(currentBlock), currentBlock = null, currentMarkDefs = []);
1355
+ if (currentBlock) {
1356
+ if (calloutPendingStyleFallbacks.length > 0 && currentBlockTookBlockquoteStyle) {
1357
+ for (let name of calloutPendingStyleFallbacks) reportStyleFallback(name, calloutStartLine);
1358
+ calloutPendingStyleFallbacks = [];
1359
+ }
1360
+ currentBlock.children.length === 0 && currentBlock.children.push({
1361
+ _type: consolidatedOptions.schema.span.name,
1362
+ _key: consolidatedOptions.keyGenerator(),
1363
+ text: "",
1364
+ marks: []
1365
+ }), currentBlock.markDefs = currentMarkDefs, currentBlockIsPlainParagraph && plainParagraphBlocks.add(currentBlock), pushBlock(currentBlock), currentBlock = null, currentMarkDefs = [];
1366
+ }
821
1367
  }, addSpan = (text) => {
822
1368
  if (text.length === 0) return;
823
1369
  if (!currentBlock) {
824
1370
  let style = currentBlockquoteStyle ?? consolidatedOptions.block.normal({ context: { schema: consolidatedOptions.schema } });
825
- style ? startBlock(style) : (console.warn("No default style found, using \"normal\""), startBlock("normal"));
1371
+ style ? startBlock(style, {
1372
+ tookBlockquoteStyle: currentBlockquoteStyle !== null,
1373
+ isPlainParagraph: !0
1374
+ }) : (reportStyleFallback("normal"), startBlock("normal", { isPlainParagraph: !0 }));
826
1375
  }
827
1376
  if (!currentBlock) throw Error("Expected current block");
828
1377
  let lastChild = currentBlock.children.at(-1);
@@ -835,7 +1384,7 @@ function markdownToPortableText(markdown, options) {
835
1384
  }, listLevel = () => currentListStack.length, ensureListBlock = (listItem, checked) => {
836
1385
  if (!currentBlock) {
837
1386
  let style = currentBlockquoteStyle ?? consolidatedOptions.block.normal({ context: { schema: consolidatedOptions.schema } });
838
- style ? startBlock(style) : (console.warn("No default style found, using \"normal\""), startBlock("normal"));
1387
+ style ? startBlock(style, { tookBlockquoteStyle: currentBlockquoteStyle !== null }) : (reportStyleFallback("normal"), startBlock("normal"));
839
1388
  }
840
1389
  if (!currentBlock) throw Error("Expected current block");
841
1390
  (currentBlock.listItem !== listItem || currentBlock.level !== listLevel()) && (currentBlock.listItem = listItem, currentBlock.level = listLevel()), checked !== void 0 && (currentBlock.checked = checked);
@@ -846,7 +1395,13 @@ function markdownToPortableText(markdown, options) {
846
1395
  case "paragraph_open": {
847
1396
  if (inListItem) {
848
1397
  if (listContainerStack.at(-1)) {
849
- currentBlock || startBlock(currentBlockquoteStyle ?? consolidatedOptions.block.normal({ context: { schema: consolidatedOptions.schema } }) ?? "normal");
1398
+ if (!currentBlock) {
1399
+ let style = currentBlockquoteStyle ?? consolidatedOptions.block.normal({ context: { schema: consolidatedOptions.schema } });
1400
+ style || reportStyleFallback("normal", lineOf(token)), startBlock(style ?? "normal", {
1401
+ tookBlockquoteStyle: currentBlockquoteStyle !== null,
1402
+ isPlainParagraph: !0
1403
+ });
1404
+ }
850
1405
  break;
851
1406
  }
852
1407
  if (!currentBlock) {
@@ -857,10 +1412,13 @@ function markdownToPortableText(markdown, options) {
857
1412
  }
858
1413
  let style = currentBlockquoteStyle ?? consolidatedOptions.block.normal({ context: { schema: consolidatedOptions.schema } });
859
1414
  if (!style) {
860
- console.warn("No default style found, using \"normal\""), startBlock("normal");
1415
+ reportStyleFallback("normal", lineOf(token)), startBlock("normal", { isPlainParagraph: !0 });
861
1416
  break;
862
1417
  }
863
- startBlock(style);
1418
+ startBlock(style, {
1419
+ tookBlockquoteStyle: currentBlockquoteStyle !== null,
1420
+ isPlainParagraph: !0
1421
+ });
864
1422
  break;
865
1423
  }
866
1424
  case "paragraph_close":
@@ -878,9 +1436,11 @@ function markdownToPortableText(markdown, options) {
878
1436
  4: consolidatedOptions.block.h4,
879
1437
  5: consolidatedOptions.block.h5,
880
1438
  6: consolidatedOptions.block.h6
881
- }[level], style = headingMatcher?.({ context: { schema: consolidatedOptions.schema } }) ?? consolidatedOptions.block.normal({ context: { schema: consolidatedOptions.schema } });
1439
+ }[level], headingStyle = headingMatcher?.({ context: { schema: consolidatedOptions.schema } });
1440
+ headingStyle || reportStyleFallback(`h${level}`, lineOf(token), tokens[tokenIndex + 1]?.content);
1441
+ let style = headingStyle ?? consolidatedOptions.block.normal({ context: { schema: consolidatedOptions.schema } });
882
1442
  if (!style) {
883
- console.warn("No heading style found, using \"normal\""), startBlock("normal");
1443
+ reportStyleFallback("normal", lineOf(token)), startBlock("normal");
884
1444
  break;
885
1445
  }
886
1446
  startBlock(style);
@@ -889,17 +1449,22 @@ function markdownToPortableText(markdown, options) {
889
1449
  case "heading_close":
890
1450
  flushBlock();
891
1451
  break;
892
- case "blockquote_open":
1452
+ case "blockquote_open": {
893
1453
  if (flushBlock(), consolidatedOptions.types.blockquote) {
894
1454
  let startTarget = blockTarget();
895
1455
  blockquoteStack.push({
896
1456
  startTarget,
897
- startIndex: startTarget.length
1457
+ startIndex: startTarget.length,
1458
+ line: lineOf(token)
898
1459
  });
899
1460
  break;
900
1461
  }
901
- currentBlockquoteStyle = consolidatedOptions.block.blockquote({ context: { schema: consolidatedOptions.schema } }) ?? consolidatedOptions.block.normal({ context: { schema: consolidatedOptions.schema } }) ?? "normal";
1462
+ let blockquoteStyle = consolidatedOptions.block.blockquote({ context: { schema: consolidatedOptions.schema } });
1463
+ blockquoteStyle || reportStyleFallback("blockquote", lineOf(token));
1464
+ let style = blockquoteStyle ?? consolidatedOptions.block.normal({ context: { schema: consolidatedOptions.schema } });
1465
+ style || reportStyleFallback("normal", lineOf(token)), currentBlockquoteStyle = style ?? "normal";
902
1466
  break;
1467
+ }
903
1468
  case "blockquote_close":
904
1469
  if (flushBlock(), consolidatedOptions.types.blockquote && blockquoteStack.length > 0) {
905
1470
  let frame = blockquoteStack.pop();
@@ -914,11 +1479,18 @@ function markdownToPortableText(markdown, options) {
914
1479
  });
915
1480
  if (blockquoteObject) pushBlock(blockquoteObject);
916
1481
  else {
917
- let blockquoteStyle = consolidatedOptions.block.blockquote({ context: { schema: consolidatedOptions.schema } }) ?? consolidatedOptions.block.normal({ context: { schema: consolidatedOptions.schema } }) ?? "blockquote";
918
- for (let block of contentBlocks) block._type === "block" ? pushBlock({
919
- ...block,
920
- style: blockquoteStyle
921
- }) : pushBlock(block);
1482
+ let blockquoteStyle = consolidatedOptions.block.blockquote({ context: { schema: consolidatedOptions.schema } });
1483
+ blockquoteStyle || reportStyleFallback("blockquote", frame.line);
1484
+ let resolvedStyle = blockquoteStyle ?? consolidatedOptions.block.normal({ context: { schema: consolidatedOptions.schema } });
1485
+ resolvedStyle || reportStyleFallback("normal", frame.line);
1486
+ let fallbackStyle = resolvedStyle ?? "blockquote";
1487
+ for (let block of contentBlocks) if (block._type === "block" && plainParagraphBlocks.has(block)) {
1488
+ let restyledBlock = {
1489
+ ...block,
1490
+ style: fallbackStyle
1491
+ };
1492
+ plainParagraphBlocks.add(restyledBlock), pushBlock(restyledBlock);
1493
+ } else pushBlock(block);
922
1494
  }
923
1495
  }
924
1496
  break;
@@ -930,16 +1502,21 @@ function markdownToPortableText(markdown, options) {
930
1502
  listContainerStack.push({
931
1503
  kind: "bullet",
932
1504
  items: [],
933
- currentItem: null
934
- }), currentListStack.push(null);
1505
+ currentItem: null,
1506
+ line: lineOf(token)
1507
+ }), currentListStack.push(null), pendingListFlattenedStack.push(null);
935
1508
  break;
936
1509
  }
937
1510
  let listItem = consolidatedOptions.listItem.bullet({ context: { schema: consolidatedOptions.schema } });
938
1511
  if (listContainerStack.push(null), !listItem) {
939
- currentListStack.push(null);
1512
+ pendingListFlattenedStack.push({
1513
+ line: lineOf(token),
1514
+ kindName: "bullet",
1515
+ reported: !1
1516
+ }), currentListStack.push(null);
940
1517
  break;
941
1518
  }
942
- currentListStack.push(listItem);
1519
+ pendingListFlattenedStack.push(null), currentListStack.push(listItem);
943
1520
  break;
944
1521
  }
945
1522
  case "ordered_list_open": {
@@ -947,22 +1524,27 @@ function markdownToPortableText(markdown, options) {
947
1524
  listContainerStack.push({
948
1525
  kind: "number",
949
1526
  items: [],
950
- currentItem: null
951
- }), currentListStack.push(null);
1527
+ currentItem: null,
1528
+ line: lineOf(token)
1529
+ }), currentListStack.push(null), pendingListFlattenedStack.push(null);
952
1530
  break;
953
1531
  }
954
1532
  let listItem = consolidatedOptions.listItem.number({ context: { schema: consolidatedOptions.schema } });
955
1533
  if (listContainerStack.push(null), !listItem) {
956
- currentListStack.push(null);
1534
+ pendingListFlattenedStack.push({
1535
+ line: lineOf(token),
1536
+ kindName: "number",
1537
+ reported: !1
1538
+ }), currentListStack.push(null);
957
1539
  break;
958
1540
  }
959
- currentListStack.push(listItem);
1541
+ pendingListFlattenedStack.push(null), currentListStack.push(listItem);
960
1542
  break;
961
1543
  }
962
1544
  case "bullet_list_close":
963
1545
  case "ordered_list_close": {
964
1546
  let frame = listContainerStack.pop();
965
- if (currentListStack.pop(), frame && consolidatedOptions.types.list) {
1547
+ if (currentListStack.pop(), pendingListFlattenedStack.pop(), frame && consolidatedOptions.types.list) {
966
1548
  let kind = frame.items.some((item) => "checked" in item) ? "task" : frame.kind, listObject = consolidatedOptions.types.list({
967
1549
  context: {
968
1550
  schema: consolidatedOptions.schema,
@@ -976,13 +1558,45 @@ function markdownToPortableText(markdown, options) {
976
1558
  });
977
1559
  if (listObject) pushBlock(listObject);
978
1560
  else {
979
- let flatListItem = (kind === "task" ? consolidatedOptions.listItem.task?.({ context: { schema: consolidatedOptions.schema } }) : kind === "number" ? consolidatedOptions.listItem.number({ context: { schema: consolidatedOptions.schema } }) : consolidatedOptions.listItem.bullet({ context: { schema: consolidatedOptions.schema } })) ?? null, level = listContainerStack.length + 1;
980
- for (let item of frame.items) for (let block of item.content) block._type === "block" && flatListItem !== null && !("listItem" in block) ? pushBlock({
981
- ...block,
982
- listItem: flatListItem,
983
- level,
984
- ...item.checked === void 0 ? {} : { checked: item.checked }
985
- }) : pushBlock(block);
1561
+ 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;
1562
+ for (let item of frame.items) {
1563
+ let itemListType = kindListItem, itemChecked;
1564
+ if (item.checked !== void 0) {
1565
+ if (taskListItemType) itemListType = taskListItemType, itemChecked = item.checked;
1566
+ else if (kindListItem !== null) {
1567
+ let info = taskInfoByListItem.get(item);
1568
+ report({
1569
+ type: "task-checkbox-stripped",
1570
+ message: degradationMessage["task-checkbox-stripped"](item.checked),
1571
+ line: info?.line,
1572
+ snippet: info?.snippet
1573
+ });
1574
+ }
1575
+ }
1576
+ itemListType === null && !flattenedReported && (report({
1577
+ type: "list-flattened",
1578
+ message: degradationMessage["list-flattened"](kindName),
1579
+ line: frame.line
1580
+ }), flattenedReported = !0);
1581
+ let mergeTarget = null;
1582
+ for (let block of item.content) {
1583
+ if (!(itemListType !== null && block._type === "block" && !("listItem" in block) && !/^h[1-6]$/.test(block.style ?? "") && plainParagraphBlocks.has(block))) {
1584
+ mergeTarget = null, pushBlock(block);
1585
+ continue;
1586
+ }
1587
+ let textBlock = block;
1588
+ if (mergeTarget && mergeTarget.style === textBlock.style) {
1589
+ mergeTarget.children.push(...textBlock.children), mergeTarget.markDefs = [...mergeTarget.markDefs ?? [], ...textBlock.markDefs ?? []];
1590
+ continue;
1591
+ }
1592
+ mergeTarget = {
1593
+ ...textBlock,
1594
+ listItem: itemListType,
1595
+ level,
1596
+ ...itemChecked === void 0 ? {} : { checked: itemChecked }
1597
+ }, pushBlock(mergeTarget);
1598
+ }
1599
+ }
986
1600
  }
987
1601
  }
988
1602
  break;
@@ -996,7 +1610,10 @@ function markdownToPortableText(markdown, options) {
996
1610
  _key: consolidatedOptions.keyGenerator(),
997
1611
  ...taskChecked === void 0 ? {} : { checked: taskChecked },
998
1612
  content: []
999
- }, inListItem = !0;
1613
+ }, taskChecked !== void 0 && taskInfoByListItem.set(frame.currentItem, {
1614
+ line: lineOf(token),
1615
+ snippet: truncateSnippet(taskItemTextByListItemIndex.get(tokenIndex) ?? "")
1616
+ }), inListItem = !0;
1000
1617
  break;
1001
1618
  }
1002
1619
  let baseListType = currentListStack.at(-1);
@@ -1007,10 +1624,25 @@ function markdownToPortableText(markdown, options) {
1007
1624
  taskListType && (listType = taskListType, checked = taskChecked);
1008
1625
  }
1009
1626
  if (listType === null) {
1627
+ let pendingFlattened = pendingListFlattenedStack.at(-1);
1628
+ pendingFlattened && !pendingFlattened.reported && (report({
1629
+ type: "list-flattened",
1630
+ message: degradationMessage["list-flattened"](pendingFlattened.kindName),
1631
+ line: pendingFlattened.line
1632
+ }), pendingFlattened.reported = !0);
1010
1633
  let style = currentBlockquoteStyle ?? consolidatedOptions.block.normal({ context: { schema: consolidatedOptions.schema } });
1011
- style ? startBlock(style) : (console.warn("No default style found, using \"normal\""), startBlock("normal")), inListItem = !0;
1634
+ style ? startBlock(style, { tookBlockquoteStyle: currentBlockquoteStyle !== null }) : (reportStyleFallback("normal", lineOf(token)), startBlock("normal")), inListItem = !0;
1012
1635
  break;
1013
1636
  }
1637
+ if (taskChecked !== void 0 && checked === void 0) {
1638
+ let snippet = truncateSnippet(taskItemTextByListItemIndex.get(tokenIndex) ?? "");
1639
+ report({
1640
+ type: "task-checkbox-stripped",
1641
+ message: degradationMessage["task-checkbox-stripped"](taskChecked),
1642
+ line: lineOf(token),
1643
+ snippet
1644
+ });
1645
+ }
1014
1646
  ensureListBlock(listType, checked), inListItem = !0;
1015
1647
  break;
1016
1648
  }
@@ -1025,7 +1657,21 @@ function markdownToPortableText(markdown, options) {
1025
1657
  }
1026
1658
  case "fence": {
1027
1659
  flushBlock();
1028
- let language = token.info.trim() || void 0, code = token.content.replace(/\n$/, ""), codeObject = consolidatedOptions.types.code({
1660
+ let language = token.info.trim() || void 0, code = token.content.replace(/\n$/, "");
1661
+ if (language === "json:object") {
1662
+ let objectValue = parseJsonObjectFence(code);
1663
+ if (objectValue) {
1664
+ pushBlock(objectValue);
1665
+ break;
1666
+ }
1667
+ report({
1668
+ type: "object-carrier-invalid",
1669
+ message: degradationMessage["object-carrier-invalid"]("fence", code),
1670
+ line: lineOf(token),
1671
+ snippet: truncateSnippet(code)
1672
+ });
1673
+ }
1674
+ let codeObject = consolidatedOptions.types.code({
1029
1675
  context: {
1030
1676
  schema: consolidatedOptions.schema,
1031
1677
  keyGenerator: consolidatedOptions.keyGenerator
@@ -1037,11 +1683,18 @@ function markdownToPortableText(markdown, options) {
1037
1683
  isInline: !1
1038
1684
  });
1039
1685
  if (!codeObject) {
1686
+ let snippet = truncateSnippet(code.split("\n")[0] ?? "");
1687
+ report({
1688
+ type: "code-block-to-text",
1689
+ message: degradationMessage["code-block-to-text"](language),
1690
+ line: lineOf(token),
1691
+ snippet
1692
+ });
1040
1693
  let style = consolidatedOptions.block.normal({ context: { schema: consolidatedOptions.schema } });
1041
- style ? startBlock(style) : (console.warn("No default style found, using \"normal\""), startBlock("normal")), addSpan(code), flushBlock();
1694
+ style ? startBlock(style) : (reportStyleFallback("normal", lineOf(token)), startBlock("normal")), addSpan(code), flushBlock();
1042
1695
  break;
1043
1696
  }
1044
- pushBlock(codeObject);
1697
+ reportFieldsDropped(codeObject, lineOf(token)), pushBlock(codeObject);
1045
1698
  break;
1046
1699
  }
1047
1700
  case "hr": {
@@ -1055,8 +1708,13 @@ function markdownToPortableText(markdown, options) {
1055
1708
  isInline: !1
1056
1709
  });
1057
1710
  if (!hrObject) {
1711
+ report({
1712
+ type: "horizontal-rule-to-text",
1713
+ message: degradationMessage["horizontal-rule-to-text"],
1714
+ line: lineOf(token)
1715
+ });
1058
1716
  let style = consolidatedOptions.block.normal({ context: { schema: consolidatedOptions.schema } });
1059
- style ? startBlock(style) : (console.warn("No default style found, using \"normal\""), startBlock("normal")), addSpan("---"), flushBlock();
1717
+ style ? startBlock(style) : (reportStyleFallback("normal", lineOf(token)), startBlock("normal")), addSpan("---"), flushBlock();
1060
1718
  break;
1061
1719
  }
1062
1720
  pushBlock(hrObject);
@@ -1075,11 +1733,18 @@ function markdownToPortableText(markdown, options) {
1075
1733
  isInline: !1
1076
1734
  });
1077
1735
  if (!htmlObject) {
1736
+ let snippet = truncateSnippet(htmlContent);
1737
+ report({
1738
+ type: "html-block-to-text",
1739
+ message: degradationMessage["html-block-to-text"],
1740
+ line: lineOf(token),
1741
+ snippet
1742
+ });
1078
1743
  let style = consolidatedOptions.block.normal({ context: { schema: consolidatedOptions.schema } });
1079
- style ? startBlock(style) : (console.warn("No default style found, using \"normal\""), startBlock("normal")), addSpan(htmlContent), flushBlock();
1744
+ style ? startBlock(style) : (reportStyleFallback("normal", lineOf(token)), startBlock("normal")), addSpan(htmlContent), flushBlock();
1080
1745
  break;
1081
1746
  }
1082
- pushBlock(htmlObject);
1747
+ reportFieldsDropped(htmlObject, lineOf(token)), pushBlock(htmlObject);
1083
1748
  break;
1084
1749
  }
1085
1750
  case "code_block": {
@@ -1095,10 +1760,17 @@ function markdownToPortableText(markdown, options) {
1095
1760
  },
1096
1761
  isInline: !1
1097
1762
  });
1098
- if (codeObject) pushBlock(codeObject);
1763
+ if (codeObject) reportFieldsDropped(codeObject, lineOf(token)), pushBlock(codeObject);
1099
1764
  else {
1765
+ let snippet = truncateSnippet(code.split("\n")[0] ?? "");
1766
+ report({
1767
+ type: "code-block-to-text",
1768
+ message: degradationMessage["code-block-to-text"](void 0),
1769
+ line: lineOf(token),
1770
+ snippet
1771
+ });
1100
1772
  let style = consolidatedOptions.block.normal({ context: { schema: consolidatedOptions.schema } });
1101
- style ? startBlock(style) : (console.warn("No default style found, using \"normal\""), startBlock("normal")), addSpan(code), flushBlock();
1773
+ style ? startBlock(style) : (reportStyleFallback("normal", lineOf(token)), startBlock("normal")), addSpan(code), flushBlock();
1102
1774
  }
1103
1775
  break;
1104
1776
  }
@@ -1107,7 +1779,8 @@ function markdownToPortableText(markdown, options) {
1107
1779
  rows: [],
1108
1780
  headerRows: 0,
1109
1781
  emptyHeaderDropped: !1,
1110
- alignment: []
1782
+ alignment: [],
1783
+ line: lineOf(token)
1111
1784
  };
1112
1785
  break;
1113
1786
  case "table_close":
@@ -1125,8 +1798,16 @@ function markdownToPortableText(markdown, options) {
1125
1798
  },
1126
1799
  isInline: !1
1127
1800
  });
1128
- tableObject ? pushBlock(tableObject) : flattenTable(currentTable, blockTarget());
1129
- } else flattenTable(currentTable, blockTarget());
1801
+ tableObject ? (reportFieldsDropped(tableObject, currentTable.line), pushBlock(tableObject)) : (report({
1802
+ type: "table-flattened",
1803
+ message: degradationMessage["table-flattened"],
1804
+ line: currentTable.line
1805
+ }), flattenTable(currentTable, blockTarget()));
1806
+ } else report({
1807
+ type: "table-flattened",
1808
+ message: degradationMessage["table-flattened"],
1809
+ line: currentTable.line
1810
+ }), flattenTable(currentTable, blockTarget());
1130
1811
  currentTable = null;
1131
1812
  break;
1132
1813
  case "thead_open":
@@ -1151,7 +1832,7 @@ function markdownToPortableText(markdown, options) {
1151
1832
  case "td_open": {
1152
1833
  currentTable && inTableHead && token.type === "th_open" && currentTable.alignment.push(extractAlignmentFromStyleAttr(token.attrGet("style")));
1153
1834
  let style = consolidatedOptions.block.normal({ context: { schema: consolidatedOptions.schema } });
1154
- style ? startBlock(style) : (console.warn("No default style found, using \"normal\""), startBlock("normal"));
1835
+ style ? startBlock(style) : (reportStyleFallback("normal", currentTable?.line), startBlock("normal"));
1155
1836
  break;
1156
1837
  }
1157
1838
  case "th_close":
@@ -1174,10 +1855,21 @@ function markdownToPortableText(markdown, options) {
1174
1855
  _key: consolidatedOptions.keyGenerator(),
1175
1856
  markDefs: []
1176
1857
  });
1177
- let firstBlock = cellBlocks[0];
1858
+ let demotedInlineImages = [];
1859
+ 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);
1860
+ let firstBlock = cellBlocks[0], liftedImage;
1178
1861
  if (cellBlocks.length === 1 && firstBlock && firstBlock._type === "block" && "children" in firstBlock && Array.isArray(firstBlock.children) && firstBlock.children.length === 1) {
1179
1862
  let onlyChild = firstBlock.children[0];
1180
- typeof onlyChild == "object" && onlyChild && "_type" in onlyChild && onlyChild._type !== consolidatedOptions.schema.span.name && onlyChild._type === "image" && (cellBlocks[0] = onlyChild);
1863
+ typeof onlyChild == "object" && onlyChild && "_type" in onlyChild && onlyChild._type !== consolidatedOptions.schema.span.name && onlyChild._type === "image" && (cellBlocks[0] = onlyChild, liftedImage = onlyChild);
1864
+ }
1865
+ for (let demotedImage of demotedInlineImages) if (demotedImage !== liftedImage) {
1866
+ let { alt, src } = demotedImage;
1867
+ report({
1868
+ type: "image-block-to-inline",
1869
+ message: degradationMessage["image-block-to-inline"](demotedTableImages.get(demotedImage) ?? "table-cell"),
1870
+ line: currentTable?.line,
1871
+ snippet: truncateSnippet(alt || src || "")
1872
+ });
1181
1873
  }
1182
1874
  currentTableRow !== null && currentTableRow.push({
1183
1875
  _type: "cell",
@@ -1187,7 +1879,7 @@ function markdownToPortableText(markdown, options) {
1187
1879
  break;
1188
1880
  }
1189
1881
  case "inline": {
1190
- let inTableCell = currentTableRow !== null;
1882
+ let inTableCell = currentTableRow !== null, inlineLine = () => lineOf(token) ?? currentTable?.line;
1191
1883
  if (token.children?.length === 1 && token.children[0]?.type === "image") {
1192
1884
  let imageToken = token.children[0];
1193
1885
  if (!imageToken) break;
@@ -1204,7 +1896,7 @@ function markdownToPortableText(markdown, options) {
1204
1896
  isInline: !1
1205
1897
  });
1206
1898
  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));
1899
+ 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
1900
  break;
1209
1901
  }
1210
1902
  let inlineImageObject = consolidatedOptions.types.image({
@@ -1220,165 +1912,281 @@ function markdownToPortableText(markdown, options) {
1220
1912
  isInline: !0
1221
1913
  });
1222
1914
  if (inlineImageObject) {
1223
- if (!currentBlock) if (inListItem) if (listContainerStack.at(-1)) startBlock(consolidatedOptions.block.normal({ context: { schema: consolidatedOptions.schema } }) ?? "normal");
1224
- else {
1225
- let listType = currentListStack.at(-1);
1226
- listType && ensureListBlock(listType);
1227
- }
1228
- else {
1229
- let style = consolidatedOptions.block.normal({ context: { schema: consolidatedOptions.schema } });
1230
- style && startBlock(style);
1915
+ if (reportFieldsDropped(inlineImageObject, inlineLine()), inTableCell ? demotedTableImages.set(inlineImageObject, "no-block-image") : report({
1916
+ type: "image-block-to-inline",
1917
+ message: degradationMessage["image-block-to-inline"]("no-block-image"),
1918
+ line: inlineLine(),
1919
+ snippet: truncateSnippet(alt || src)
1920
+ }), !currentBlock) {
1921
+ if (inListItem) {
1922
+ if (listContainerStack.at(-1)) {
1923
+ let style = consolidatedOptions.block.normal({ context: { schema: consolidatedOptions.schema } });
1924
+ style || reportStyleFallback("normal", inlineLine()), startBlock(style ?? "normal");
1925
+ } else {
1926
+ let listType = currentListStack.at(-1);
1927
+ listType && ensureListBlock(listType);
1928
+ }
1929
+ } else {
1930
+ let style = consolidatedOptions.block.normal({ context: { schema: consolidatedOptions.schema } });
1931
+ style && startBlock(style);
1932
+ }
1231
1933
  }
1232
1934
  currentBlock && "children" in currentBlock && currentBlock.children.push(inlineImageObject);
1233
1935
  break;
1234
1936
  }
1235
- addSpan(`![${alt}](${src})`);
1937
+ let standaloneImageSnippet = truncateSnippet(alt || src);
1938
+ report({
1939
+ type: "image-to-text",
1940
+ message: degradationMessage["image-to-text"],
1941
+ line: inlineLine(),
1942
+ snippet: standaloneImageSnippet
1943
+ }), addSpan(`![${alt}](${src})`);
1236
1944
  break;
1237
1945
  }
1238
- for (let childToken of token.children ?? []) switch (childToken.type) {
1239
- case "text":
1240
- addSpan(childToken.content);
1241
- break;
1242
- case "softbreak":
1243
- case "hardbreak":
1244
- addSpan("\n");
1245
- break;
1246
- case "code_inline": {
1247
- let decorator = consolidatedOptions.marks.code({ context: { schema: consolidatedOptions.schema } });
1248
- if (!decorator) {
1946
+ let inlineChildren = token.children ?? [];
1947
+ for (let childIndex = 0; childIndex < inlineChildren.length; childIndex++) {
1948
+ let childToken = inlineChildren[childIndex];
1949
+ if (childToken) switch (childToken.type) {
1950
+ case "text": {
1951
+ let nextToken = inlineChildren[childIndex + 1];
1952
+ if (childToken.content.endsWith("json:object") && nextToken?.type === "code_inline" && currentBlock && "children" in currentBlock) {
1953
+ let objectValue = parseJsonObjectFence(nextToken.content);
1954
+ if (objectValue) {
1955
+ let prefix = childToken.content.slice(0, -11);
1956
+ prefix.length > 0 && addSpan(prefix), currentBlock.children.push(objectValue), childIndex++;
1957
+ break;
1958
+ }
1959
+ report({
1960
+ type: "object-carrier-invalid",
1961
+ message: degradationMessage["object-carrier-invalid"]("code-span", nextToken.content),
1962
+ line: inlineLine(),
1963
+ snippet: truncateSnippet(nextToken.content)
1964
+ });
1965
+ }
1249
1966
  addSpan(childToken.content);
1250
1967
  break;
1251
1968
  }
1252
- markDefRefs.push(decorator), addSpan(childToken.content);
1253
- let index = markDefRefs.lastIndexOf(decorator);
1254
- index !== -1 && markDefRefs.splice(index, 1);
1255
- break;
1256
- }
1257
- case "strong_open": {
1258
- let decorator = consolidatedOptions.marks.strong({ context: { schema: consolidatedOptions.schema } });
1259
- if (!decorator) break;
1260
- markDefRefs.push(decorator);
1261
- break;
1262
- }
1263
- case "strong_close": {
1264
- let decorator = consolidatedOptions.marks.strong({ context: { schema: consolidatedOptions.schema } });
1265
- if (!decorator) break;
1266
- let index = markDefRefs.lastIndexOf(decorator);
1267
- index !== -1 && markDefRefs.splice(index, 1);
1268
- break;
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
1969
+ case "softbreak":
1970
+ addSpan(" ");
1971
+ break;
1972
+ case "hardbreak":
1973
+ addSpan("\n");
1974
+ break;
1975
+ case "code_inline": {
1976
+ let decorator = consolidatedOptions.marks.code({ context: { schema: consolidatedOptions.schema } });
1977
+ if (!decorator) {
1978
+ let codeSnippet = truncateSnippet(childToken.content);
1979
+ report({
1980
+ type: "decorator-dropped",
1981
+ message: degradationMessage["decorator-dropped"]("code"),
1982
+ line: inlineLine(),
1983
+ snippet: codeSnippet
1984
+ }), addSpan(childToken.content);
1985
+ break;
1307
1986
  }
1308
- });
1309
- if (!linkObject) break;
1310
- currentMarkDefs.push(linkObject), markDefRefs.push(linkObject._key);
1311
- break;
1312
- }
1313
- case "link_close": {
1314
- let markDefKeys = new Set(currentMarkDefs.map((d) => d._key)), lastLinkIndex;
1315
- for (let markDefRef of markDefRefs.reverse()) if (markDefKeys.has(markDefRef)) {
1316
- lastLinkIndex = markDefRefs.indexOf(markDefRef);
1987
+ markDefRefs.push(decorator), addSpan(childToken.content);
1988
+ let index = markDefRefs.lastIndexOf(decorator);
1989
+ index !== -1 && markDefRefs.splice(index, 1);
1317
1990
  break;
1318
1991
  }
1319
- if (lastLinkIndex !== void 0) {
1320
- let realIndex = markDefRefs.length - 1 - lastLinkIndex;
1321
- markDefRefs.splice(realIndex, 1);
1992
+ case "strong_open": {
1993
+ let decorator = consolidatedOptions.marks.strong({ context: { schema: consolidatedOptions.schema } });
1994
+ if (!decorator) {
1995
+ let strongSnippet = truncateSnippet(collectInlineText(inlineChildren, childIndex, "strong_open", "strong_close"));
1996
+ report({
1997
+ type: "decorator-dropped",
1998
+ message: degradationMessage["decorator-dropped"]("strong"),
1999
+ line: inlineLine(),
2000
+ snippet: strongSnippet
2001
+ });
2002
+ break;
2003
+ }
2004
+ markDefRefs.push(decorator);
2005
+ break;
1322
2006
  }
1323
- break;
1324
- }
1325
- case "image": {
1326
- let src = childToken.attrs?.find(([name]) => name === "src")?.at(1) || "", alt = unescapeImageAndLinkText(childToken.content || ""), inlineImageObject = consolidatedOptions.types.image({
1327
- context: {
1328
- schema: consolidatedOptions.schema,
1329
- keyGenerator: consolidatedOptions.keyGenerator
1330
- },
1331
- value: {
1332
- src,
1333
- alt,
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"));
2007
+ case "strong_close": {
2008
+ let decorator = consolidatedOptions.marks.strong({ context: { schema: consolidatedOptions.schema } });
2009
+ if (!decorator) break;
2010
+ let index = markDefRefs.lastIndexOf(decorator);
2011
+ index !== -1 && markDefRefs.splice(index, 1);
2012
+ break;
2013
+ }
2014
+ case "em_open": {
2015
+ let decorator = consolidatedOptions.marks.em({ context: { schema: consolidatedOptions.schema } });
2016
+ if (!decorator) {
2017
+ let emSnippet = truncateSnippet(collectInlineText(inlineChildren, childIndex, "em_open", "em_close"));
2018
+ report({
2019
+ type: "decorator-dropped",
2020
+ message: degradationMessage["decorator-dropped"]("em"),
2021
+ line: inlineLine(),
2022
+ snippet: emSnippet
2023
+ });
2024
+ break;
1342
2025
  }
1343
- if (!currentBlock) throw Error("Expected current block after startBlock");
1344
- currentBlock.children.push(inlineImageObject);
2026
+ markDefRefs.push(decorator);
1345
2027
  break;
1346
2028
  }
1347
- let blockImageObject = consolidatedOptions.types.image({
1348
- context: {
1349
- schema: consolidatedOptions.schema,
1350
- keyGenerator: consolidatedOptions.keyGenerator
1351
- },
1352
- value: {
1353
- src,
1354
- alt,
1355
- title: void 0
1356
- },
1357
- isInline: !1
1358
- });
1359
- if (!blockImageObject) {
1360
- addSpan(`![${alt}](${src})`);
2029
+ case "em_close": {
2030
+ let decorator = consolidatedOptions.marks.em({ context: { schema: consolidatedOptions.schema } });
2031
+ if (!decorator) break;
2032
+ let index = markDefRefs.lastIndexOf(decorator);
2033
+ index !== -1 && markDefRefs.splice(index, 1);
1361
2034
  break;
1362
2035
  }
1363
- if (inTableCell) {
1364
- currentBlock && "children" in currentBlock && currentBlock.children.push(blockImageObject);
2036
+ case "s_open": {
2037
+ let decorator = consolidatedOptions.marks.strikeThrough({ context: { schema: consolidatedOptions.schema } });
2038
+ if (!decorator) {
2039
+ let strikeSnippet = truncateSnippet(collectInlineText(inlineChildren, childIndex, "s_open", "s_close"));
2040
+ report({
2041
+ type: "decorator-dropped",
2042
+ message: degradationMessage["decorator-dropped"]("strikeThrough"),
2043
+ line: inlineLine(),
2044
+ snippet: strikeSnippet
2045
+ });
2046
+ break;
2047
+ }
2048
+ markDefRefs.push(decorator);
1365
2049
  break;
1366
2050
  }
1367
- flushBlock(), pushBlock(blockImageObject);
1368
- let style = consolidatedOptions.block.normal({ context: { schema: consolidatedOptions.schema } });
1369
- style && startBlock(style);
1370
- break;
2051
+ case "s_close": {
2052
+ let decorator = consolidatedOptions.marks.strikeThrough({ context: { schema: consolidatedOptions.schema } });
2053
+ if (!decorator) break;
2054
+ let index = markDefRefs.lastIndexOf(decorator);
2055
+ index !== -1 && markDefRefs.splice(index, 1);
2056
+ break;
2057
+ }
2058
+ case "link_open": {
2059
+ let href = childToken.attrs?.find(([name]) => name === "href")?.at(1);
2060
+ if (!href) {
2061
+ let missingHrefSnippet = truncateSnippet(collectInlineText(inlineChildren, childIndex, "link_open", "link_close"));
2062
+ report({
2063
+ type: "annotation-dropped",
2064
+ message: degradationMessage["annotation-dropped"]("missing-url"),
2065
+ line: inlineLine(),
2066
+ snippet: missingHrefSnippet
2067
+ });
2068
+ break;
2069
+ }
2070
+ let title = childToken.attrs?.find(([name]) => name === "title")?.at(1), linkObject = consolidatedOptions.marks.link({
2071
+ context: {
2072
+ schema: consolidatedOptions.schema,
2073
+ keyGenerator: consolidatedOptions.keyGenerator
2074
+ },
2075
+ value: {
2076
+ href,
2077
+ title
2078
+ }
2079
+ });
2080
+ if (!linkObject) {
2081
+ let linkSnippet = truncateSnippet(collectInlineText(inlineChildren, childIndex, "link_open", "link_close"));
2082
+ report({
2083
+ type: "annotation-dropped",
2084
+ message: degradationMessage["annotation-dropped"]("no-annotation"),
2085
+ line: inlineLine(),
2086
+ snippet: linkSnippet
2087
+ });
2088
+ break;
2089
+ }
2090
+ reportFieldsDropped(linkObject, inlineLine()), currentMarkDefs.push(linkObject), markDefRefs.push(linkObject._key);
2091
+ break;
2092
+ }
2093
+ case "link_close": {
2094
+ let markDefKeys = new Set(currentMarkDefs.map((d) => d._key)), lastLinkIndex;
2095
+ for (let markDefRef of markDefRefs.reverse()) if (markDefKeys.has(markDefRef)) {
2096
+ lastLinkIndex = markDefRefs.indexOf(markDefRef);
2097
+ break;
2098
+ }
2099
+ if (lastLinkIndex !== void 0) {
2100
+ let realIndex = markDefRefs.length - 1 - lastLinkIndex;
2101
+ markDefRefs.splice(realIndex, 1);
2102
+ }
2103
+ break;
2104
+ }
2105
+ case "image": {
2106
+ let src = childToken.attrs?.find(([name]) => name === "src")?.at(1) || "", alt = unescapeImageAndLinkText(childToken.content || ""), inlineImageObject = consolidatedOptions.types.image({
2107
+ context: {
2108
+ schema: consolidatedOptions.schema,
2109
+ keyGenerator: consolidatedOptions.keyGenerator
2110
+ },
2111
+ value: {
2112
+ src,
2113
+ alt,
2114
+ title: void 0
2115
+ },
2116
+ isInline: !0
2117
+ });
2118
+ if (inlineImageObject) {
2119
+ if (reportFieldsDropped(inlineImageObject, inlineLine()), !currentBlock) {
2120
+ let style = consolidatedOptions.block.normal({ context: { schema: consolidatedOptions.schema } });
2121
+ style ? startBlock(style) : (reportStyleFallback("normal", inlineLine()), startBlock("normal"));
2122
+ }
2123
+ if (!currentBlock) throw Error("Expected current block after startBlock");
2124
+ currentBlock.children.push(inlineImageObject);
2125
+ break;
2126
+ }
2127
+ let blockImageObject = consolidatedOptions.types.image({
2128
+ context: {
2129
+ schema: consolidatedOptions.schema,
2130
+ keyGenerator: consolidatedOptions.keyGenerator
2131
+ },
2132
+ value: {
2133
+ src,
2134
+ alt,
2135
+ title: void 0
2136
+ },
2137
+ isInline: !1
2138
+ });
2139
+ if (!blockImageObject) {
2140
+ let inlineImageSnippet = truncateSnippet(alt || src);
2141
+ report({
2142
+ type: "image-to-text",
2143
+ message: degradationMessage["image-to-text"],
2144
+ line: inlineLine(),
2145
+ snippet: inlineImageSnippet
2146
+ }), addSpan(`![${alt}](${src})`);
2147
+ break;
2148
+ }
2149
+ if (inTableCell) {
2150
+ reportFieldsDropped(blockImageObject, inlineLine()), demotedTableImages.set(blockImageObject, "table-cell"), currentBlock && "children" in currentBlock && currentBlock.children.push(blockImageObject);
2151
+ break;
2152
+ }
2153
+ reportFieldsDropped(blockImageObject, inlineLine()), report({
2154
+ type: "image-inline-to-block",
2155
+ message: degradationMessage["image-inline-to-block"],
2156
+ line: inlineLine(),
2157
+ snippet: truncateSnippet(alt || src)
2158
+ }), flushBlock(), pushBlock(blockImageObject);
2159
+ let style = consolidatedOptions.block.normal({ context: { schema: consolidatedOptions.schema } });
2160
+ style && startBlock(style);
2161
+ break;
2162
+ }
2163
+ case "html_inline": if (consolidatedOptions.html.inline === "text") addSpan(childToken.content);
2164
+ else if (childToken.content) {
2165
+ let htmlInlineSnippet = truncateSnippet(childToken.content);
2166
+ report({
2167
+ type: "inline-html-dropped",
2168
+ message: degradationMessage["inline-html-dropped"],
2169
+ line: inlineLine(),
2170
+ snippet: htmlInlineSnippet
2171
+ });
2172
+ }
1371
2173
  }
1372
- case "html_inline": consolidatedOptions.html.inline === "text" && addSpan(childToken.content);
1373
2174
  }
1374
2175
  break;
1375
2176
  }
1376
- case "alert_open":
1377
- flushBlock(), calloutStartTarget = blockTarget(), calloutStartIndex = calloutStartTarget.length, calloutType = token.markup, currentBlockquoteStyle = consolidatedOptions.block.blockquote({ context: { schema: consolidatedOptions.schema } }) ?? consolidatedOptions.block.normal({ context: { schema: consolidatedOptions.schema } }) ?? "normal";
2177
+ case "alert_open": {
2178
+ flushBlock(), calloutStartTarget = blockTarget(), calloutStartIndex = calloutStartTarget.length, calloutType = token.markup, calloutStartLine = lineOf(token);
2179
+ let blockquoteStyle = consolidatedOptions.block.blockquote({ context: { schema: consolidatedOptions.schema } });
2180
+ calloutPendingStyleFallbacks = [], blockquoteStyle || calloutPendingStyleFallbacks.push("blockquote");
2181
+ let style = blockquoteStyle ?? consolidatedOptions.block.normal({ context: { schema: consolidatedOptions.schema } });
2182
+ style || calloutPendingStyleFallbacks.push("normal"), currentBlockquoteStyle = style ?? "normal";
2183
+ break;
2184
+ }
2185
+ case "alert_title":
2186
+ calloutStartLine = lineOf(token);
1378
2187
  break;
1379
- case "alert_title": break;
1380
2188
  case "alert_close":
1381
- if (flushBlock(), calloutStartIndex !== null && calloutType !== null && calloutStartTarget !== null) {
2189
+ if (flushBlock(), calloutPendingStyleFallbacks = [], calloutStartIndex !== null && calloutType !== null && calloutStartTarget !== null) {
1382
2190
  let contentBlocks = calloutStartTarget.splice(calloutStartIndex), calloutObject = consolidatedOptions.types.callout?.({
1383
2191
  context: {
1384
2192
  schema: consolidatedOptions.schema,
@@ -1390,13 +2198,41 @@ function markdownToPortableText(markdown, options) {
1390
2198
  },
1391
2199
  isInline: !1
1392
2200
  });
1393
- if (calloutObject) pushBlock(calloutObject);
1394
- else for (let block of contentBlocks) pushBlock(block);
2201
+ if (calloutObject) reportFieldsDropped(calloutObject, calloutStartLine), pushBlock(calloutObject);
2202
+ else {
2203
+ report({
2204
+ type: "callout-fallback",
2205
+ message: degradationMessage["callout-fallback"](calloutType, currentBlockquoteStyle ?? "normal"),
2206
+ line: calloutStartLine
2207
+ });
2208
+ for (let block of contentBlocks) pushBlock(block);
2209
+ }
1395
2210
  }
1396
- calloutStartIndex = null, calloutStartTarget = null, calloutType = null, currentBlockquoteStyle = null;
2211
+ calloutStartIndex = null, calloutStartTarget = null, calloutType = null, calloutStartLine = void 0, currentBlockquoteStyle = null;
1397
2212
  }
1398
2213
  }
1399
- return flushBlock(), portableText;
2214
+ return flushBlock(), degradationEvents.length > 0 && options?.onDegradation?.({
2215
+ degradations: degradationEvents,
2216
+ message: buildDegradationMessage(degradationEvents)
2217
+ }), portableText;
2218
+ }
2219
+ /**
2220
+ * A `json:object` fence always reconstructs its object, schema or no
2221
+ * schema: the fence carries its own `_type`, and degrading it to a code
2222
+ * block would reintroduce the loss the syntax exists to remove. Returns
2223
+ * `undefined` instead of throwing, so an unusable fence falls through to
2224
+ * the regular code path.
2225
+ */
2226
+ function parseJsonObjectFence(code) {
2227
+ let parsed;
2228
+ try {
2229
+ parsed = JSON.parse(code);
2230
+ } catch {
2231
+ return;
2232
+ }
2233
+ if (typeof parsed != "object" || !parsed || Array.isArray(parsed)) return;
2234
+ let objectValue = parsed;
2235
+ if (typeof objectValue._type == "string" && objectValue._type.length !== 0) return objectValue;
1400
2236
  }
1401
2237
  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 };
1402
2238