@astryxdesign/core 0.6.2-canary.bfee8a4 → 0.6.2-canary.cb7a565

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.
Files changed (157) hide show
  1. package/dist/CheckboxInput/CheckboxInput.d.ts.map +1 -1
  2. package/dist/CheckboxInput/CheckboxInput.js +12 -2
  3. package/dist/Collapsible/Collapsible.d.ts.map +1 -1
  4. package/dist/Collapsible/Collapsible.js +6 -1
  5. package/dist/Kbd/Kbd.d.ts +5 -3
  6. package/dist/Kbd/Kbd.d.ts.map +1 -1
  7. package/dist/Kbd/Kbd.js +36 -42
  8. package/dist/Link/Link.d.ts.map +1 -1
  9. package/dist/Link/Link.js +6 -2
  10. package/dist/Markdown/Markdown.d.ts +8 -2
  11. package/dist/Markdown/Markdown.d.ts.map +1 -1
  12. package/dist/Markdown/Markdown.fr23.bench.d.ts +2 -0
  13. package/dist/Markdown/Markdown.fr23.bench.d.ts.map +1 -0
  14. package/dist/Markdown/Markdown.fr23.bench.js +248 -0
  15. package/dist/Markdown/Markdown.js +262 -109
  16. package/dist/Markdown/ast.d.ts +162 -0
  17. package/dist/Markdown/ast.d.ts.map +1 -0
  18. package/dist/Markdown/ast.js +74 -0
  19. package/dist/Markdown/index.d.ts +3 -1
  20. package/dist/Markdown/index.d.ts.map +1 -1
  21. package/dist/Markdown/index.js +1 -0
  22. package/dist/Markdown/parser.d.ts +104 -58
  23. package/dist/Markdown/parser.d.ts.map +1 -1
  24. package/dist/Markdown/parser.js +693 -167
  25. package/dist/Markdown/plugins/index.d.ts +14 -0
  26. package/dist/Markdown/plugins/index.d.ts.map +1 -0
  27. package/dist/Markdown/plugins/index.js +12 -0
  28. package/dist/Markdown/plugins/protocol.d.ts +191 -0
  29. package/dist/Markdown/plugins/protocol.d.ts.map +1 -0
  30. package/dist/Markdown/plugins/protocol.js +957 -0
  31. package/dist/Markdown/plugins/semanticFence.d.ts +36 -0
  32. package/dist/Markdown/plugins/semanticFence.d.ts.map +1 -0
  33. package/dist/Markdown/plugins/semanticFence.js +213 -0
  34. package/dist/Markdown/plugins/sourceDecoration.d.ts +76 -0
  35. package/dist/Markdown/plugins/sourceDecoration.d.ts.map +1 -0
  36. package/dist/Markdown/plugins/sourceDecoration.js +393 -0
  37. package/dist/Markdown/plugins/textTransform.d.ts +30 -0
  38. package/dist/Markdown/plugins/textTransform.d.ts.map +1 -0
  39. package/dist/Markdown/plugins/textTransform.js +374 -0
  40. package/dist/Markdown/url.d.ts +4 -0
  41. package/dist/Markdown/url.d.ts.map +1 -0
  42. package/dist/Markdown/url.js +25 -0
  43. package/dist/Markdown/utils.d.ts +4 -2
  44. package/dist/Markdown/utils.d.ts.map +1 -1
  45. package/dist/Markdown/utils.js +2 -1
  46. package/dist/Outline/index.d.ts +1 -0
  47. package/dist/Outline/index.d.ts.map +1 -1
  48. package/dist/Outline/parseOutlineFromMarkdown.d.ts +7 -1
  49. package/dist/Outline/parseOutlineFromMarkdown.d.ts.map +1 -1
  50. package/dist/Outline/parseOutlineFromMarkdown.js +14 -8
  51. package/dist/Outline/useOutlineFromMarkdown.d.ts +3 -1
  52. package/dist/Outline/useOutlineFromMarkdown.d.ts.map +1 -1
  53. package/dist/Outline/useOutlineFromMarkdown.js +8 -3
  54. package/dist/PowerSearch/PowerSearchEditPopover.d.ts.map +1 -1
  55. package/dist/PowerSearch/PowerSearchEditPopover.js +46 -30
  56. package/dist/RadioList/RadioListItem.d.ts.map +1 -1
  57. package/dist/RadioList/RadioListItem.js +13 -1
  58. package/dist/SegmentedControl/SegmentedControlItem.d.ts.map +1 -1
  59. package/dist/SegmentedControl/SegmentedControlItem.js +5 -5
  60. package/dist/SideNav/SideNav.d.ts +2 -1
  61. package/dist/SideNav/SideNav.d.ts.map +1 -1
  62. package/dist/SideNav/SideNav.js +9 -3
  63. package/dist/Slider/Slider.d.ts.map +1 -1
  64. package/dist/Slider/Slider.js +14 -8
  65. package/dist/Switch/Switch.d.ts.map +1 -1
  66. package/dist/Switch/Switch.js +11 -0
  67. package/dist/TabList/Tab.d.ts +1 -1
  68. package/dist/TabList/Tab.d.ts.map +1 -1
  69. package/dist/TabList/Tab.js +20 -7
  70. package/dist/ToggleButton/ToggleButton.d.ts +2 -1
  71. package/dist/ToggleButton/ToggleButton.d.ts.map +1 -1
  72. package/dist/ToggleButton/ToggleButton.js +7 -1
  73. package/dist/Typeahead/BaseTypeahead.d.ts.map +1 -1
  74. package/dist/Typeahead/BaseTypeahead.js +15 -6
  75. package/dist/astryx.css +8 -0
  76. package/dist/utils/interactionOverlay.stylex.d.ts +8 -0
  77. package/dist/utils/interactionOverlay.stylex.d.ts.map +1 -1
  78. package/dist/utils/interactionOverlay.stylex.js +9 -0
  79. package/package.json +8 -3
  80. package/src/Button/__tests__/Button.a11y.chromium.spec.ts +17 -1
  81. package/src/Button/__tests__/Button.a11y.known-failures.ts +0 -29
  82. package/src/Button/__tests__/Button.a11y.renders.tsx +9 -2
  83. package/src/Button/__tests__/Button.a11y.states.ts +9 -0
  84. package/src/CheckboxInput/CheckboxInput.doc.mjs +11 -0
  85. package/src/CheckboxInput/CheckboxInput.test.tsx +34 -0
  86. package/src/CheckboxInput/CheckboxInput.tsx +21 -1
  87. package/src/ClickableCard/ClickableCard.test.tsx +102 -5
  88. package/src/Collapsible/Collapsible.doc.mjs +11 -0
  89. package/src/Collapsible/Collapsible.test.tsx +21 -0
  90. package/src/Collapsible/Collapsible.tsx +5 -0
  91. package/src/Kbd/Kbd.doc.mjs +3 -3
  92. package/src/Kbd/Kbd.test.tsx +57 -1
  93. package/src/Kbd/Kbd.tsx +57 -37
  94. package/src/Link/Link.doc.mjs +11 -0
  95. package/src/Link/Link.test.tsx +24 -0
  96. package/src/Link/Link.tsx +5 -0
  97. package/src/Markdown/Markdown.doc.mjs +175 -0
  98. package/src/Markdown/Markdown.fr23.bench.ts +288 -0
  99. package/src/Markdown/Markdown.fr23.perf.test.ts +211 -0
  100. package/src/Markdown/Markdown.helpers.perf.test.ts +489 -0
  101. package/src/Markdown/Markdown.public.test.ts +126 -3
  102. package/src/Markdown/Markdown.spec.md +159 -43
  103. package/src/Markdown/Markdown.test.tsx +11 -0
  104. package/src/Markdown/Markdown.tsx +414 -135
  105. package/src/Markdown/ast.test.ts +307 -0
  106. package/src/Markdown/ast.ts +337 -0
  107. package/src/Markdown/incremental.test.ts +12 -0
  108. package/src/Markdown/index.ts +34 -0
  109. package/src/Markdown/parser.ts +1117 -235
  110. package/src/Markdown/plugins/index.ts +47 -0
  111. package/src/Markdown/plugins/protocol.test.tsx +955 -0
  112. package/src/Markdown/plugins/protocol.ts +1619 -0
  113. package/src/Markdown/plugins/semanticFence.test.tsx +546 -0
  114. package/src/Markdown/plugins/semanticFence.ts +424 -0
  115. package/src/Markdown/plugins/sourceDecoration.test.tsx +799 -0
  116. package/src/Markdown/plugins/sourceDecoration.ts +539 -0
  117. package/src/Markdown/plugins/textTransform.test.ts +467 -0
  118. package/src/Markdown/plugins/textTransform.ts +554 -0
  119. package/src/Markdown/url.ts +30 -0
  120. package/src/Markdown/utils.ts +13 -1
  121. package/src/Outline/Outline.doc.mjs +7 -5
  122. package/src/Outline/Outline.spec.md +1 -1
  123. package/src/Outline/index.ts +1 -0
  124. package/src/Outline/modules/parseOutlineFromMarkdown.spec.md +142 -0
  125. package/src/Outline/parseOutlineFromMarkdown.ts +39 -13
  126. package/src/Outline/useOutlineFromMarkdown.ts +15 -3
  127. package/src/PowerSearch/PowerSearchEditPopover.test.tsx +150 -1
  128. package/src/PowerSearch/PowerSearchEditPopover.tsx +51 -28
  129. package/src/RadioList/RadioList.doc.mjs +11 -0
  130. package/src/RadioList/RadioList.test.tsx +32 -0
  131. package/src/RadioList/RadioListItem.tsx +25 -1
  132. package/src/SegmentedControl/SegmentedControl.doc.mjs +2 -2
  133. package/src/SegmentedControl/SegmentedControl.test.tsx +31 -0
  134. package/src/SegmentedControl/SegmentedControlItem.tsx +6 -9
  135. package/src/SideNav/SideNav.doc.mjs +1 -1
  136. package/src/SideNav/SideNav.test.tsx +10 -0
  137. package/src/SideNav/SideNav.tsx +14 -2
  138. package/src/Slider/Slider.doc.mjs +11 -0
  139. package/src/Slider/Slider.test.tsx +128 -0
  140. package/src/Slider/Slider.tsx +25 -7
  141. package/src/Switch/Switch.doc.mjs +11 -0
  142. package/src/Switch/Switch.test.tsx +16 -0
  143. package/src/Switch/Switch.tsx +28 -0
  144. package/src/TabList/Tab.tsx +35 -7
  145. package/src/TabList/TabList.doc.mjs +11 -0
  146. package/src/TabList/TabList.test.tsx +66 -0
  147. package/src/TabList/__tests__/Tabs.a11y.known-failures.ts +1 -34
  148. package/src/ToggleButton/ToggleButton.test.tsx +133 -0
  149. package/src/ToggleButton/ToggleButton.tsx +9 -2
  150. package/src/ToggleButton/__tests__/ToggleButton.a11y.chromium.spec.ts +209 -0
  151. package/src/Tokenizer/Tokenizer.spec.md +142 -75
  152. package/src/Typeahead/BaseTypeahead.spec.md +4 -3
  153. package/src/Typeahead/BaseTypeahead.tsx +15 -6
  154. package/src/Typeahead/Typeahead.test.tsx +53 -0
  155. package/src/__tests__/PressedState.a11y.chromium.spec.ts +813 -0
  156. package/src/__tests__/pressState.ts +93 -0
  157. package/src/utils/interactionOverlay.stylex.ts +20 -0
@@ -2,14 +2,16 @@
2
2
 
3
3
  /**
4
4
  * @file parser.ts
5
- * @input Markdown string
6
- * @output Array of MarkdownNode AST nodes, including opt-in math; heading slug
7
- * helpers
8
- * (inlineText, slugify, uniqueSlug) shared by Markdown rendering and
9
- * Outline's parseOutlineFromMarkdown
10
- * @position Core parser; consumed by Markdown.tsx and Outline
5
+ * @input Markdown string, released parse options, and optional ordered plugins
6
+ * @output Canonical MDAST-aligned nodes for internal consumers plus unchanged
7
+ * released parser-node projections; shared heading slug helpers
8
+ * @position Core parser and compatibility boundary; consumed by Markdown and Outline
11
9
  */
12
10
 
11
+ import { getMarkdownAstLegacyCodeLanguage, markMarkdownAstLegacyCodeLanguage } from "./ast.js";
12
+ import { applyMarkdownTransforms, freezeMarkdownPluginData, isMarkdownPluginData, prepareMarkdownPlugins, reportMarkdownPluginFailure } from "./plugins/protocol.js";
13
+ import { isSafeMarkdownParserUrl } from "./url.js";
14
+
13
15
  // ---------------------------------------------------------------------------
14
16
  // Types
15
17
  // ---------------------------------------------------------------------------
@@ -36,6 +38,240 @@
36
38
  * optional fields without breaking anyone.
37
39
  */
38
40
 
41
+ function projectRange(position) {
42
+ const start = position?.start.offset;
43
+ const end = position?.end.offset;
44
+ return start == null || end == null ? {} : {
45
+ range: {
46
+ start,
47
+ end
48
+ }
49
+ };
50
+ }
51
+
52
+ /**
53
+ * Canonical nodes always carry Core-authored positions; the released block
54
+ * shape exposes them as `range` only when the caller passed `sourceRanges`,
55
+ * so this projection stays byte-for-byte what it has always returned.
56
+ *
57
+ * An extension node is the one kind the projection passes through instead of
58
+ * rebuilding field by field, so it is the one kind that could carry a
59
+ * canonical `position` out into released output. Strip it unless the caller
60
+ * asked for provenance, in which case the node keeps exactly the `position`
61
+ * it has always exposed.
62
+ */
63
+ function projectExtensionNode(node, withRanges) {
64
+ if (withRanges || node.position === undefined) {
65
+ return node;
66
+ }
67
+ const {
68
+ position: _canonicalPosition,
69
+ ...released
70
+ } = node;
71
+ return released;
72
+ }
73
+ function projectInlineNode(node, cache, withRanges) {
74
+ const cached = cache.get(node);
75
+ if (cached != null) {
76
+ return cached;
77
+ }
78
+ let projected;
79
+ switch (node.type) {
80
+ case 'text':
81
+ projected = {
82
+ type: 'text',
83
+ content: node.value
84
+ };
85
+ break;
86
+ case 'strong':
87
+ projected = {
88
+ type: 'bold',
89
+ children: node.children.map(child => projectInlineNode(child, cache, withRanges))
90
+ };
91
+ break;
92
+ case 'emphasis':
93
+ projected = {
94
+ type: 'italic',
95
+ children: node.children.map(child => projectInlineNode(child, cache, withRanges))
96
+ };
97
+ break;
98
+ case 'delete':
99
+ projected = {
100
+ type: 'strikethrough',
101
+ children: node.children.map(child => projectInlineNode(child, cache, withRanges))
102
+ };
103
+ break;
104
+ case 'inlineCode':
105
+ projected = {
106
+ type: 'code',
107
+ content: node.value
108
+ };
109
+ break;
110
+ case 'inlineMath':
111
+ projected = {
112
+ type: 'math',
113
+ value: node.value
114
+ };
115
+ break;
116
+ case 'link':
117
+ projected = {
118
+ type: 'link',
119
+ href: node.url,
120
+ children: node.children.map(child => projectInlineNode(child, cache, withRanges))
121
+ };
122
+ break;
123
+ case 'image':
124
+ projected = {
125
+ type: 'image',
126
+ src: node.url,
127
+ alt: node.alt
128
+ };
129
+ break;
130
+ case 'citation':
131
+ projected = {
132
+ type: 'citation',
133
+ sourceId: node.sourceId
134
+ };
135
+ break;
136
+ case 'break':
137
+ projected = {
138
+ type: 'break'
139
+ };
140
+ break;
141
+ case 'extension':
142
+ projected = projectExtensionNode(node, withRanges);
143
+ break;
144
+ }
145
+ cache.set(node, projected);
146
+ return projected;
147
+ }
148
+ function projectTableCell(node, cache, withRanges) {
149
+ const cached = cache.get(node);
150
+ if (cached != null) {
151
+ return cached;
152
+ }
153
+ const projected = {
154
+ children: node.children.map(child => projectInlineNode(child, cache, withRanges))
155
+ };
156
+ cache.set(node, projected);
157
+ return projected;
158
+ }
159
+ function projectListItem(node, cache, withRanges) {
160
+ const cached = cache.get(node);
161
+ if (cached != null) {
162
+ return cached;
163
+ }
164
+ const projected = {
165
+ checked: node.checked,
166
+ children: node.children.map(child => projectBlockNode(child, cache, withRanges))
167
+ };
168
+ cache.set(node, projected);
169
+ return projected;
170
+ }
171
+ function projectBlockNode(node, cache, withRanges) {
172
+ const cached = cache.get(node);
173
+ if (cached != null) {
174
+ return cached;
175
+ }
176
+ const metadata = withRanges ? projectRange(node.position) : {};
177
+ let projected;
178
+ switch (node.type) {
179
+ case 'heading':
180
+ projected = {
181
+ type: 'heading',
182
+ level: node.depth,
183
+ children: node.children.map(child => projectInlineNode(child, cache, withRanges)),
184
+ ...metadata
185
+ };
186
+ break;
187
+ case 'paragraph':
188
+ projected = {
189
+ type: 'paragraph',
190
+ children: node.children.map(child => projectInlineNode(child, cache, withRanges)),
191
+ ...metadata
192
+ };
193
+ break;
194
+ case 'code':
195
+ projected = {
196
+ type: 'codeblock',
197
+ language: getMarkdownAstLegacyCodeLanguage(node) ?? 'plaintext',
198
+ content: node.value,
199
+ ...metadata
200
+ };
201
+ break;
202
+ case 'math':
203
+ projected = {
204
+ type: 'math',
205
+ value: node.value,
206
+ ...metadata
207
+ };
208
+ break;
209
+ case 'blockquote':
210
+ projected = {
211
+ type: 'blockquote',
212
+ children: node.children.map(child => projectBlockNode(child, cache, withRanges)),
213
+ ...metadata
214
+ };
215
+ break;
216
+ case 'list':
217
+ projected = {
218
+ type: 'list',
219
+ ordered: node.ordered,
220
+ start: node.start,
221
+ delimiter: node.delimiter,
222
+ loose: node.spread,
223
+ items: node.children.map(item => projectListItem(item, cache, withRanges)),
224
+ ...metadata
225
+ };
226
+ break;
227
+ case 'table':
228
+ {
229
+ const [header = {
230
+ type: 'tableRow',
231
+ children: []
232
+ }, ...rows] = node.children;
233
+ projected = {
234
+ type: 'table',
235
+ headers: header.children.map(cell => projectTableCell(cell, cache, withRanges)),
236
+ alignments: [...node.align],
237
+ rows: rows.map(row => row.children.map(cell => projectTableCell(cell, cache, withRanges))),
238
+ ...metadata
239
+ };
240
+ break;
241
+ }
242
+ case 'thematicBreak':
243
+ projected = {
244
+ type: 'hr',
245
+ ...metadata
246
+ };
247
+ break;
248
+ case 'image':
249
+ projected = {
250
+ type: 'image',
251
+ alt: node.alt,
252
+ src: node.url,
253
+ ...metadata
254
+ };
255
+ break;
256
+ case 'extension':
257
+ projected = projectExtensionNode(node, withRanges);
258
+ break;
259
+ }
260
+ cache.set(node, projected);
261
+ return projected;
262
+ }
263
+ function projectInlineNodes(nodes, withRanges, cache = new WeakMap()) {
264
+ return nodes.map(node => projectInlineNode(node, cache, withRanges));
265
+ }
266
+ function projectMarkdownRoot(root, withRanges, cache = new WeakMap()) {
267
+ return root.children.map(node => projectBlockNode(node, cache, withRanges));
268
+ }
269
+
270
+ /** Whether a caller asked for the released `range` field on its blocks. */
271
+ function wantsLegacyRanges(arg) {
272
+ return arg != null && typeof arg.has !== 'function' && arg.sourceRanges === true;
273
+ }
274
+
39
275
  // ---------------------------------------------------------------------------
40
276
  // Parse options
41
277
  // ---------------------------------------------------------------------------
@@ -56,13 +292,37 @@
56
292
  * from silently widening an exhaustive node switch.
57
293
  */
58
294
 
59
- const EMPTY_OPTS = {
60
- sourceIds: undefined,
61
- autolink: undefined
62
- };
63
- function resolveOptions(arg) {
295
+ /**
296
+ * Every resolved options object is built here, so all of them share one key
297
+ * set in one order — and therefore one hidden class. The parser reads these
298
+ * fields in its hottest loops; letting an omitted-options parse and an
299
+ * options-bag parse produce differently-shaped objects makes those reads
300
+ * polymorphic in any process that does both, which is exactly what a caller
301
+ * comparing an empty pipeline against a configured one does.
302
+ */
303
+ function makeResolvedOptions(fields) {
304
+ return {
305
+ sourceIds: fields.sourceIds,
306
+ autolink: fields.autolink,
307
+ math: fields.math,
308
+ sourceRanges: fields.sourceRanges,
309
+ astPositions: fields.astPositions ?? true,
310
+ plugins: fields.plugins,
311
+ isFinal: fields.isFinal,
312
+ allowBlockSyntax: fields.allowBlockSyntax ?? true,
313
+ baseOffset: fields.baseOffset,
314
+ linkDefs: fields.linkDefs
315
+ };
316
+ }
317
+ const EMPTY_OPTS = makeResolvedOptions({
318
+ isFinal: true
319
+ });
320
+ const EMPTY_INCREMENTAL_OPTS = makeResolvedOptions({
321
+ isFinal: false
322
+ });
323
+ function resolveOptions(arg, incremental = false) {
64
324
  if (arg == null) {
65
- return EMPTY_OPTS;
325
+ return incremental ? EMPTY_INCREMENTAL_OPTS : EMPTY_OPTS;
66
326
  }
67
327
  // Duck-type the legacy `ReadonlySet<string>` form: any object whose
68
328
  // `.has` is callable is treated as the legacy sourceIds set. This is
@@ -70,18 +330,20 @@ function resolveOptions(arg) {
70
330
  // or polyfilled `ReadonlySet` implementations as a `ParseOptions` bag
71
331
  // and silently lose citation resolution.
72
332
  if (typeof arg.has === 'function') {
73
- return {
333
+ return makeResolvedOptions({
74
334
  sourceIds: arg,
75
- autolink: undefined
76
- };
335
+ isFinal: !incremental
336
+ });
77
337
  }
78
338
  const opts = arg;
79
- return {
339
+ return makeResolvedOptions({
80
340
  sourceIds: opts.sourceIds,
81
341
  autolink: opts.autolink,
82
342
  math: opts.math === true ? true : undefined,
83
- sourceRanges: opts.sourceRanges
84
- };
343
+ sourceRanges: opts.sourceRanges,
344
+ plugins: opts.plugins != null && opts.plugins.length > 0 ? prepareMarkdownPlugins(opts.plugins) : undefined,
345
+ isFinal: incremental ? opts.isFinal === true : true
346
+ });
85
347
  }
86
348
 
87
349
  // ---------------------------------------------------------------------------
@@ -325,6 +587,11 @@ function extractLinkDefinitions(input, math = false) {
325
587
  };
326
588
  }
327
589
 
590
+ /** Order-independent signature of a citation-source set, for cache checks. */
591
+ function sourceIdsSignature(sourceIds) {
592
+ return sourceIds == null || sourceIds.size === 0 ? '' : [...sourceIds].sort().join('\u0000');
593
+ }
594
+
328
595
  /** Order-independent signature of a link-definition set, for cache checks. */
329
596
  function linkDefsSignature(defs) {
330
597
  if (defs.size === 0) {
@@ -339,6 +606,12 @@ function linkDefsSignature(defs) {
339
606
  * Returns the node plus the index just past the reference, or null when it is
340
607
  * not a resolvable reference (caller falls through to literal handling).
341
608
  */
609
+ function protectedInlineOptions(opts) {
610
+ return opts.plugins == null ? opts : {
611
+ ...opts,
612
+ plugins: undefined
613
+ };
614
+ }
342
615
  function matchReferenceLink(text, start, linkDefs, opts) {
343
616
  const textClose = text.indexOf(']', start + 1);
344
617
  if (textClose === -1) {
@@ -355,12 +628,12 @@ function matchReferenceLink(text, start, linkDefs, opts) {
355
628
  // matches nothing.
356
629
  const label = rawLabel === '' ? linkText : rawLabel;
357
630
  const href = linkDefs.get(normalizeLinkLabel(label));
358
- if (href != null && isSafeUrl(href)) {
631
+ if (href != null && isSafeMarkdownParserUrl(href)) {
359
632
  return {
360
633
  node: {
361
634
  type: 'link',
362
- href,
363
- children: parseInlineImpl(linkText, opts)
635
+ url: href,
636
+ children: parseInlineImpl(linkText, protectedInlineOptions(opts))
364
637
  },
365
638
  end: labelClose + 1
366
639
  };
@@ -374,14 +647,14 @@ function matchReferenceLink(text, start, linkDefs, opts) {
374
647
  return null;
375
648
  }
376
649
  const href = linkDefs.get(normalizeLinkLabel(linkText));
377
- if (href == null || !isSafeUrl(href)) {
650
+ if (href == null || !isSafeMarkdownParserUrl(href)) {
378
651
  return null;
379
652
  }
380
653
  return {
381
654
  node: {
382
655
  type: 'link',
383
- href,
384
- children: parseInlineImpl(linkText, opts)
656
+ url: href,
657
+ children: parseInlineImpl(linkText, protectedInlineOptions(opts))
385
658
  },
386
659
  end: textClose + 1
387
660
  };
@@ -400,11 +673,11 @@ function matchReferenceImage(text, start, linkDefs) {
400
673
  const rawLabel = text.slice(altClose + 2, labelClose);
401
674
  const label = rawLabel === '' ? alt : rawLabel;
402
675
  const src = linkDefs.get(normalizeLinkLabel(label));
403
- if (src != null && isSafeUrl(src)) {
676
+ if (src != null && isSafeMarkdownParserUrl(src)) {
404
677
  return {
405
678
  node: {
406
679
  type: 'image',
407
- src,
680
+ url: src,
408
681
  alt
409
682
  },
410
683
  end: labelClose + 1
@@ -417,13 +690,13 @@ function matchReferenceImage(text, start, linkDefs) {
417
690
  return null;
418
691
  }
419
692
  const src = linkDefs.get(normalizeLinkLabel(alt));
420
- if (src == null || !isSafeUrl(src)) {
693
+ if (src == null || !isSafeMarkdownParserUrl(src)) {
421
694
  return null;
422
695
  }
423
696
  return {
424
697
  node: {
425
698
  type: 'image',
426
- src,
699
+ url: src,
427
700
  alt
428
701
  },
429
702
  end: altClose + 1
@@ -500,26 +773,6 @@ function findInlineMathEnd(text, start) {
500
773
  }
501
774
 
502
775
  // ---------------------------------------------------------------------------
503
- // URL scheme sanitization
504
- // ---------------------------------------------------------------------------
505
-
506
- /**
507
- * Reject URLs with dangerous schemes (javascript:, vbscript:, data:) that
508
- * could execute arbitrary code when rendered as link hrefs or image srcs.
509
- * Returns true if the URL is safe to use, false otherwise.
510
- */
511
- function isSafeUrl(url) {
512
- // Trim and collapse whitespace/control chars that browsers tolerate but
513
- // could bypass a naive prefix check (e.g. "java\nscript:alert(1)").
514
- // eslint-disable-next-line no-control-regex -- control chars are the bypass
515
- const normalized = url.replace(/[\x00-\x1f\x7f]/g, '').trim();
516
- const lower = normalized.toLowerCase();
517
- if (lower.startsWith('javascript:') || lower.startsWith('vbscript:') || lower.startsWith('data:text/html')) {
518
- return false;
519
- }
520
- return true;
521
- }
522
-
523
776
  // ---------------------------------------------------------------------------
524
777
  // Inline parser
525
778
  // ---------------------------------------------------------------------------
@@ -570,8 +823,127 @@ function matchBracketCitation(text, i, opts) {
570
823
  end: closeIndex + 1
571
824
  };
572
825
  }
826
+ function currentLineStart(source, offset) {
827
+ return source.lastIndexOf('\n', offset - 1) + 1;
828
+ }
829
+ function matchExtensionSyntax(source, offset, context, opts) {
830
+ const byFirstCharacter = context === 'inline' ? opts.plugins?.inlineByFirstCharacter : opts.allowBlockSyntax === false ? undefined : opts.plugins?.blockByFirstCharacter;
831
+ const candidates = byFirstCharacter?.get(source[offset]);
832
+ if (candidates == null) {
833
+ return {
834
+ status: 'none'
835
+ };
836
+ }
837
+ const seen = new Set();
838
+ for (const candidate of candidates) {
839
+ if (seen.has(candidate)) {
840
+ continue;
841
+ }
842
+ seen.add(candidate);
843
+ let fullPrefix = false;
844
+ for (const prefix of candidate.contribution.startsWith) {
845
+ if (source.startsWith(prefix, offset)) {
846
+ fullPrefix = true;
847
+ break;
848
+ }
849
+ if (!opts.isFinal) {
850
+ const remaining = source.slice(offset);
851
+ if (remaining.length < prefix.length && prefix.startsWith(remaining)) {
852
+ return {
853
+ status: 'defer'
854
+ };
855
+ }
856
+ }
857
+ }
858
+ if (!fullPrefix) {
859
+ continue;
860
+ }
861
+ const end = Math.min(source.length, offset + candidate.contribution.maxSpan);
862
+ let result;
863
+ try {
864
+ result = candidate.contribution.tokenize({
865
+ source: end === source.length ? source : source.slice(0, end),
866
+ offset,
867
+ end,
868
+ isFinal: opts.isFinal,
869
+ context,
870
+ lineStart: currentLineStart(source, offset),
871
+ column: offset - currentLineStart(source, offset)
872
+ });
873
+ } catch (error) {
874
+ reportMarkdownPluginFailure(candidate.pluginName, 'syntax', error);
875
+ continue;
876
+ }
877
+ if (result == null || typeof result !== 'object' || typeof result.then === 'function') {
878
+ reportMarkdownPluginFailure(candidate.pluginName, 'syntax', new TypeError('Tokenizer returned an invalid result'));
879
+ continue;
880
+ }
881
+ if (result.status === 'no-match') {
882
+ continue;
883
+ }
884
+ if (result.status === 'defer') {
885
+ return !opts.isFinal && end === source.length && source.length - offset < candidate.contribution.maxSpan ? {
886
+ status: 'defer'
887
+ } : {
888
+ status: 'none'
889
+ };
890
+ }
891
+ if (result.status !== 'match' || !Number.isInteger(result.end) || result.end <= offset || result.end > end || result.node.type !== 'extension' || result.node.plugin !== candidate.pluginName || result.node.display !== context || result.node.name.trim() === '' || !isMarkdownPluginData(result.node.data) || !opts.plugins?.renderers.has(`${candidate.pluginName}\0${result.node.name}`)) {
892
+ reportMarkdownPluginFailure(candidate.pluginName, 'syntax', new TypeError('Tokenizer returned an invalid extension node'));
893
+ continue;
894
+ }
895
+ const rawNode = result.node;
896
+ const {
897
+ source: _ignoredSource,
898
+ position: _ignoredPosition,
899
+ ...safeNode
900
+ } = rawNode;
901
+ const absoluteStart = (opts.baseOffset ?? 0) + offset;
902
+ const absoluteEnd = (opts.baseOffset ?? 0) + result.end;
903
+ return {
904
+ status: 'match',
905
+ end: result.end,
906
+ node: Object.freeze({
907
+ ...safeNode,
908
+ data: freezeMarkdownPluginData(result.node.data),
909
+ source: source.slice(offset, result.end),
910
+ ...(opts.sourceRanges && context === 'block' ? {
911
+ position: Object.freeze({
912
+ start: Object.freeze({
913
+ offset: absoluteStart
914
+ }),
915
+ end: Object.freeze({
916
+ offset: absoluteEnd
917
+ })
918
+ })
919
+ } : null)
920
+ })
921
+ };
922
+ }
923
+ return {
924
+ status: 'none'
925
+ };
926
+ }
573
927
  export function parseInline(text, arg) {
574
- return parseInlineEntry(text, resolveOptions(arg));
928
+ return projectInlineNodes(parseInlineAst(text, arg), wantsLegacyRanges(arg));
929
+ }
930
+
931
+ /** @internal Canonical inline parse used by Markdown rendering. */
932
+ export function parseInlineAst(text, arg) {
933
+ const opts = resolveOptions(arg);
934
+ const nodes = parseInlineEntry(text, opts);
935
+ if ((opts.plugins?.transforms.length ?? 0) === 0) {
936
+ return nodes;
937
+ }
938
+ const transformed = applyMarkdownTransforms({
939
+ type: 'root',
940
+ children: [{
941
+ type: 'paragraph',
942
+ children: nodes
943
+ }]
944
+ }, opts.plugins, text, opts.isFinal, 'inline');
945
+ const paragraph = transformed.children[0];
946
+ return paragraph?.type === 'paragraph' ? [...paragraph.children] : nodes;
575
947
  }
576
948
 
577
949
  /**
@@ -589,13 +961,18 @@ function parseInlineEntry(text, opts) {
589
961
  }
590
962
  function parseInlineImpl(text, opts) {
591
963
  const nodes = [];
964
+ // Only a plugin that actually contributes INLINE syntax may cost anything
965
+ // per source position. A transform-only list contributes none, so it takes
966
+ // the same path as an omitted or empty one: no candidate probe per
967
+ // position, and no map lookup per character in the plain-text scan below.
968
+ const inlineExtensionStarts = opts.plugins != null && opts.plugins.inlineByFirstCharacter.size > 0 ? opts.plugins.inlineByFirstCharacter : undefined;
592
969
  let i = 0;
593
970
  while (i < text.length) {
594
971
  // --- Escape ---
595
972
  if (text[i] === '\\' && i + 1 < text.length) {
596
973
  nodes.push({
597
974
  type: 'text',
598
- content: text[i + 1]
975
+ value: text[i + 1]
599
976
  });
600
977
  i += 2;
601
978
  continue;
@@ -608,8 +985,8 @@ function parseInlineImpl(text, opts) {
608
985
  const closeIndex = text.indexOf('`'.repeat(tickCount), openIndex);
609
986
  if (closeIndex !== -1) {
610
987
  nodes.push({
611
- type: 'code',
612
- content: text.slice(openIndex, closeIndex)
988
+ type: 'inlineCode',
989
+ value: text.slice(openIndex, closeIndex)
613
990
  });
614
991
  i = closeIndex + tickCount;
615
992
  continue;
@@ -621,7 +998,7 @@ function parseInlineImpl(text, opts) {
621
998
  const closeIndex = findInlineMathEnd(text, i);
622
999
  if (closeIndex !== -1) {
623
1000
  nodes.push({
624
- type: 'math',
1001
+ type: 'inlineMath',
625
1002
  value: text.slice(i + 1, closeIndex)
626
1003
  });
627
1004
  i = closeIndex + 1;
@@ -649,16 +1026,16 @@ function parseInlineImpl(text, opts) {
649
1026
  const srcClose = findClosingParen(text, altClose + 2);
650
1027
  if (srcClose !== -1) {
651
1028
  const src = text.slice(altClose + 2, srcClose);
652
- if (!isSafeUrl(src)) {
1029
+ if (!isSafeMarkdownParserUrl(src)) {
653
1030
  // Dangerous scheme — emit as plain text.
654
1031
  nodes.push({
655
1032
  type: 'text',
656
- content: text.slice(i, srcClose + 1)
1033
+ value: text.slice(i, srcClose + 1)
657
1034
  });
658
1035
  } else {
659
1036
  nodes.push({
660
1037
  type: 'image',
661
- src,
1038
+ url: src,
662
1039
  alt: text.slice(i + 2, altClose)
663
1040
  });
664
1041
  }
@@ -698,17 +1075,17 @@ function parseInlineImpl(text, opts) {
698
1075
  const urlClose = findClosingParen(text, textClose + 2);
699
1076
  if (urlClose !== -1) {
700
1077
  const href = text.slice(textClose + 2, urlClose);
701
- if (!isSafeUrl(href)) {
1078
+ if (!isSafeMarkdownParserUrl(href)) {
702
1079
  // Dangerous scheme — emit as plain text instead of a link.
703
1080
  nodes.push({
704
1081
  type: 'text',
705
- content: text.slice(i, urlClose + 1)
1082
+ value: text.slice(i, urlClose + 1)
706
1083
  });
707
1084
  } else {
708
1085
  nodes.push({
709
1086
  type: 'link',
710
- href,
711
- children: parseInlineImpl(text.slice(i + 1, textClose), opts)
1087
+ url: href,
1088
+ children: parseInlineImpl(text.slice(i + 1, textClose), protectedInlineOptions(opts))
712
1089
  });
713
1090
  }
714
1091
  i = urlClose + 1;
@@ -737,9 +1114,9 @@ function parseInlineImpl(text, opts) {
737
1114
  const closeIndex = text.indexOf(marker, i + 3);
738
1115
  if (closeIndex !== -1 && (!isUnderscore || !isWordChar(text[closeIndex + 3]))) {
739
1116
  nodes.push({
740
- type: 'bold',
1117
+ type: 'strong',
741
1118
  children: [{
742
- type: 'italic',
1119
+ type: 'emphasis',
743
1120
  children: parseInlineImpl(text.slice(i + 3, closeIndex), opts)
744
1121
  }]
745
1122
  });
@@ -759,7 +1136,7 @@ function parseInlineImpl(text, opts) {
759
1136
  const closeIndex = text.indexOf(marker, i + 2);
760
1137
  if (closeIndex !== -1 && (!isUnderscore || !isWordChar(text[closeIndex + 2]))) {
761
1138
  nodes.push({
762
- type: 'bold',
1139
+ type: 'strong',
763
1140
  children: parseInlineImpl(text.slice(i + 2, closeIndex), opts)
764
1141
  });
765
1142
  i = closeIndex + 2;
@@ -773,7 +1150,7 @@ function parseInlineImpl(text, opts) {
773
1150
  const closeIndex = text.indexOf('~~', i + 2);
774
1151
  if (closeIndex !== -1) {
775
1152
  nodes.push({
776
- type: 'strikethrough',
1153
+ type: 'delete',
777
1154
  children: parseInlineImpl(text.slice(i + 2, closeIndex), opts)
778
1155
  });
779
1156
  i = closeIndex + 2;
@@ -790,7 +1167,7 @@ function parseInlineImpl(text, opts) {
790
1167
  const closeIndex = text.indexOf(text[i], i + 1);
791
1168
  if (closeIndex !== -1 && closeIndex > i + 1 && (!isUnderscore || !isWordChar(text[closeIndex + 1]))) {
792
1169
  nodes.push({
793
- type: 'italic',
1170
+ type: 'emphasis',
794
1171
  children: parseInlineImpl(text.slice(i + 1, closeIndex), opts)
795
1172
  });
796
1173
  i = closeIndex + 1;
@@ -799,9 +1176,22 @@ function parseInlineImpl(text, opts) {
799
1176
  }
800
1177
  }
801
1178
 
1179
+ // --- Extension syntax (built-ins and protected contexts win) ---
1180
+ if (inlineExtensionStarts !== undefined) {
1181
+ const extension = matchExtensionSyntax(text, i, 'inline', opts);
1182
+ if (extension.status === 'match') {
1183
+ nodes.push(extension.node);
1184
+ i = extension.end;
1185
+ continue;
1186
+ }
1187
+ if (extension.status === 'defer') {
1188
+ break;
1189
+ }
1190
+ }
1191
+
802
1192
  // --- Plain text (with line-break detection) ---
803
1193
  let end = i + 1;
804
- while (end < text.length && !'*_~`[!\\\n\u3010'.includes(text[end]) && !(opts.math && text[end] === '$')) {
1194
+ while (end < text.length && !'*_~`[!\\\n\u3010'.includes(text[end]) && !(opts.math && text[end] === '$') && (inlineExtensionStarts === undefined || !inlineExtensionStarts.has(text[end]))) {
805
1195
  end++;
806
1196
  }
807
1197
  const content = text.slice(i, end);
@@ -813,11 +1203,14 @@ function parseInlineImpl(text, opts) {
813
1203
  if (trimmed.length > 0) {
814
1204
  const last = nodes[nodes.length - 1];
815
1205
  if (last?.type === 'text') {
816
- last.content += trimmed;
1206
+ nodes[nodes.length - 1] = {
1207
+ ...last,
1208
+ value: last.value + trimmed
1209
+ };
817
1210
  } else {
818
1211
  nodes.push({
819
1212
  type: 'text',
820
- content: trimmed
1213
+ value: trimmed
821
1214
  });
822
1215
  }
823
1216
  }
@@ -830,11 +1223,14 @@ function parseInlineImpl(text, opts) {
830
1223
  }
831
1224
  const last = nodes[nodes.length - 1];
832
1225
  if (last?.type === 'text') {
833
- last.content += content;
1226
+ nodes[nodes.length - 1] = {
1227
+ ...last,
1228
+ value: last.value + content
1229
+ };
834
1230
  } else {
835
1231
  nodes.push({
836
1232
  type: 'text',
837
- content
1233
+ value: content
838
1234
  });
839
1235
  }
840
1236
  i = end;
@@ -938,7 +1334,7 @@ function scanAutolinksInText(text) {
938
1334
  while ((m = re.exec(text)) !== null) {
939
1335
  const url = m[1];
940
1336
  // Skip dangerous URL schemes (javascript:, vbscript:, data:text/html)
941
- if (!isSafeUrl(url)) {
1337
+ if (!isSafeMarkdownParserUrl(url)) {
942
1338
  continue;
943
1339
  }
944
1340
  matches.push({
@@ -1063,7 +1459,7 @@ function splitTextOnAutolinks(content) {
1063
1459
  if (matches.length === 0) {
1064
1460
  return [{
1065
1461
  type: 'text',
1066
- content
1462
+ value: content
1067
1463
  }];
1068
1464
  }
1069
1465
  const out = [];
@@ -1072,15 +1468,15 @@ function splitTextOnAutolinks(content) {
1072
1468
  if (m.start > cursor) {
1073
1469
  out.push({
1074
1470
  type: 'text',
1075
- content: content.slice(cursor, m.start)
1471
+ value: content.slice(cursor, m.start)
1076
1472
  });
1077
1473
  }
1078
1474
  out.push({
1079
1475
  type: 'link',
1080
- href: m.href,
1476
+ url: m.href,
1081
1477
  children: [{
1082
1478
  type: 'text',
1083
- content: m.display
1479
+ value: m.display
1084
1480
  }]
1085
1481
  });
1086
1482
  cursor = m.end;
@@ -1088,7 +1484,7 @@ function splitTextOnAutolinks(content) {
1088
1484
  if (cursor < content.length) {
1089
1485
  out.push({
1090
1486
  type: 'text',
1091
- content: content.slice(cursor)
1487
+ value: content.slice(cursor)
1092
1488
  });
1093
1489
  }
1094
1490
  return out;
@@ -1106,16 +1502,19 @@ function transformAutolinks(nodes) {
1106
1502
  const out = [];
1107
1503
  for (const node of nodes) {
1108
1504
  if (node.type === 'text') {
1109
- const split = splitTextOnAutolinks(node.content);
1505
+ const split = splitTextOnAutolinks(node.value);
1110
1506
  for (const seg of split) {
1111
1507
  const last = out[out.length - 1];
1112
1508
  if (seg.type === 'text' && last?.type === 'text') {
1113
- last.content += seg.content;
1509
+ out[out.length - 1] = {
1510
+ ...last,
1511
+ value: last.value + seg.value
1512
+ };
1114
1513
  } else {
1115
1514
  out.push(seg);
1116
1515
  }
1117
1516
  }
1118
- } else if (node.type === 'bold' || node.type === 'italic' || node.type === 'strikethrough') {
1517
+ } else if (node.type === 'strong' || node.type === 'emphasis' || node.type === 'delete') {
1119
1518
  out.push({
1120
1519
  ...node,
1121
1520
  children: transformAutolinks(node.children)
@@ -1200,10 +1599,19 @@ function isTableSeparator(line) {
1200
1599
  * offset into it would not address the document.
1201
1600
  */
1202
1601
  function nested(opts) {
1203
- return opts.sourceRanges ? {
1602
+ const nestedOptions = opts.allowBlockSyntax === false ? opts : {
1204
1603
  ...opts,
1205
- sourceRanges: false
1206
- } : opts;
1604
+ allowBlockSyntax: false
1605
+ };
1606
+ return nestedOptions.sourceRanges || nestedOptions.astPositions ? {
1607
+ ...nestedOptions,
1608
+ sourceRanges: false,
1609
+ astPositions: false
1610
+ } : nestedOptions;
1611
+ }
1612
+ function blockExtensionColumn(line) {
1613
+ const indentation = line.length - line.trimStart().length;
1614
+ return indentation <= 3 ? indentation : null;
1207
1615
  }
1208
1616
 
1209
1617
  /**
@@ -1271,6 +1679,7 @@ function splitTableRow(line) {
1271
1679
  }
1272
1680
  function parseTable(lines, lineIndex, opts) {
1273
1681
  const headers = splitTableRow(lines[lineIndex]).map(cell => ({
1682
+ type: 'tableCell',
1274
1683
  children: parseInlineEntry(cell, opts)
1275
1684
  }));
1276
1685
  const alignments = splitTableRow(lines[lineIndex + 1]).map(cell => {
@@ -1282,17 +1691,24 @@ function parseTable(lines, lineIndex, opts) {
1282
1691
  const rows = [];
1283
1692
  let rowIndex = lineIndex + 2;
1284
1693
  while (rowIndex < lines.length && lines[rowIndex].includes('|') && lines[rowIndex].trim() !== '') {
1285
- rows.push(splitTableRow(lines[rowIndex]).map(cell => ({
1286
- children: parseInlineEntry(cell, opts)
1287
- })));
1694
+ rows.push({
1695
+ type: 'tableRow',
1696
+ children: splitTableRow(lines[rowIndex]).map(cell => ({
1697
+ type: 'tableCell',
1698
+ children: parseInlineEntry(cell, opts)
1699
+ }))
1700
+ });
1288
1701
  rowIndex++;
1289
1702
  }
1703
+ const header = {
1704
+ type: 'tableRow',
1705
+ children: headers
1706
+ };
1290
1707
  return {
1291
1708
  node: {
1292
1709
  type: 'table',
1293
- headers,
1294
- alignments,
1295
- rows
1710
+ align: alignments,
1711
+ children: [header, ...rows]
1296
1712
  },
1297
1713
  nextIndex: rowIndex
1298
1714
  };
@@ -1335,6 +1751,7 @@ function parseList(lines, startIndex, ordered, opts) {
1335
1751
  itemText += '\n' + deindented.join('\n');
1336
1752
  }
1337
1753
  items.push({
1754
+ type: 'listItem',
1338
1755
  checked,
1339
1756
  children: parseMarkdownImpl(itemText, nested(opts))
1340
1757
  });
@@ -1351,15 +1768,16 @@ function parseList(lines, startIndex, ordered, opts) {
1351
1768
  index = lookahead;
1352
1769
  }
1353
1770
  }
1771
+ const node = {
1772
+ type: 'list',
1773
+ ordered,
1774
+ start,
1775
+ delimiter: ordered ? delim : undefined,
1776
+ spread: loose || undefined,
1777
+ children: items
1778
+ };
1354
1779
  return {
1355
- node: {
1356
- type: 'list',
1357
- ordered,
1358
- start,
1359
- delimiter: ordered ? delim : undefined,
1360
- loose: loose || undefined,
1361
- items
1362
- },
1780
+ node,
1363
1781
  nextIndex: index
1364
1782
  };
1365
1783
  }
@@ -1369,7 +1787,21 @@ function parseList(lines, startIndex, ordered, opts) {
1369
1787
  // ---------------------------------------------------------------------------
1370
1788
 
1371
1789
  export function parseMarkdown(input, arg) {
1372
- return parseMarkdownImpl(input, resolveOptions(arg));
1790
+ return projectMarkdownRoot(parseMarkdownAst(input, arg), wantsLegacyRanges(arg));
1791
+ }
1792
+
1793
+ /** @internal Canonical block parse used by Markdown and Outline rendering. */
1794
+ export function parseMarkdownAst(input, arg, isFinal = true) {
1795
+ const resolved = resolveOptions(arg);
1796
+ const opts = isFinal ? resolved : {
1797
+ ...resolved,
1798
+ isFinal: false
1799
+ };
1800
+ const root = {
1801
+ type: 'root',
1802
+ children: parseMarkdownImpl(input, opts)
1803
+ };
1804
+ return applyMarkdownTransforms(root, opts.plugins, input, opts.isFinal, 'block');
1373
1805
  }
1374
1806
  function parseMarkdownImpl(input, baseOpts) {
1375
1807
  // Collect this input's link reference definitions and strip their lines,
@@ -1398,14 +1830,26 @@ function parseMarkdownImpl(input, baseOpts) {
1398
1830
  linkDefs
1399
1831
  } : baseOpts;
1400
1832
  const lines = cleaned.split('\n');
1833
+ const hasBlockExtensionSyntax = opts.allowBlockSyntax !== false && (opts.plugins?.blockByFirstCharacter.size ?? 0) > 0;
1834
+ // Derived from the already-split lines rather than a second character
1835
+ // scan: every line contributes its own length plus the newline it ended on.
1836
+ const lineOffsets = [0];
1837
+ if (opts.astPositions === true || hasBlockExtensionSyntax) {
1838
+ let offset = 0;
1839
+ for (let index = 0; index < lines.length; index++) {
1840
+ offset += lines[index].length + 1;
1841
+ lineOffsets.push(offset);
1842
+ }
1843
+ }
1844
+ const blockExtensionMatches = new Map();
1401
1845
  const blocks = [];
1402
1846
  // The line each block started on, parallel to `blocks`. Only collected when
1403
1847
  // ranges were asked for; a block's end is resolved after the loop, since the
1404
1848
  // branch that produced it has already moved `index` past whatever it read.
1405
- const blockStartLines = opts.sourceRanges ? [] : null;
1849
+ const blockStartLines = opts.astPositions === true ? [] : null;
1406
1850
  // Set only by a block that consumes blank lines as content, where the
1407
1851
  // positional end derivation would trim them away.
1408
- const blockEndLines = opts.sourceRanges ? [] : null;
1852
+ const blockEndLines = opts.astPositions === true ? [] : null;
1409
1853
  let blockStartLine = 0;
1410
1854
  const pushBlock = (node, endLine) => {
1411
1855
  blocks.push(node);
@@ -1422,10 +1866,13 @@ function parseMarkdownImpl(input, baseOpts) {
1422
1866
  }
1423
1867
 
1424
1868
  // --- Fenced code block ---
1425
- const fenceMatch = line.match(/^(`{3,}|~{3,})(\w*)/);
1869
+ const fenceMatch = line.match(/^(`{3,}|~{3,})/);
1426
1870
  if (fenceMatch) {
1427
1871
  const fence = fenceMatch[1];
1428
- const language = fenceMatch[2] || 'plaintext';
1872
+ const info = line.slice(fence.length);
1873
+ const language = info.match(/^(\S+)/)?.[1] ?? null;
1874
+ const legacyLanguage = info.match(/^(\w*)/)?.[1] || null;
1875
+ const meta = language == null ? undefined : info.slice(language.length).trim() || undefined;
1429
1876
  const codeLines = [];
1430
1877
  index++;
1431
1878
  while (index < lines.length && !lines[index].startsWith(fence)) {
@@ -1436,11 +1883,14 @@ function parseMarkdownImpl(input, baseOpts) {
1436
1883
  // A fence owns its blank lines, and an unterminated one (mid-stream)
1437
1884
  // can end on them, so it states its own end rather than letting the
1438
1885
  // positional derivation trim them off.
1439
- pushBlock({
1440
- type: 'codeblock',
1441
- language,
1442
- content: codeLines.join('\n')
1443
- }, Math.min(index, lines.length) - 1);
1886
+ pushBlock(markMarkdownAstLegacyCodeLanguage({
1887
+ type: 'code',
1888
+ lang: language,
1889
+ ...(meta == null ? {} : {
1890
+ meta
1891
+ }),
1892
+ value: codeLines.join('\n')
1893
+ }, legacyLanguage), Math.min(index, lines.length) - 1);
1444
1894
  continue;
1445
1895
  }
1446
1896
 
@@ -1462,7 +1912,7 @@ function parseMarkdownImpl(input, baseOpts) {
1462
1912
  if (headingMatch) {
1463
1913
  pushBlock({
1464
1914
  type: 'heading',
1465
- level: headingMatch[1].length,
1915
+ depth: headingMatch[1].length,
1466
1916
  children: parseInlineEntry(headingMatch[2], opts)
1467
1917
  });
1468
1918
  index++;
@@ -1472,7 +1922,7 @@ function parseMarkdownImpl(input, baseOpts) {
1472
1922
  // --- HR (must precede list check to handle `- - -`, `* * *`, `_ _ _`) ---
1473
1923
  if (isHorizontalRule(line)) {
1474
1924
  pushBlock({
1475
- type: 'hr'
1925
+ type: 'thematicBreak'
1476
1926
  });
1477
1927
  index++;
1478
1928
  continue;
@@ -1482,11 +1932,11 @@ function parseMarkdownImpl(input, baseOpts) {
1482
1932
  // An unsafe src falls through to the paragraph path and renders as
1483
1933
  // literal text, the same rule the inline image path applies.
1484
1934
  const imageMatch = line.match(/^!\[([^\]]*)\]\(([^)]+)\)/);
1485
- if (imageMatch && line.trim() === imageMatch[0] && isSafeUrl(imageMatch[2])) {
1935
+ if (imageMatch && line.trim() === imageMatch[0] && isSafeMarkdownParserUrl(imageMatch[2])) {
1486
1936
  pushBlock({
1487
1937
  type: 'image',
1488
1938
  alt: imageMatch[1],
1489
- src: imageMatch[2]
1939
+ url: imageMatch[2]
1490
1940
  });
1491
1941
  index++;
1492
1942
  continue;
@@ -1530,11 +1980,52 @@ function parseMarkdownImpl(input, baseOpts) {
1530
1980
  continue;
1531
1981
  }
1532
1982
 
1983
+ // --- Extension block syntax (built-in blocks take precedence) ---
1984
+ if (hasBlockExtensionSyntax) {
1985
+ const extensionColumn = blockExtensionColumn(line);
1986
+ const extensionOffset = extensionColumn == null ? lineOffsets[index] : lineOffsets[index] + extensionColumn;
1987
+ const extension = blockExtensionMatches.get(extensionOffset) ?? (extensionColumn == null ? {
1988
+ status: 'none'
1989
+ } : matchExtensionSyntax(cleaned, extensionOffset, 'block', opts));
1990
+ blockExtensionMatches.delete(extensionOffset);
1991
+ if (extension.status === 'match') {
1992
+ const consumedEnd = extension.end;
1993
+ const consumed = cleaned.slice(lineOffsets[index], consumedEnd);
1994
+ if (consumedEnd < cleaned.length && cleaned[consumedEnd] !== '\n' && cleaned[consumedEnd - 1] !== '\n') {
1995
+ reportMarkdownPluginFailure(extension.node.plugin, 'syntax', new TypeError('Block syntax must consume complete lines'));
1996
+ } else {
1997
+ const newlineCount = consumed.split('\n').length - 1;
1998
+ const consumedLines = Math.max(1, newlineCount + (consumed.endsWith('\n') ? 0 : 1));
1999
+ pushBlock(extension.node, index + consumedLines - 1);
2000
+ index += consumedLines;
2001
+ continue;
2002
+ }
2003
+ }
2004
+ if (extension.status === 'defer') {
2005
+ break;
2006
+ }
2007
+ }
2008
+
1533
2009
  // --- Paragraph ---
1534
2010
  const paraLines = [line];
1535
2011
  index++;
1536
- while (index < lines.length && !isBlockStart(lines[index]) && !(opts.math && matchDisplayMathBlock(lines, index) != null) && lines[index].trim() !== '') {
1537
- paraLines.push(lines[index]);
2012
+ while (index < lines.length) {
2013
+ const nextLine = lines[index];
2014
+ if (isBlockStart(nextLine) || opts.math && matchDisplayMathBlock(lines, index) != null || nextLine.trim() === '') {
2015
+ break;
2016
+ }
2017
+ if (hasBlockExtensionSyntax) {
2018
+ const extensionColumn = blockExtensionColumn(nextLine);
2019
+ if (extensionColumn != null) {
2020
+ const extensionOffset = lineOffsets[index] + extensionColumn;
2021
+ const extension = matchExtensionSyntax(cleaned, extensionOffset, 'block', opts);
2022
+ if (extension.status !== 'none') {
2023
+ blockExtensionMatches.set(extensionOffset, extension);
2024
+ break;
2025
+ }
2026
+ }
2027
+ }
2028
+ paraLines.push(nextLine);
1538
2029
  index++;
1539
2030
  }
1540
2031
  pushBlock({
@@ -1586,9 +2077,13 @@ function stampSourceRanges(blocks, blockStartLines, blockEndLines, lines, lineMa
1586
2077
  const end = lineStart(endLine) + lines[endLine].length;
1587
2078
  blocks[i] = {
1588
2079
  ...blocks[i],
1589
- range: {
1590
- start: lineStart(startLine),
1591
- end
2080
+ position: {
2081
+ start: {
2082
+ offset: lineStart(startLine)
2083
+ },
2084
+ end: {
2085
+ offset: end
2086
+ }
1592
2087
  }
1593
2088
  };
1594
2089
  }
@@ -1609,6 +2104,11 @@ function makeIncrementalCache(state) {
1609
2104
  tailLinkDefs: new Map(),
1610
2105
  linkDefs: defs,
1611
2106
  linkDefsKey: linkDefsSignature(defs),
2107
+ settledAstBlocks: [],
2108
+ settledRevision: 0,
2109
+ projectedRevision: -1,
2110
+ projectedSettledBlocks: [],
2111
+ projectionCache: new WeakMap(),
1612
2112
  work: {
1613
2113
  splitCharacters: 0,
1614
2114
  boundaryLines: 0,
@@ -2041,7 +2541,7 @@ function trimUnsettledStructural(text) {
2041
2541
  * rather than into the slice.
2042
2542
  */
2043
2543
  function atOffset(opts, offset) {
2044
- return opts.sourceRanges ? {
2544
+ return opts.astPositions === true ? {
2045
2545
  ...opts,
2046
2546
  baseOffset: offset
2047
2547
  } : opts;
@@ -2066,14 +2566,13 @@ function mergeSettledBlocks(prev, delta) {
2066
2566
  ordered: prevLast.ordered,
2067
2567
  start: prevLast.start,
2068
2568
  delimiter: prevLast.delimiter,
2069
- loose: true,
2070
- items: [...prevLast.items, ...deltaFirst.items],
2071
- // One list now, so one range: from where the first half started to
2072
- // where the second half ended.
2073
- ...(prevLast.range != null && deltaFirst.range != null ? {
2074
- range: {
2075
- start: prevLast.range.start,
2076
- end: deltaFirst.range.end
2569
+ spread: true,
2570
+ children: [...prevLast.children, ...deltaFirst.children],
2571
+ // One list now, so one position spans both halves.
2572
+ ...(prevLast.position != null && deltaFirst.position != null ? {
2573
+ position: {
2574
+ start: prevLast.position.start,
2575
+ end: deltaFirst.position.end
2077
2576
  }
2078
2577
  } : null)
2079
2578
  };
@@ -2085,11 +2584,11 @@ function mergeSettledBlocks(prev, delta) {
2085
2584
  /** Append a newly-settled slice without copying the already-settled prefix. */
2086
2585
  function appendSettledBlocks(prev, delta) {
2087
2586
  if (delta.length === 0) {
2088
- return;
2587
+ return false;
2089
2588
  }
2090
2589
  if (prev.length === 0) {
2091
2590
  prev.push(...delta);
2092
- return;
2591
+ return false;
2093
2592
  }
2094
2593
  const prevLast = prev[prev.length - 1];
2095
2594
  const deltaFirst = delta[0];
@@ -2099,19 +2598,20 @@ function appendSettledBlocks(prev, delta) {
2099
2598
  ordered: prevLast.ordered,
2100
2599
  start: prevLast.start,
2101
2600
  delimiter: prevLast.delimiter,
2102
- loose: true,
2103
- items: [...prevLast.items, ...deltaFirst.items],
2104
- ...(prevLast.range != null && deltaFirst.range != null ? {
2105
- range: {
2106
- start: prevLast.range.start,
2107
- end: deltaFirst.range.end
2601
+ spread: true,
2602
+ children: [...prevLast.children, ...deltaFirst.children],
2603
+ ...(prevLast.position != null && deltaFirst.position != null ? {
2604
+ position: {
2605
+ start: prevLast.position.start,
2606
+ end: deltaFirst.position.end
2108
2607
  }
2109
2608
  } : null)
2110
2609
  };
2111
2610
  prev.push(...delta.slice(1));
2112
- return;
2611
+ return true;
2113
2612
  }
2114
2613
  prev.push(...delta);
2614
+ return false;
2115
2615
  }
2116
2616
  function sameUnsettledDefinitions(previous, next, settled) {
2117
2617
  for (const [label, destination] of previous) {
@@ -2132,12 +2632,19 @@ function resetIncrementalCache(state, cache) {
2132
2632
  state.settledBlocks = [];
2133
2633
  state.settledUpTo = 0;
2134
2634
  state.linkDefsKey = undefined;
2635
+ state.sourceIdsKey = undefined;
2636
+ state.pluginSyntaxIdentity = undefined;
2135
2637
  state.math = undefined;
2136
2638
  cache.settledEnd = 0;
2137
2639
  cache.settledLinkDefs.clear();
2138
2640
  cache.tailLinkDefs = new Map();
2139
2641
  cache.linkDefs = new Map();
2140
2642
  cache.linkDefsKey = '';
2643
+ cache.settledAstBlocks = [];
2644
+ cache.settledRevision++;
2645
+ cache.projectedRevision = -1;
2646
+ cache.projectedSettledBlocks = [];
2647
+ cache.projectionCache = new WeakMap();
2141
2648
  cache.work = {
2142
2649
  splitCharacters: 0,
2143
2650
  boundaryLines: 0,
@@ -2159,24 +2666,47 @@ function resetIncrementalCache(state, cache) {
2159
2666
  */
2160
2667
 
2161
2668
  export function parseMarkdownIncremental(input, state, arg) {
2162
- const opts = resolveOptions(arg);
2669
+ const withRanges = wantsLegacyRanges(arg);
2670
+ const root = parseMarkdownAstIncremental(input, state, arg);
2163
2671
  const cache = incrementalCaches.get(state) ?? makeIncrementalCache(state);
2164
- let reparseSettled = false;
2672
+ const projected = projectMarkdownRoot(root, withRanges, cache.projectionCache);
2673
+ if (cache.projectedRevision !== cache.settledRevision) {
2674
+ cache.projectedSettledBlocks = cache.settledAstBlocks.map(block => projectBlockNode(block, cache.projectionCache, withRanges));
2675
+ cache.projectedRevision = cache.settledRevision;
2676
+ }
2677
+ state.settledBlocks = cache.projectedSettledBlocks;
2678
+ return projected;
2679
+ }
2165
2680
 
2681
+ /** @internal Canonical incremental parse used by Markdown rendering. */
2682
+ export function parseMarkdownAstIncremental(input, state, arg) {
2683
+ const opts = resolveOptions(arg, true);
2684
+ const root = {
2685
+ type: 'root',
2686
+ children: parseMarkdownIncrementalAstBlocks(input, state, opts)
2687
+ };
2688
+ return applyMarkdownTransforms(root, opts.plugins, input, opts.isFinal, 'block');
2689
+ }
2690
+ function parseMarkdownIncrementalAstBlocks(input, state, opts) {
2691
+ const cache = incrementalCaches.get(state) ?? makeIncrementalCache(state);
2692
+ let reparseSettled = false;
2693
+ const nextSourceIdsKey = sourceIdsSignature(opts.sourceIds);
2694
+ const nextPluginSyntaxIdentity = opts.plugins?.syntaxIdentity ?? '';
2166
2695
  // Invalidate cache when an option that changes parsed nodes flips — cached
2167
2696
  // settled blocks were parsed with the previous setting and would otherwise
2168
2697
  // be reused unchanged.
2169
- if (state.autolink !== opts.autolink || state.math !== opts.math || Boolean(state.sourceRanges) !== Boolean(opts.sourceRanges)) {
2698
+ if (state.autolink !== opts.autolink || state.math !== opts.math || Boolean(state.sourceRanges) !== Boolean(opts.sourceRanges) || state.sourceIdsKey !== nextSourceIdsKey || state.pluginSyntaxIdentity !== nextPluginSyntaxIdentity) {
2170
2699
  reparseSettled = true;
2171
2700
  state.autolink = opts.autolink;
2172
2701
  state.math = opts.math;
2173
2702
  state.sourceRanges = opts.sourceRanges;
2703
+ state.sourceIdsKey = nextSourceIdsKey;
2704
+ state.pluginSyntaxIdentity = nextPluginSyntaxIdentity;
2174
2705
  }
2175
2706
  if (input === '') {
2176
2707
  resetIncrementalCache(state, cache);
2177
2708
  return [];
2178
2709
  }
2179
-
2180
2710
  // The settled prefix is only reusable while the input still contains it
2181
2711
  // verbatim. Lengths alone cannot tell: a same-length or longer replacement
2182
2712
  // (new args after reusing a state) disagrees with the prefix without ever
@@ -2190,6 +2720,8 @@ export function parseMarkdownIncremental(input, state, arg) {
2190
2720
  state.autolink = opts.autolink;
2191
2721
  state.math = opts.math;
2192
2722
  state.sourceRanges = opts.sourceRanges;
2723
+ state.sourceIdsKey = nextSourceIdsKey;
2724
+ state.pluginSyntaxIdentity = nextPluginSyntaxIdentity;
2193
2725
  }
2194
2726
 
2195
2727
  // The recurring parse costs are confined to the mutable suffix: splitting,
@@ -2260,22 +2792,38 @@ export function parseMarkdownIncremental(input, state, arg) {
2260
2792
  // table, which inside a fence is ordinary code: a TypeScript union or a `- `
2261
2793
  // would disappear from the code block as it streams.
2262
2794
  const unsettledText = openFence ? unsettledRaw : openMath ? trimOpenDisplayMath(unsettledRaw) : trimUnsettledStructural(unsettledRaw);
2795
+ const legacyRanges = opts.sourceRanges === true;
2263
2796
  let parsedSettledBlocks = 0;
2264
2797
  if (reparseSettled) {
2265
- state.settledBlocks = state.settledText ? parseMarkdownImpl(state.settledText, parseOpts) : [];
2266
- parsedSettledBlocks = state.settledBlocks.length;
2798
+ cache.settledAstBlocks = state.settledText ? parseMarkdownImpl(state.settledText, parseOpts) : [];
2799
+ parsedSettledBlocks = cache.settledAstBlocks.length;
2800
+ cache.settledRevision++;
2801
+ cache.projectedSettledBlocks = cache.settledAstBlocks.map(block => projectBlockNode(block, cache.projectionCache, legacyRanges));
2802
+ cache.projectedRevision = cache.settledRevision;
2267
2803
  } else if (settledDelta !== '') {
2268
2804
  const deltaBlocks = parseMarkdownImpl(settledDelta, atOffset(parseOpts, oldSettledEnd));
2269
- appendSettledBlocks(state.settledBlocks, deltaBlocks);
2805
+ const mergeIndex = cache.settledAstBlocks.length - 1;
2806
+ const mergedList = appendSettledBlocks(cache.settledAstBlocks, deltaBlocks);
2270
2807
  parsedSettledBlocks = deltaBlocks.length;
2808
+ cache.settledRevision++;
2809
+ const projectedDelta = deltaBlocks.map(block => projectBlockNode(block, cache.projectionCache, legacyRanges));
2810
+ if (mergedList) {
2811
+ const canonicalMerged = cache.settledAstBlocks[mergeIndex];
2812
+ const projectedMerged = projectBlockNode(canonicalMerged, cache.projectionCache, legacyRanges);
2813
+ cache.projectedSettledBlocks[mergeIndex] = projectedMerged;
2814
+ cache.projectedSettledBlocks.push(...projectedDelta.slice(1));
2815
+ } else {
2816
+ cache.projectedSettledBlocks.push(...projectedDelta);
2817
+ }
2818
+ cache.projectedRevision = cache.settledRevision;
2271
2819
  }
2272
2820
 
2273
2821
  // The unsettled tail is trimmed before parsing, so its offset in the
2274
2822
  // document is where that trimmed text actually starts — not the boundary,
2275
- // which is a line index. If it somehow can't be located, parse it without
2276
- // ranges rather than report wrong ones. Only worth searching for when
2277
- // ranges were asked for: this runs on every streamed chunk.
2278
- const unsettledStart = unsettledText && opts.sourceRanges ? input.indexOf(unsettledText, cache.settledEnd) : -1;
2823
+ // which is a line index. If it somehow cannot be located, parse it without
2824
+ // positions rather than report wrong ones. The search starts at the
2825
+ // settled end, so it scans the tail and never the prefix.
2826
+ const unsettledStart = unsettledText && opts.astPositions === true ? input.indexOf(unsettledText, cache.settledEnd) : -1;
2279
2827
  const unsettledBlocks = unsettledText ? parseMarkdownImpl(unsettledText, unsettledStart >= 0 ? atOffset(parseOpts, unsettledStart) : nested(parseOpts)) : [];
2280
2828
  state.prevInput = input;
2281
2829
 
@@ -2290,7 +2838,7 @@ export function parseMarkdownIncremental(input, state, arg) {
2290
2838
  definitionCharacters: settledDelta.length + unsettledInput.length,
2291
2839
  renderedBlocks: parsedSettledBlocks + unsettledBlocks.length
2292
2840
  };
2293
- return mergeSettledBlocks(state.settledBlocks, unsettledBlocks);
2841
+ return mergeSettledBlocks(cache.settledAstBlocks, unsettledBlocks);
2294
2842
  }
2295
2843
 
2296
2844
  // ---------------------------------------------------------------------------
@@ -2301,28 +2849,6 @@ export function parseMarkdownIncremental(input, state, arg) {
2301
2849
  // derives its item ids from the same functions, so outline hash links always
2302
2850
  // resolve to a rendered heading by construction.
2303
2851
 
2304
- /** Flatten inline nodes into their plain text content. */
2305
- export function inlineText(nodes) {
2306
- return nodes.map(node => {
2307
- switch (node.type) {
2308
- case 'text':
2309
- case 'code':
2310
- case 'math':
2311
- return node.type === 'math' ? node.value : node.content;
2312
- case 'bold':
2313
- case 'italic':
2314
- case 'strikethrough':
2315
- case 'link':
2316
- return inlineText(node.children);
2317
- case 'image':
2318
- return node.alt;
2319
- case 'citation':
2320
- case 'break':
2321
- return '';
2322
- }
2323
- }).join('');
2324
- }
2325
-
2326
2852
  /** Turn heading text into a URL-safe slug (lowercase, hyphen-separated). */
2327
2853
  export function slugify(value) {
2328
2854
  return value.trim().toLowerCase().replace(/['"]/g, '').replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');