@astryxdesign/core 0.6.1 → 0.6.2-canary.023c3e4

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 (305) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/dist/BottomSheet/BottomSheet.d.ts +1 -1
  3. package/dist/BottomSheet/BottomSheet.d.ts.map +1 -1
  4. package/dist/BottomSheet/BottomSheet.js +3 -1
  5. package/dist/BottomSheet/BottomSheetPanel.d.ts +7 -5
  6. package/dist/BottomSheet/BottomSheetPanel.d.ts.map +1 -1
  7. package/dist/BottomSheet/BottomSheetPanel.js +56 -19
  8. package/dist/Chat/ChatComposerInput.d.ts.map +1 -1
  9. package/dist/Chat/ChatComposerInput.js +2 -2
  10. package/dist/CheckboxInput/CheckboxInput.d.ts.map +1 -1
  11. package/dist/CheckboxInput/CheckboxInput.js +12 -2
  12. package/dist/Collapsible/Collapsible.d.ts.map +1 -1
  13. package/dist/Collapsible/Collapsible.js +6 -1
  14. package/dist/CommandPalette/CommandPaletteInput.d.ts.map +1 -1
  15. package/dist/CommandPalette/CommandPaletteInput.js +1 -1
  16. package/dist/ComplexSelector/ComplexSelector.d.ts.map +1 -1
  17. package/dist/ComplexSelector/ComplexSelector.js +1 -1
  18. package/dist/DateInput/DateInput.d.ts.map +1 -1
  19. package/dist/DateInput/DateInput.js +4 -4
  20. package/dist/DateInput/NativeDateField.d.ts.map +1 -1
  21. package/dist/DateInput/NativeDateField.js +16 -16
  22. package/dist/DateInput/TouchDateField.d.ts.map +1 -1
  23. package/dist/DateInput/TouchDateField.js +2 -2
  24. package/dist/DateRangeInput/DateRangeInput.d.ts +3 -0
  25. package/dist/DateRangeInput/DateRangeInput.d.ts.map +1 -1
  26. package/dist/DateRangeInput/DateRangeInput.js +20 -13
  27. package/dist/DateTimeInput/DateTimeInput.d.ts.map +1 -1
  28. package/dist/DateTimeInput/DateTimeInput.js +8 -8
  29. package/dist/DateTimeInput/TouchDateTimeField.d.ts.map +1 -1
  30. package/dist/DateTimeInput/TouchDateTimeField.js +4 -4
  31. package/dist/DateTimeInput/nativePickerSegmentStyles.d.ts +9 -3
  32. package/dist/DateTimeInput/nativePickerSegmentStyles.d.ts.map +1 -1
  33. package/dist/DateTimeInput/nativePickerSegmentStyles.js +3 -3
  34. package/dist/Dialog/DialogHeader.d.ts +1 -1
  35. package/dist/Dialog/DialogHeader.d.ts.map +1 -1
  36. package/dist/Dialog/DialogHeader.js +10 -7
  37. package/dist/Field/PanelSearchInput.d.ts.map +1 -1
  38. package/dist/Field/PanelSearchInput.js +1 -1
  39. package/dist/FileInput/FileInput.d.ts.map +1 -1
  40. package/dist/FileInput/FileInput.js +16 -10
  41. package/dist/Kbd/Kbd.d.ts +5 -3
  42. package/dist/Kbd/Kbd.d.ts.map +1 -1
  43. package/dist/Kbd/Kbd.js +36 -42
  44. package/dist/Link/Link.d.ts.map +1 -1
  45. package/dist/Link/Link.js +6 -2
  46. package/dist/Markdown/Markdown.d.ts +18 -4
  47. package/dist/Markdown/Markdown.d.ts.map +1 -1
  48. package/dist/Markdown/Markdown.fr23.bench.d.ts +2 -0
  49. package/dist/Markdown/Markdown.fr23.bench.d.ts.map +1 -0
  50. package/dist/Markdown/Markdown.fr23.bench.js +150 -0
  51. package/dist/Markdown/Markdown.fr23.sampling.d.ts +2 -0
  52. package/dist/Markdown/Markdown.fr23.sampling.d.ts.map +1 -0
  53. package/dist/Markdown/Markdown.fr23.sampling.js +43 -0
  54. package/dist/Markdown/Markdown.js +311 -114
  55. package/dist/Markdown/ast.d.ts +162 -0
  56. package/dist/Markdown/ast.d.ts.map +1 -0
  57. package/dist/Markdown/ast.js +74 -0
  58. package/dist/Markdown/index.d.ts +5 -3
  59. package/dist/Markdown/index.d.ts.map +1 -1
  60. package/dist/Markdown/index.js +3 -2
  61. package/dist/Markdown/parser/index.d.ts +10 -0
  62. package/dist/Markdown/parser/index.d.ts.map +1 -0
  63. package/dist/Markdown/parser/index.js +10 -0
  64. package/dist/Markdown/parser.d.ts +220 -38
  65. package/dist/Markdown/parser.d.ts.map +1 -1
  66. package/dist/Markdown/parser.js +1072 -188
  67. package/dist/Markdown/plugins/frontmatter.d.ts +32 -0
  68. package/dist/Markdown/plugins/frontmatter.d.ts.map +1 -0
  69. package/dist/Markdown/plugins/frontmatter.js +132 -0
  70. package/dist/Markdown/plugins/index.d.ts +16 -0
  71. package/dist/Markdown/plugins/index.d.ts.map +1 -0
  72. package/dist/Markdown/plugins/index.js +13 -0
  73. package/dist/Markdown/plugins/protocol.d.ts +191 -0
  74. package/dist/Markdown/plugins/protocol.d.ts.map +1 -0
  75. package/dist/Markdown/plugins/protocol.js +957 -0
  76. package/dist/Markdown/plugins/semanticFence.d.ts +36 -0
  77. package/dist/Markdown/plugins/semanticFence.d.ts.map +1 -0
  78. package/dist/Markdown/plugins/semanticFence.js +213 -0
  79. package/dist/Markdown/plugins/sourceDecoration.d.ts +76 -0
  80. package/dist/Markdown/plugins/sourceDecoration.d.ts.map +1 -0
  81. package/dist/Markdown/plugins/sourceDecoration.js +393 -0
  82. package/dist/Markdown/plugins/textTransform.d.ts +30 -0
  83. package/dist/Markdown/plugins/textTransform.d.ts.map +1 -0
  84. package/dist/Markdown/plugins/textTransform.js +374 -0
  85. package/dist/Markdown/remark.d.ts +196 -0
  86. package/dist/Markdown/remark.d.ts.map +1 -0
  87. package/dist/Markdown/remark.js +1062 -0
  88. package/dist/Markdown/url.d.ts +4 -0
  89. package/dist/Markdown/url.d.ts.map +1 -0
  90. package/dist/Markdown/url.js +25 -0
  91. package/dist/Markdown/utils.d.ts +4 -2
  92. package/dist/Markdown/utils.d.ts.map +1 -1
  93. package/dist/Markdown/utils.js +2 -1
  94. package/dist/NumberInput/NumberInput.d.ts.map +1 -1
  95. package/dist/NumberInput/NumberInput.js +4 -4
  96. package/dist/Outline/index.d.ts +1 -0
  97. package/dist/Outline/index.d.ts.map +1 -1
  98. package/dist/Outline/parseOutlineFromMarkdown.d.ts +7 -1
  99. package/dist/Outline/parseOutlineFromMarkdown.d.ts.map +1 -1
  100. package/dist/Outline/parseOutlineFromMarkdown.js +14 -8
  101. package/dist/Outline/useOutlineFromMarkdown.d.ts +3 -1
  102. package/dist/Outline/useOutlineFromMarkdown.d.ts.map +1 -1
  103. package/dist/Outline/useOutlineFromMarkdown.js +8 -3
  104. package/dist/PowerSearch/PowerSearchEditPopover.d.ts.map +1 -1
  105. package/dist/PowerSearch/PowerSearchEditPopover.js +46 -30
  106. package/dist/ProgressBar/ProgressBar.d.ts.map +1 -1
  107. package/dist/ProgressBar/ProgressBar.js +36 -31
  108. package/dist/RadioList/RadioListItem.d.ts.map +1 -1
  109. package/dist/RadioList/RadioListItem.js +13 -1
  110. package/dist/SegmentedControl/SegmentedControlItem.d.ts.map +1 -1
  111. package/dist/SegmentedControl/SegmentedControlItem.js +5 -5
  112. package/dist/Selector/Selector.d.ts.map +1 -1
  113. package/dist/Selector/Selector.js +1 -1
  114. package/dist/SideNav/SideNav.d.ts +2 -1
  115. package/dist/SideNav/SideNav.d.ts.map +1 -1
  116. package/dist/SideNav/SideNav.js +9 -3
  117. package/dist/Slider/Slider.d.ts.map +1 -1
  118. package/dist/Slider/Slider.js +19 -10
  119. package/dist/Spinner/Spinner.d.ts +1 -1
  120. package/dist/Spinner/Spinner.d.ts.map +1 -1
  121. package/dist/Spinner/Spinner.js +45 -21
  122. package/dist/Switch/Switch.d.ts.map +1 -1
  123. package/dist/Switch/Switch.js +11 -0
  124. package/dist/TabList/Tab.d.ts +1 -1
  125. package/dist/TabList/Tab.d.ts.map +1 -1
  126. package/dist/TabList/Tab.js +20 -7
  127. package/dist/TextArea/TextArea.d.ts.map +1 -1
  128. package/dist/TextArea/TextArea.js +1 -1
  129. package/dist/TextInput/TextInput.d.ts.map +1 -1
  130. package/dist/TextInput/TextInput.js +2 -2
  131. package/dist/TimeInput/TimeInput.d.ts.map +1 -1
  132. package/dist/TimeInput/TimeInput.js +4 -4
  133. package/dist/ToggleButton/ToggleButton.d.ts +2 -1
  134. package/dist/ToggleButton/ToggleButton.d.ts.map +1 -1
  135. package/dist/ToggleButton/ToggleButton.js +7 -1
  136. package/dist/Typeahead/BaseTypeahead.d.ts.map +1 -1
  137. package/dist/Typeahead/BaseTypeahead.js +16 -7
  138. package/dist/astryx.css +19 -8
  139. package/dist/hooks/scrollKeyboardDelegation.d.ts +3 -0
  140. package/dist/hooks/scrollKeyboardDelegation.d.ts.map +1 -0
  141. package/dist/hooks/scrollKeyboardDelegation.js +146 -0
  142. package/dist/hooks/useScrollableArea.d.ts +6 -2
  143. package/dist/hooks/useScrollableArea.d.ts.map +1 -1
  144. package/dist/hooks/useScrollableArea.js +17 -5
  145. package/dist/utils/interactionOverlay.stylex.d.ts +8 -0
  146. package/dist/utils/interactionOverlay.stylex.d.ts.map +1 -1
  147. package/dist/utils/interactionOverlay.stylex.js +9 -0
  148. package/locales/en.json +16 -0
  149. package/locales/pseudo.json +12 -0
  150. package/package.json +22 -5
  151. package/scripts/agent-doc-state.mjs +1 -1
  152. package/src/BottomSheet/BottomSheet.doc.mjs +8 -1
  153. package/src/BottomSheet/BottomSheet.spec.md +46 -20
  154. package/src/BottomSheet/BottomSheet.test.tsx +6 -3
  155. package/src/BottomSheet/BottomSheet.tsx +3 -1
  156. package/src/BottomSheet/BottomSheetKeyboard.test.tsx +195 -0
  157. package/src/BottomSheet/BottomSheetPanel.test.tsx +11 -1
  158. package/src/BottomSheet/BottomSheetPanel.tsx +49 -16
  159. package/src/BottomSheet/__tests__/BottomSheetKeyboard.a11y.browser.spec.ts +344 -0
  160. package/src/Button/__tests__/Button.a11y.chromium.spec.ts +17 -1
  161. package/src/Button/__tests__/Button.a11y.known-failures.ts +0 -29
  162. package/src/Button/__tests__/Button.a11y.renders.tsx +9 -2
  163. package/src/Button/__tests__/Button.a11y.states.ts +9 -0
  164. package/src/Chat/ChatComposerInput.doc.mjs +3 -3
  165. package/src/Chat/ChatComposerInput.tsx +10 -2
  166. package/src/CheckboxInput/CheckboxInput.doc.mjs +11 -0
  167. package/src/CheckboxInput/CheckboxInput.test.tsx +34 -0
  168. package/src/CheckboxInput/CheckboxInput.tsx +21 -1
  169. package/src/ClickableCard/ClickableCard.test.tsx +102 -5
  170. package/src/Collapsible/Collapsible.doc.mjs +11 -0
  171. package/src/Collapsible/Collapsible.test.tsx +21 -0
  172. package/src/Collapsible/Collapsible.tsx +5 -0
  173. package/src/CommandPalette/CommandPaletteInput.tsx +6 -1
  174. package/src/ComplexSelector/ComplexSelector.tsx +6 -1
  175. package/src/DateInput/DateInput.tsx +6 -1
  176. package/src/DateInput/NativeDateField.tsx +24 -8
  177. package/src/DateInput/TouchDateField.tsx +6 -2
  178. package/src/DateRangeInput/DateRangeInput.doc.mjs +35 -7
  179. package/src/DateRangeInput/DateRangeInput.spec.md +203 -0
  180. package/src/DateRangeInput/DateRangeInput.test.tsx +100 -4
  181. package/src/DateRangeInput/DateRangeInput.tsx +35 -21
  182. package/src/DateTimeInput/DateTimeInput.tsx +6 -1
  183. package/src/DateTimeInput/TouchDateTimeField.tsx +6 -1
  184. package/src/DateTimeInput/nativePickerSegmentStyles.ts +24 -8
  185. package/src/Dialog/Dialog.doc.mjs +3 -0
  186. package/src/Dialog/Dialog.spec.md +1 -1
  187. package/src/Dialog/DialogHeader.doc.mjs +38 -0
  188. package/src/Dialog/DialogHeader.test.tsx +49 -0
  189. package/src/Dialog/DialogHeader.tsx +23 -4
  190. package/src/Dialog/modules/DialogHeader.spec.md +152 -0
  191. package/src/Field/PanelSearchInput.tsx +5 -2
  192. package/src/FieldStatus/__tests__/StatusMessage.a11y.states.ts +1 -1
  193. package/src/FileInput/FileInput.doc.mjs +2 -0
  194. package/src/FileInput/FileInput.spec.md +199 -0
  195. package/src/FileInput/FileInput.test.tsx +14 -0
  196. package/src/FileInput/FileInput.tsx +24 -5
  197. package/src/Kbd/Kbd.doc.mjs +3 -3
  198. package/src/Kbd/Kbd.test.tsx +57 -1
  199. package/src/Kbd/Kbd.tsx +57 -37
  200. package/src/Layout/LayoutFooter.doc.mjs +17 -0
  201. package/src/Link/Link.doc.mjs +11 -0
  202. package/src/Link/Link.test.tsx +24 -0
  203. package/src/Link/Link.tsx +5 -0
  204. package/src/Markdown/Markdown.doc.mjs +439 -42
  205. package/src/Markdown/Markdown.fr23.bench.ts +174 -0
  206. package/src/Markdown/Markdown.fr23.perf.test.ts +249 -0
  207. package/src/Markdown/Markdown.fr23.sampling.ts +49 -0
  208. package/src/Markdown/Markdown.helpers.perf.test.ts +489 -0
  209. package/src/Markdown/Markdown.public.test.ts +280 -0
  210. package/src/Markdown/Markdown.spec.md +304 -72
  211. package/src/Markdown/Markdown.test.tsx +118 -3
  212. package/src/Markdown/Markdown.tsx +489 -129
  213. package/src/Markdown/ast.test.ts +307 -0
  214. package/src/Markdown/ast.ts +337 -0
  215. package/src/Markdown/incremental.test.ts +187 -7
  216. package/src/Markdown/index.ts +43 -1
  217. package/src/Markdown/modules/remark.spec.md +269 -0
  218. package/src/Markdown/parser/index.ts +64 -0
  219. package/src/Markdown/parser.perf.test.ts +3 -1
  220. package/src/Markdown/parser.public.test.ts +103 -0
  221. package/src/Markdown/parser.test.ts +122 -0
  222. package/src/Markdown/parser.ts +1721 -248
  223. package/src/Markdown/plugins/frontmatter.test.tsx +125 -0
  224. package/src/Markdown/plugins/frontmatter.ts +198 -0
  225. package/src/Markdown/plugins/index.ts +53 -0
  226. package/src/Markdown/plugins/protocol.test.tsx +1022 -0
  227. package/src/Markdown/plugins/protocol.ts +1618 -0
  228. package/src/Markdown/plugins/semanticFence.test.tsx +546 -0
  229. package/src/Markdown/plugins/semanticFence.ts +424 -0
  230. package/src/Markdown/plugins/sourceDecoration.test.tsx +799 -0
  231. package/src/Markdown/plugins/sourceDecoration.ts +539 -0
  232. package/src/Markdown/plugins/textTransform.test.ts +467 -0
  233. package/src/Markdown/plugins/textTransform.ts +554 -0
  234. package/src/Markdown/remark.test.tsx +1877 -0
  235. package/src/Markdown/remark.ts +1574 -0
  236. package/src/Markdown/url.ts +30 -0
  237. package/src/Markdown/utils.ts +19 -1
  238. package/src/MultiSelector/MultiSelector.test.tsx +1 -35
  239. package/src/MultiSelector/__tests__/MultiSelector.listbox.a11y.test.tsx +128 -0
  240. package/src/NumberInput/NumberInput.tsx +6 -1
  241. package/src/Outline/Outline.doc.mjs +7 -5
  242. package/src/Outline/Outline.spec.md +1 -1
  243. package/src/Outline/index.ts +1 -0
  244. package/src/Outline/modules/parseOutlineFromMarkdown.spec.md +142 -0
  245. package/src/Outline/parseOutlineFromMarkdown.ts +40 -10
  246. package/src/Outline/useOutlineFromMarkdown.ts +15 -3
  247. package/src/PowerSearch/PowerSearch.doc.mjs +2 -2
  248. package/src/PowerSearch/PowerSearchEditPopover.test.tsx +150 -1
  249. package/src/PowerSearch/PowerSearchEditPopover.tsx +51 -28
  250. package/src/ProgressBar/ProgressBar.test.tsx +29 -11
  251. package/src/ProgressBar/ProgressBar.tsx +59 -72
  252. package/src/RadioList/RadioList.doc.mjs +11 -0
  253. package/src/RadioList/RadioList.test.tsx +32 -0
  254. package/src/RadioList/RadioListItem.tsx +25 -1
  255. package/src/ScrollableArea/modules/useScrollableArea.spec.md +50 -22
  256. package/src/SegmentedControl/SegmentedControl.doc.mjs +2 -2
  257. package/src/SegmentedControl/SegmentedControl.test.tsx +31 -0
  258. package/src/SegmentedControl/SegmentedControlItem.tsx +6 -9
  259. package/src/Selector/Selector.spec.md +12 -9
  260. package/src/Selector/Selector.tsx +6 -1
  261. package/src/Selector/__tests__/Listbox.a11y.chromium.spec.ts +130 -0
  262. package/src/Selector/__tests__/Listbox.a11y.dom.ts +38 -0
  263. package/src/Selector/__tests__/Listbox.a11y.inventory.test.ts +63 -0
  264. package/src/Selector/__tests__/Listbox.a11y.known-failures.ts +37 -0
  265. package/src/Selector/__tests__/Listbox.a11y.states.ts +232 -0
  266. package/src/Selector/__tests__/Selector.listbox.a11y.test.tsx +121 -0
  267. package/src/SideNav/SideNav.doc.mjs +1 -1
  268. package/src/SideNav/SideNav.test.tsx +10 -0
  269. package/src/SideNav/SideNav.tsx +14 -2
  270. package/src/Slider/Slider.doc.mjs +27 -0
  271. package/src/Slider/Slider.spec.md +61 -47
  272. package/src/Slider/Slider.test.tsx +146 -0
  273. package/src/Slider/Slider.tsx +37 -13
  274. package/src/Spinner/Spinner.doc.mjs +6 -3
  275. package/src/Spinner/Spinner.test.tsx +41 -0
  276. package/src/Spinner/Spinner.tsx +62 -32
  277. package/src/Switch/Switch.doc.mjs +11 -0
  278. package/src/Switch/Switch.test.tsx +16 -0
  279. package/src/Switch/Switch.tsx +28 -0
  280. package/src/TabList/Tab.tsx +35 -7
  281. package/src/TabList/TabList.doc.mjs +11 -0
  282. package/src/TabList/TabList.test.tsx +66 -0
  283. package/src/TabList/__tests__/Tabs.a11y.known-failures.ts +1 -34
  284. package/src/TextArea/TextArea.tsx +6 -1
  285. package/src/TextInput/TextInput.test.tsx +20 -1
  286. package/src/TextInput/TextInput.tsx +6 -1
  287. package/src/TimeInput/TimeInput.tsx +6 -1
  288. package/src/ToggleButton/ToggleButton.test.tsx +133 -0
  289. package/src/ToggleButton/ToggleButton.tsx +9 -2
  290. package/src/ToggleButton/__tests__/ToggleButton.a11y.chromium.spec.ts +209 -0
  291. package/src/Tokenizer/Tokenizer.spec.md +142 -75
  292. package/src/TreeList/TreeList.spec.md +123 -75
  293. package/src/Typeahead/BaseTypeahead.spec.md +4 -3
  294. package/src/Typeahead/BaseTypeahead.tsx +21 -7
  295. package/src/Typeahead/Typeahead.test.tsx +53 -0
  296. package/src/__tests__/PressedState.a11y.chromium.spec.ts +813 -0
  297. package/src/__tests__/pressState.ts +93 -0
  298. package/src/hooks/scrollKeyboardDelegation.test.ts +155 -0
  299. package/src/hooks/scrollKeyboardDelegation.ts +233 -0
  300. package/src/hooks/useScrollableArea.doc.mjs +15 -3
  301. package/src/hooks/useScrollableArea.test.tsx +59 -1
  302. package/src/hooks/useScrollableArea.ts +34 -10
  303. package/src/inputFontFloor.test.ts +106 -0
  304. package/src/theme/derivedVarRegistry.test.ts +6 -4
  305. package/src/utils/interactionOverlay.stylex.ts +20 -0
@@ -2,17 +2,33 @@
2
2
 
3
3
  /**
4
4
  * @file parser.ts
5
- * @input Markdown string
6
- * @output Array of MarkdownNode AST nodes; heading slug helpers
7
- * (inlineText, slugify, uniqueSlug) shared by Markdown rendering and
8
- * Outline's parseOutlineFromMarkdown
9
- * @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 public/server consumers plus unchanged
7
+ * released parser-node projections; shared heading slug helpers
8
+ * @position Core parser and compatibility boundary; consumed by public parser entry,
9
+ * Markdown, and Outline
10
10
  */
11
11
 
12
+ import { getMarkdownAstLegacyCodeLanguage, markMarkdownAstLegacyCodeLanguage } from "./ast.js";
13
+ import { applyMarkdownTransforms, freezeMarkdownPluginData, isMarkdownPluginData, prepareMarkdownPlugins, reportMarkdownPluginFailure } from "./plugins/protocol.js";
14
+ import { isSafeMarkdownParserUrl } from "./url.js";
15
+
12
16
  // ---------------------------------------------------------------------------
13
17
  // Types
14
18
  // ---------------------------------------------------------------------------
15
19
 
20
+ /** Nodes returned by default and legacy parser calls. */
21
+
22
+ /** The additional inline node returned only when parsing with `math: true`. */
23
+
24
+ /** Nodes returned by an explicitly math-enabled inline parse. */
25
+
26
+ /** Blocks returned by default and legacy parser calls. */
27
+
28
+ /** The additional block returned only when parsing with `math: true`. */
29
+
30
+ /** Blocks returned by an explicitly math-enabled block parse. */
31
+
16
32
  /**
17
33
  * Where a block sits in the source string handed to `parseMarkdown`:
18
34
  * `source.slice(start, end)` is the block, and `end` excludes the block's
@@ -23,6 +39,240 @@
23
39
  * optional fields without breaking anyone.
24
40
  */
25
41
 
42
+ function projectRange(position) {
43
+ const start = position?.start.offset;
44
+ const end = position?.end.offset;
45
+ return start == null || end == null ? {} : {
46
+ range: {
47
+ start,
48
+ end
49
+ }
50
+ };
51
+ }
52
+
53
+ /**
54
+ * Canonical nodes always carry Core-authored positions; the released block
55
+ * shape exposes them as `range` only when the caller passed `sourceRanges`,
56
+ * so this projection stays byte-for-byte what it has always returned.
57
+ *
58
+ * An extension node is the one kind the projection passes through instead of
59
+ * rebuilding field by field, so it is the one kind that could carry a
60
+ * canonical `position` out into released output. Strip it unless the caller
61
+ * asked for provenance, in which case the node keeps exactly the `position`
62
+ * it has always exposed.
63
+ */
64
+ function projectExtensionNode(node, withRanges) {
65
+ if (withRanges || node.position === undefined) {
66
+ return node;
67
+ }
68
+ const {
69
+ position: _canonicalPosition,
70
+ ...released
71
+ } = node;
72
+ return released;
73
+ }
74
+ function projectInlineNode(node, cache, withRanges) {
75
+ const cached = cache.get(node);
76
+ if (cached != null) {
77
+ return cached;
78
+ }
79
+ let projected;
80
+ switch (node.type) {
81
+ case 'text':
82
+ projected = {
83
+ type: 'text',
84
+ content: node.value
85
+ };
86
+ break;
87
+ case 'strong':
88
+ projected = {
89
+ type: 'bold',
90
+ children: node.children.map(child => projectInlineNode(child, cache, withRanges))
91
+ };
92
+ break;
93
+ case 'emphasis':
94
+ projected = {
95
+ type: 'italic',
96
+ children: node.children.map(child => projectInlineNode(child, cache, withRanges))
97
+ };
98
+ break;
99
+ case 'delete':
100
+ projected = {
101
+ type: 'strikethrough',
102
+ children: node.children.map(child => projectInlineNode(child, cache, withRanges))
103
+ };
104
+ break;
105
+ case 'inlineCode':
106
+ projected = {
107
+ type: 'code',
108
+ content: node.value
109
+ };
110
+ break;
111
+ case 'inlineMath':
112
+ projected = {
113
+ type: 'math',
114
+ value: node.value
115
+ };
116
+ break;
117
+ case 'link':
118
+ projected = {
119
+ type: 'link',
120
+ href: node.url,
121
+ children: node.children.map(child => projectInlineNode(child, cache, withRanges))
122
+ };
123
+ break;
124
+ case 'image':
125
+ projected = {
126
+ type: 'image',
127
+ src: node.url,
128
+ alt: node.alt
129
+ };
130
+ break;
131
+ case 'citation':
132
+ projected = {
133
+ type: 'citation',
134
+ sourceId: node.sourceId
135
+ };
136
+ break;
137
+ case 'break':
138
+ projected = {
139
+ type: 'break'
140
+ };
141
+ break;
142
+ case 'extension':
143
+ projected = projectExtensionNode(node, withRanges);
144
+ break;
145
+ }
146
+ cache.set(node, projected);
147
+ return projected;
148
+ }
149
+ function projectTableCell(node, cache, withRanges) {
150
+ const cached = cache.get(node);
151
+ if (cached != null) {
152
+ return cached;
153
+ }
154
+ const projected = {
155
+ children: node.children.map(child => projectInlineNode(child, cache, withRanges))
156
+ };
157
+ cache.set(node, projected);
158
+ return projected;
159
+ }
160
+ function projectListItem(node, cache, withRanges) {
161
+ const cached = cache.get(node);
162
+ if (cached != null) {
163
+ return cached;
164
+ }
165
+ const projected = {
166
+ checked: node.checked,
167
+ children: node.children.map(child => projectBlockNode(child, cache, withRanges))
168
+ };
169
+ cache.set(node, projected);
170
+ return projected;
171
+ }
172
+ function projectBlockNode(node, cache, withRanges) {
173
+ const cached = cache.get(node);
174
+ if (cached != null) {
175
+ return cached;
176
+ }
177
+ const metadata = withRanges ? projectRange(node.position) : {};
178
+ let projected;
179
+ switch (node.type) {
180
+ case 'heading':
181
+ projected = {
182
+ type: 'heading',
183
+ level: node.depth,
184
+ children: node.children.map(child => projectInlineNode(child, cache, withRanges)),
185
+ ...metadata
186
+ };
187
+ break;
188
+ case 'paragraph':
189
+ projected = {
190
+ type: 'paragraph',
191
+ children: node.children.map(child => projectInlineNode(child, cache, withRanges)),
192
+ ...metadata
193
+ };
194
+ break;
195
+ case 'code':
196
+ projected = {
197
+ type: 'codeblock',
198
+ language: getMarkdownAstLegacyCodeLanguage(node) ?? 'plaintext',
199
+ content: node.value,
200
+ ...metadata
201
+ };
202
+ break;
203
+ case 'math':
204
+ projected = {
205
+ type: 'math',
206
+ value: node.value,
207
+ ...metadata
208
+ };
209
+ break;
210
+ case 'blockquote':
211
+ projected = {
212
+ type: 'blockquote',
213
+ children: node.children.map(child => projectBlockNode(child, cache, withRanges)),
214
+ ...metadata
215
+ };
216
+ break;
217
+ case 'list':
218
+ projected = {
219
+ type: 'list',
220
+ ordered: node.ordered,
221
+ start: node.start,
222
+ delimiter: node.delimiter,
223
+ loose: node.spread,
224
+ items: node.children.map(item => projectListItem(item, cache, withRanges)),
225
+ ...metadata
226
+ };
227
+ break;
228
+ case 'table':
229
+ {
230
+ const [header = {
231
+ type: 'tableRow',
232
+ children: []
233
+ }, ...rows] = node.children;
234
+ projected = {
235
+ type: 'table',
236
+ headers: header.children.map(cell => projectTableCell(cell, cache, withRanges)),
237
+ alignments: [...node.align],
238
+ rows: rows.map(row => row.children.map(cell => projectTableCell(cell, cache, withRanges))),
239
+ ...metadata
240
+ };
241
+ break;
242
+ }
243
+ case 'thematicBreak':
244
+ projected = {
245
+ type: 'hr',
246
+ ...metadata
247
+ };
248
+ break;
249
+ case 'image':
250
+ projected = {
251
+ type: 'image',
252
+ alt: node.alt,
253
+ src: node.url,
254
+ ...metadata
255
+ };
256
+ break;
257
+ case 'extension':
258
+ projected = projectExtensionNode(node, withRanges);
259
+ break;
260
+ }
261
+ cache.set(node, projected);
262
+ return projected;
263
+ }
264
+ function projectInlineNodes(nodes, withRanges, cache = new WeakMap()) {
265
+ return nodes.map(node => projectInlineNode(node, cache, withRanges));
266
+ }
267
+ function projectMarkdownRoot(root, withRanges, cache = new WeakMap()) {
268
+ return root.children.map(node => projectBlockNode(node, cache, withRanges));
269
+ }
270
+
271
+ /** Whether a caller asked for the released `range` field on its blocks. */
272
+ function wantsLegacyRanges(arg) {
273
+ return arg != null && typeof arg.has !== 'function' && arg.sourceRanges === true;
274
+ }
275
+
26
276
  // ---------------------------------------------------------------------------
27
277
  // Parse options
28
278
  // ---------------------------------------------------------------------------
@@ -35,13 +285,45 @@
35
285
  * `ReadonlySet<string>` shape as the second argument.
36
286
  */
37
287
 
38
- const EMPTY_OPTS = {
39
- sourceIds: undefined,
40
- autolink: undefined
41
- };
42
- function resolveOptions(arg) {
288
+ /** Options for default and legacy parser results. */
289
+
290
+ /**
291
+ * Options that explicitly parse `$…$` and `$$…$$` into math-enabled result
292
+ * unions. Keeping this separate prevents a legacy `ParseOptions` annotation
293
+ * from silently widening an exhaustive node switch.
294
+ */
295
+
296
+ /**
297
+ * Every resolved options object is built here, so all of them share one key
298
+ * set in one order — and therefore one hidden class. The parser reads these
299
+ * fields in its hottest loops; letting an omitted-options parse and an
300
+ * options-bag parse produce differently-shaped objects makes those reads
301
+ * polymorphic in any process that does both, which is exactly what a caller
302
+ * comparing an empty pipeline against a configured one does.
303
+ */
304
+ function makeResolvedOptions(fields) {
305
+ return {
306
+ sourceIds: fields.sourceIds,
307
+ autolink: fields.autolink,
308
+ math: fields.math,
309
+ sourceRanges: fields.sourceRanges,
310
+ astPositions: fields.astPositions ?? true,
311
+ plugins: fields.plugins,
312
+ isFinal: fields.isFinal,
313
+ allowBlockSyntax: fields.allowBlockSyntax ?? true,
314
+ baseOffset: fields.baseOffset,
315
+ linkDefs: fields.linkDefs
316
+ };
317
+ }
318
+ const EMPTY_OPTS = makeResolvedOptions({
319
+ isFinal: true
320
+ });
321
+ const EMPTY_INCREMENTAL_OPTS = makeResolvedOptions({
322
+ isFinal: false
323
+ });
324
+ function resolveOptions(arg, incremental = false) {
43
325
  if (arg == null) {
44
- return EMPTY_OPTS;
326
+ return incremental ? EMPTY_INCREMENTAL_OPTS : EMPTY_OPTS;
45
327
  }
46
328
  // Duck-type the legacy `ReadonlySet<string>` form: any object whose
47
329
  // `.has` is callable is treated as the legacy sourceIds set. This is
@@ -49,17 +331,20 @@ function resolveOptions(arg) {
49
331
  // or polyfilled `ReadonlySet` implementations as a `ParseOptions` bag
50
332
  // and silently lose citation resolution.
51
333
  if (typeof arg.has === 'function') {
52
- return {
334
+ return makeResolvedOptions({
53
335
  sourceIds: arg,
54
- autolink: undefined
55
- };
336
+ isFinal: !incremental
337
+ });
56
338
  }
57
339
  const opts = arg;
58
- return {
340
+ return makeResolvedOptions({
59
341
  sourceIds: opts.sourceIds,
60
342
  autolink: opts.autolink,
61
- sourceRanges: opts.sourceRanges
62
- };
343
+ math: opts.math === true ? true : undefined,
344
+ sourceRanges: opts.sourceRanges,
345
+ plugins: opts.plugins != null && opts.plugins.length > 0 ? prepareMarkdownPlugins(opts.plugins) : undefined,
346
+ isFinal: incremental ? opts.isFinal === true : true
347
+ });
63
348
  }
64
349
 
65
350
  // ---------------------------------------------------------------------------
@@ -83,6 +368,106 @@ const LINK_TITLE_ONLY_RE = /^ {0,3}(?:"[^"\n]*"|'[^'\n]*'|\([^()\n]*\))[ \t]*$/;
83
368
  function normalizeLinkLabel(label) {
84
369
  return label.trim().replace(/\s+/g, ' ').toLowerCase();
85
370
  }
371
+ function stripBlockquoteMarkers(line) {
372
+ let content = line.endsWith('\r') ? line.slice(0, -1) : line;
373
+ let quoteDepth = 0;
374
+ while (true) {
375
+ const marker = /^ {0,3}> ?/.exec(content);
376
+ if (marker == null) {
377
+ return {
378
+ content,
379
+ quoteDepth
380
+ };
381
+ }
382
+ content = content.slice(marker[0].length);
383
+ quoteDepth++;
384
+ }
385
+ }
386
+ function stripExactBlockquoteDepth(line, quoteDepth) {
387
+ let content = line;
388
+ for (let depth = 0; depth < quoteDepth; depth++) {
389
+ const marker = /^ {0,3}> ?/.exec(content);
390
+ if (marker == null) {
391
+ return null;
392
+ }
393
+ content = content.slice(marker[0].length);
394
+ }
395
+ return content;
396
+ }
397
+
398
+ /**
399
+ * Recognize a standalone display-math marker at the current container boundary.
400
+ * A list opener carries its base indent so an indented continuation marker can
401
+ * close it; blockquotes must keep the same quote depth.
402
+ */
403
+ function displayMathContainer(line) {
404
+ const outer = stripBlockquoteMarkers(line);
405
+ const listMarker = /^( {0,9})(?:[-*+]|\d+[.)]) +(.*)$/.exec(outer.content);
406
+ if (listMarker != null) {
407
+ const taskMarker = /^\[[ xX]\] +(.*)$/.exec(listMarker[2]);
408
+ const inner = stripBlockquoteMarkers(taskMarker?.[1] ?? listMarker[2]);
409
+ return inner.content.trim() === '$$' ? {
410
+ outerQuoteDepth: outer.quoteDepth,
411
+ listBaseIndent: listMarker[1].length,
412
+ innerQuoteDepth: inner.quoteDepth
413
+ } : null;
414
+ }
415
+ return outer.content.trim() === '$$' ? {
416
+ outerQuoteDepth: outer.quoteDepth,
417
+ listBaseIndent: null,
418
+ innerQuoteDepth: 0
419
+ } : null;
420
+ }
421
+ function displayMathLineState(line, container) {
422
+ const outerContent = stripExactBlockquoteDepth(line, container.outerQuoteDepth);
423
+ if (outerContent == null) {
424
+ return 'outside';
425
+ }
426
+ if (container.listBaseIndent == null) {
427
+ // A deeper quote starts a different container. It cannot close or continue
428
+ // the math expression owned by the shallower quote.
429
+ if (/^ {0,3}> ?/.test(outerContent)) {
430
+ return 'outside';
431
+ }
432
+ return outerContent.trim() === '$$' ? 'close' : 'inside';
433
+ }
434
+ const continuationIndent = outerContent.length - outerContent.trimStart().length;
435
+ if (continuationIndent <= container.listBaseIndent) {
436
+ return 'outside';
437
+ }
438
+ const innerContent = stripExactBlockquoteDepth(outerContent.trimStart(), container.innerQuoteDepth);
439
+ if (innerContent == null || /^ {0,3}> ?/.test(innerContent)) {
440
+ return 'outside';
441
+ }
442
+ return innerContent.trim() === '$$' ? 'close' : 'inside';
443
+ }
444
+
445
+ /** Match a complete `$$…$$` display-math block without consuming partial input. */
446
+ function matchDisplayMathBlock(lines, lineIndex) {
447
+ const trimmed = lines[lineIndex].trim();
448
+ if (trimmed.length > 4 && trimmed.startsWith('$$') && trimmed.endsWith('$$')) {
449
+ const value = trimmed.slice(2, -2);
450
+ return value.trim() === '' ? null : {
451
+ value,
452
+ nextIndex: lineIndex + 1,
453
+ endLine: lineIndex
454
+ };
455
+ }
456
+ if (trimmed !== '$$') {
457
+ return null;
458
+ }
459
+ for (let index = lineIndex + 1; index < lines.length; index++) {
460
+ if (lines[index].trim() === '$$') {
461
+ const value = lines.slice(lineIndex + 1, index).join('\n');
462
+ return value.trim() === '' ? null : {
463
+ value,
464
+ nextIndex: index + 1,
465
+ endLine: index
466
+ };
467
+ }
468
+ }
469
+ return null;
470
+ }
86
471
  function matchLinkDefinition(line) {
87
472
  const match = LINK_DEFINITION_RE.exec(line);
88
473
  if (match == null) {
@@ -121,7 +506,7 @@ function matchLinkDefinition(line) {
121
506
  * where every definition is global. Separating a footer definition block with
122
507
  * a blank line — the usual form — always works.
123
508
  */
124
- function extractLinkDefinitions(input) {
509
+ function extractLinkDefinitions(input, math = false) {
125
510
  const lines = input.split('\n');
126
511
  const defs = new Map();
127
512
  const keep = new Array(lines.length).fill(true);
@@ -141,6 +526,16 @@ function extractLinkDefinitions(input) {
141
526
  }
142
527
  continue;
143
528
  }
529
+ if (math) {
530
+ const displayMath = matchDisplayMathBlock(lines, index);
531
+ if (displayMath != null) {
532
+ // Math is opaque Markdown content: definition-shaped TeX must not leak
533
+ // into the document-wide link-definition map.
534
+ index = displayMath.endLine;
535
+ atBoundary = true;
536
+ continue;
537
+ }
538
+ }
144
539
  const fenceMatch = line.match(/^(`{3,}|~{3,})/);
145
540
  if (fenceMatch) {
146
541
  inFence = true;
@@ -193,6 +588,11 @@ function extractLinkDefinitions(input) {
193
588
  };
194
589
  }
195
590
 
591
+ /** Order-independent signature of a citation-source set, for cache checks. */
592
+ function sourceIdsSignature(sourceIds) {
593
+ return sourceIds == null || sourceIds.size === 0 ? '' : [...sourceIds].sort().join('\u0000');
594
+ }
595
+
196
596
  /** Order-independent signature of a link-definition set, for cache checks. */
197
597
  function linkDefsSignature(defs) {
198
598
  if (defs.size === 0) {
@@ -207,6 +607,12 @@ function linkDefsSignature(defs) {
207
607
  * Returns the node plus the index just past the reference, or null when it is
208
608
  * not a resolvable reference (caller falls through to literal handling).
209
609
  */
610
+ function protectedInlineOptions(opts) {
611
+ return opts.plugins == null ? opts : {
612
+ ...opts,
613
+ plugins: undefined
614
+ };
615
+ }
210
616
  function matchReferenceLink(text, start, linkDefs, opts) {
211
617
  const textClose = text.indexOf(']', start + 1);
212
618
  if (textClose === -1) {
@@ -223,12 +629,12 @@ function matchReferenceLink(text, start, linkDefs, opts) {
223
629
  // matches nothing.
224
630
  const label = rawLabel === '' ? linkText : rawLabel;
225
631
  const href = linkDefs.get(normalizeLinkLabel(label));
226
- if (href != null && isSafeUrl(href)) {
632
+ if (href != null && isSafeMarkdownParserUrl(href)) {
227
633
  return {
228
634
  node: {
229
635
  type: 'link',
230
- href,
231
- children: parseInlineImpl(linkText, opts)
636
+ url: href,
637
+ children: parseInlineImpl(linkText, protectedInlineOptions(opts))
232
638
  },
233
639
  end: labelClose + 1
234
640
  };
@@ -242,14 +648,14 @@ function matchReferenceLink(text, start, linkDefs, opts) {
242
648
  return null;
243
649
  }
244
650
  const href = linkDefs.get(normalizeLinkLabel(linkText));
245
- if (href == null || !isSafeUrl(href)) {
651
+ if (href == null || !isSafeMarkdownParserUrl(href)) {
246
652
  return null;
247
653
  }
248
654
  return {
249
655
  node: {
250
656
  type: 'link',
251
- href,
252
- children: parseInlineImpl(linkText, opts)
657
+ url: href,
658
+ children: parseInlineImpl(linkText, protectedInlineOptions(opts))
253
659
  },
254
660
  end: textClose + 1
255
661
  };
@@ -268,11 +674,11 @@ function matchReferenceImage(text, start, linkDefs) {
268
674
  const rawLabel = text.slice(altClose + 2, labelClose);
269
675
  const label = rawLabel === '' ? alt : rawLabel;
270
676
  const src = linkDefs.get(normalizeLinkLabel(label));
271
- if (src != null && isSafeUrl(src)) {
677
+ if (src != null && isSafeMarkdownParserUrl(src)) {
272
678
  return {
273
679
  node: {
274
680
  type: 'image',
275
- src,
681
+ url: src,
276
682
  alt
277
683
  },
278
684
  end: labelClose + 1
@@ -285,13 +691,13 @@ function matchReferenceImage(text, start, linkDefs) {
285
691
  return null;
286
692
  }
287
693
  const src = linkDefs.get(normalizeLinkLabel(alt));
288
- if (src == null || !isSafeUrl(src)) {
694
+ if (src == null || !isSafeMarkdownParserUrl(src)) {
289
695
  return null;
290
696
  }
291
697
  return {
292
698
  node: {
293
699
  type: 'image',
294
- src,
700
+ url: src,
295
701
  alt
296
702
  },
297
703
  end: altClose + 1
@@ -324,27 +730,50 @@ function isWordChar(ch) {
324
730
  return /\w/.test(ch);
325
731
  }
326
732
 
327
- // ---------------------------------------------------------------------------
328
- // URL scheme sanitization
329
- // ---------------------------------------------------------------------------
733
+ /** True when the character at `index` is preceded by an odd backslash run. */
734
+ function isEscaped(text, index) {
735
+ let backslashes = 0;
736
+ for (let i = index - 1; i >= 0 && text[i] === '\\'; i--) {
737
+ backslashes++;
738
+ }
739
+ return backslashes % 2 === 1;
740
+ }
330
741
 
331
742
  /**
332
- * Reject URLs with dangerous schemes (javascript:, vbscript:, data:) that
333
- * could execute arbitrary code when rendered as link hrefs or image srcs.
334
- * Returns true if the URL is safe to use, false otherwise.
743
+ * Find the closing delimiter for `$…$` math on the same line.
744
+ *
745
+ * The whitespace and numeric-edge rules mirror common dollar-math parsers:
746
+ * whitespace cannot hug the delimiters, a digit cannot sit immediately before
747
+ * the opener or after the closer, and `$$` is reserved for display math. The
748
+ * numeric guard prevents ordinary prose such as "$20 and $30" from becoming a
749
+ * formula even in a math-enabled document.
335
750
  */
336
- function isSafeUrl(url) {
337
- // Trim and collapse whitespace/control chars that browsers tolerate but
338
- // could bypass a naive prefix check (e.g. "java\nscript:alert(1)").
339
- // eslint-disable-next-line no-control-regex -- control chars are the bypass
340
- const normalized = url.replace(/[\x00-\x1f\x7f]/g, '').trim();
341
- const lower = normalized.toLowerCase();
342
- if (lower.startsWith('javascript:') || lower.startsWith('vbscript:') || lower.startsWith('data:text/html')) {
343
- return false;
751
+ function isInlineMathStart(text, index) {
752
+ return text[index] === '$' && text[index - 1] !== '$' && text[index + 1] !== '$' && text[index + 1] != null && !/\s/.test(text[index + 1]) && !/\d/.test(text[index - 1] ?? '') && !isEscaped(text, index);
753
+ }
754
+ function findInlineMathEnd(text, start) {
755
+ if (!isInlineMathStart(text, start)) {
756
+ return -1;
344
757
  }
345
- return true;
758
+ for (let index = start + 1; index < text.length; index++) {
759
+ if (text[index] === '\n') {
760
+ return -1;
761
+ }
762
+ if (text[index] !== '$' || isEscaped(text, index)) {
763
+ continue;
764
+ }
765
+ // An unescaped dollar ends this candidate: it either forms a valid closer
766
+ // or makes the whole span literal. Never skip over one and pair with a
767
+ // later dollar, which would swallow currency or another expression.
768
+ if (text[index - 1] === '$' || text[index + 1] === '$' || /\s/.test(text[index - 1]) || /\d/.test(text[index + 1] ?? '')) {
769
+ return -1;
770
+ }
771
+ return index;
772
+ }
773
+ return -1;
346
774
  }
347
775
 
776
+ // ---------------------------------------------------------------------------
348
777
  // ---------------------------------------------------------------------------
349
778
  // Inline parser
350
779
  // ---------------------------------------------------------------------------
@@ -395,8 +824,137 @@ function matchBracketCitation(text, i, opts) {
395
824
  end: closeIndex + 1
396
825
  };
397
826
  }
827
+ function currentLineStart(source, offset) {
828
+ return source.lastIndexOf('\n', offset - 1) + 1;
829
+ }
830
+ function matchExtensionSyntax(source, offset, context, opts) {
831
+ const byFirstCharacter = context === 'inline' ? opts.plugins?.inlineByFirstCharacter : opts.allowBlockSyntax === false ? undefined : opts.plugins?.blockByFirstCharacter;
832
+ const candidates = byFirstCharacter?.get(source[offset]);
833
+ if (candidates == null) {
834
+ return {
835
+ status: 'none'
836
+ };
837
+ }
838
+ const seen = new Set();
839
+ for (const candidate of candidates) {
840
+ if (seen.has(candidate)) {
841
+ continue;
842
+ }
843
+ seen.add(candidate);
844
+ let fullPrefix = false;
845
+ for (const prefix of candidate.contribution.startsWith) {
846
+ if (source.startsWith(prefix, offset)) {
847
+ fullPrefix = true;
848
+ break;
849
+ }
850
+ if (!opts.isFinal) {
851
+ const remaining = source.slice(offset);
852
+ if (remaining.length < prefix.length && prefix.startsWith(remaining)) {
853
+ return {
854
+ status: 'defer'
855
+ };
856
+ }
857
+ }
858
+ }
859
+ if (!fullPrefix) {
860
+ continue;
861
+ }
862
+ const end = Math.min(source.length, offset + candidate.contribution.maxSpan);
863
+ let result;
864
+ try {
865
+ result = candidate.contribution.tokenize({
866
+ source: end === source.length ? source : source.slice(0, end),
867
+ offset,
868
+ end,
869
+ isFinal: opts.isFinal,
870
+ context,
871
+ lineStart: currentLineStart(source, offset),
872
+ column: offset - currentLineStart(source, offset)
873
+ });
874
+ } catch (error) {
875
+ reportMarkdownPluginFailure(candidate.pluginName, 'syntax', error);
876
+ continue;
877
+ }
878
+ if (result == null || typeof result !== 'object' || typeof result.then === 'function') {
879
+ reportMarkdownPluginFailure(candidate.pluginName, 'syntax', new TypeError('Tokenizer returned an invalid result'));
880
+ continue;
881
+ }
882
+ if (result.status === 'no-match') {
883
+ continue;
884
+ }
885
+ if (result.status === 'defer') {
886
+ return !opts.isFinal && end === source.length && source.length - offset < candidate.contribution.maxSpan ? {
887
+ status: 'defer'
888
+ } : {
889
+ status: 'none'
890
+ };
891
+ }
892
+ 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}`)) {
893
+ reportMarkdownPluginFailure(candidate.pluginName, 'syntax', new TypeError('Tokenizer returned an invalid extension node'));
894
+ continue;
895
+ }
896
+ const rawNode = result.node;
897
+ const {
898
+ source: _ignoredSource,
899
+ position: _ignoredPosition,
900
+ ...safeNode
901
+ } = rawNode;
902
+ const absoluteStart = (opts.baseOffset ?? 0) + offset;
903
+ const absoluteEnd = (opts.baseOffset ?? 0) + result.end;
904
+ return {
905
+ status: 'match',
906
+ end: result.end,
907
+ node: Object.freeze({
908
+ ...safeNode,
909
+ data: freezeMarkdownPluginData(result.node.data),
910
+ source: source.slice(offset, result.end),
911
+ ...(opts.sourceRanges && context === 'block' ? {
912
+ position: Object.freeze({
913
+ start: Object.freeze({
914
+ offset: absoluteStart
915
+ }),
916
+ end: Object.freeze({
917
+ offset: absoluteEnd
918
+ })
919
+ })
920
+ } : null)
921
+ })
922
+ };
923
+ }
924
+ return {
925
+ status: 'none'
926
+ };
927
+ }
398
928
  export function parseInline(text, arg) {
399
- return parseInlineEntry(text, resolveOptions(arg));
929
+ return projectInlineNodes(parseInlineAstRuntime(text, arg), wantsLegacyRanges(arg));
930
+ }
931
+
932
+ /**
933
+ * Parse inline Markdown into the canonical immutable Astryx AST.
934
+ *
935
+ * Unlike `parseInline`, this returns MDAST-aligned node names and fields. Math
936
+ * nodes are present only when `math: true`, and plugin extension nodes are
937
+ * inferred from the ordered `plugins` tuple.
938
+ */
939
+
940
+ export function parseInlineAst(text, arg) {
941
+ return parseInlineAstRuntime(text, arg);
942
+ }
943
+ function parseInlineAstRuntime(text, arg) {
944
+ const opts = resolveOptions(arg);
945
+ const nodes = parseInlineEntry(text, opts);
946
+ if ((opts.plugins?.transforms.length ?? 0) === 0) {
947
+ return nodes;
948
+ }
949
+ const transformed = applyMarkdownTransforms({
950
+ type: 'root',
951
+ children: [{
952
+ type: 'paragraph',
953
+ children: nodes
954
+ }]
955
+ }, opts.plugins, text, opts.isFinal, 'inline');
956
+ const paragraph = transformed.children[0];
957
+ return paragraph?.type === 'paragraph' ? [...paragraph.children] : nodes;
400
958
  }
401
959
 
402
960
  /**
@@ -406,7 +964,7 @@ export function parseInline(text, arg) {
406
964
  * wrapper and call `parseInlineImpl` directly so the transform runs only on
407
965
  * the outermost block's inline tree — letting `transformAutolinks` decide
408
966
  * which subtrees to descend into (text, bold, italic, strikethrough) and
409
- * which to skip (link, code, image, citation, break).
967
+ * which to skip (link, code, math, image, citation, break).
410
968
  */
411
969
  function parseInlineEntry(text, opts) {
412
970
  const nodes = parseInlineImpl(text, opts);
@@ -414,13 +972,18 @@ function parseInlineEntry(text, opts) {
414
972
  }
415
973
  function parseInlineImpl(text, opts) {
416
974
  const nodes = [];
975
+ // Only a plugin that actually contributes INLINE syntax may cost anything
976
+ // per source position. A transform-only list contributes none, so it takes
977
+ // the same path as an omitted or empty one: no candidate probe per
978
+ // position, and no map lookup per character in the plain-text scan below.
979
+ const inlineExtensionStarts = opts.plugins != null && opts.plugins.inlineByFirstCharacter.size > 0 ? opts.plugins.inlineByFirstCharacter : undefined;
417
980
  let i = 0;
418
981
  while (i < text.length) {
419
982
  // --- Escape ---
420
983
  if (text[i] === '\\' && i + 1 < text.length) {
421
984
  nodes.push({
422
985
  type: 'text',
423
- content: text[i + 1]
986
+ value: text[i + 1]
424
987
  });
425
988
  i += 2;
426
989
  continue;
@@ -433,14 +996,27 @@ function parseInlineImpl(text, opts) {
433
996
  const closeIndex = text.indexOf('`'.repeat(tickCount), openIndex);
434
997
  if (closeIndex !== -1) {
435
998
  nodes.push({
436
- type: 'code',
437
- content: text.slice(openIndex, closeIndex)
999
+ type: 'inlineCode',
1000
+ value: text.slice(openIndex, closeIndex)
438
1001
  });
439
1002
  i = closeIndex + tickCount;
440
1003
  continue;
441
1004
  }
442
1005
  }
443
1006
 
1007
+ // --- Inline math (opt-in; code takes precedence) ---
1008
+ if (opts.math && text[i] === '$') {
1009
+ const closeIndex = findInlineMathEnd(text, i);
1010
+ if (closeIndex !== -1) {
1011
+ nodes.push({
1012
+ type: 'inlineMath',
1013
+ value: text.slice(i + 1, closeIndex)
1014
+ });
1015
+ i = closeIndex + 1;
1016
+ continue;
1017
+ }
1018
+ }
1019
+
444
1020
  // --- Citation: fullwidth 【id】 ---
445
1021
  {
446
1022
  const citation = matchFullwidthCitation(text, i, opts);
@@ -461,16 +1037,16 @@ function parseInlineImpl(text, opts) {
461
1037
  const srcClose = findClosingParen(text, altClose + 2);
462
1038
  if (srcClose !== -1) {
463
1039
  const src = text.slice(altClose + 2, srcClose);
464
- if (!isSafeUrl(src)) {
1040
+ if (!isSafeMarkdownParserUrl(src)) {
465
1041
  // Dangerous scheme — emit as plain text.
466
1042
  nodes.push({
467
1043
  type: 'text',
468
- content: text.slice(i, srcClose + 1)
1044
+ value: text.slice(i, srcClose + 1)
469
1045
  });
470
1046
  } else {
471
1047
  nodes.push({
472
1048
  type: 'image',
473
- src,
1049
+ url: src,
474
1050
  alt: text.slice(i + 2, altClose)
475
1051
  });
476
1052
  }
@@ -510,17 +1086,17 @@ function parseInlineImpl(text, opts) {
510
1086
  const urlClose = findClosingParen(text, textClose + 2);
511
1087
  if (urlClose !== -1) {
512
1088
  const href = text.slice(textClose + 2, urlClose);
513
- if (!isSafeUrl(href)) {
1089
+ if (!isSafeMarkdownParserUrl(href)) {
514
1090
  // Dangerous scheme — emit as plain text instead of a link.
515
1091
  nodes.push({
516
1092
  type: 'text',
517
- content: text.slice(i, urlClose + 1)
1093
+ value: text.slice(i, urlClose + 1)
518
1094
  });
519
1095
  } else {
520
1096
  nodes.push({
521
1097
  type: 'link',
522
- href,
523
- children: parseInlineImpl(text.slice(i + 1, textClose), opts)
1098
+ url: href,
1099
+ children: parseInlineImpl(text.slice(i + 1, textClose), protectedInlineOptions(opts))
524
1100
  });
525
1101
  }
526
1102
  i = urlClose + 1;
@@ -549,9 +1125,9 @@ function parseInlineImpl(text, opts) {
549
1125
  const closeIndex = text.indexOf(marker, i + 3);
550
1126
  if (closeIndex !== -1 && (!isUnderscore || !isWordChar(text[closeIndex + 3]))) {
551
1127
  nodes.push({
552
- type: 'bold',
1128
+ type: 'strong',
553
1129
  children: [{
554
- type: 'italic',
1130
+ type: 'emphasis',
555
1131
  children: parseInlineImpl(text.slice(i + 3, closeIndex), opts)
556
1132
  }]
557
1133
  });
@@ -571,7 +1147,7 @@ function parseInlineImpl(text, opts) {
571
1147
  const closeIndex = text.indexOf(marker, i + 2);
572
1148
  if (closeIndex !== -1 && (!isUnderscore || !isWordChar(text[closeIndex + 2]))) {
573
1149
  nodes.push({
574
- type: 'bold',
1150
+ type: 'strong',
575
1151
  children: parseInlineImpl(text.slice(i + 2, closeIndex), opts)
576
1152
  });
577
1153
  i = closeIndex + 2;
@@ -585,7 +1161,7 @@ function parseInlineImpl(text, opts) {
585
1161
  const closeIndex = text.indexOf('~~', i + 2);
586
1162
  if (closeIndex !== -1) {
587
1163
  nodes.push({
588
- type: 'strikethrough',
1164
+ type: 'delete',
589
1165
  children: parseInlineImpl(text.slice(i + 2, closeIndex), opts)
590
1166
  });
591
1167
  i = closeIndex + 2;
@@ -602,7 +1178,7 @@ function parseInlineImpl(text, opts) {
602
1178
  const closeIndex = text.indexOf(text[i], i + 1);
603
1179
  if (closeIndex !== -1 && closeIndex > i + 1 && (!isUnderscore || !isWordChar(text[closeIndex + 1]))) {
604
1180
  nodes.push({
605
- type: 'italic',
1181
+ type: 'emphasis',
606
1182
  children: parseInlineImpl(text.slice(i + 1, closeIndex), opts)
607
1183
  });
608
1184
  i = closeIndex + 1;
@@ -611,9 +1187,22 @@ function parseInlineImpl(text, opts) {
611
1187
  }
612
1188
  }
613
1189
 
1190
+ // --- Extension syntax (built-ins and protected contexts win) ---
1191
+ if (inlineExtensionStarts !== undefined) {
1192
+ const extension = matchExtensionSyntax(text, i, 'inline', opts);
1193
+ if (extension.status === 'match') {
1194
+ nodes.push(extension.node);
1195
+ i = extension.end;
1196
+ continue;
1197
+ }
1198
+ if (extension.status === 'defer') {
1199
+ break;
1200
+ }
1201
+ }
1202
+
614
1203
  // --- Plain text (with line-break detection) ---
615
1204
  let end = i + 1;
616
- while (end < text.length && !'*_~`[!\\\n\u3010'.includes(text[end])) {
1205
+ while (end < text.length && !'*_~`[!\\\n\u3010'.includes(text[end]) && !(opts.math && text[end] === '$') && (inlineExtensionStarts === undefined || !inlineExtensionStarts.has(text[end]))) {
617
1206
  end++;
618
1207
  }
619
1208
  const content = text.slice(i, end);
@@ -625,11 +1214,14 @@ function parseInlineImpl(text, opts) {
625
1214
  if (trimmed.length > 0) {
626
1215
  const last = nodes[nodes.length - 1];
627
1216
  if (last?.type === 'text') {
628
- last.content += trimmed;
1217
+ nodes[nodes.length - 1] = {
1218
+ ...last,
1219
+ value: last.value + trimmed
1220
+ };
629
1221
  } else {
630
1222
  nodes.push({
631
1223
  type: 'text',
632
- content: trimmed
1224
+ value: trimmed
633
1225
  });
634
1226
  }
635
1227
  }
@@ -642,11 +1234,14 @@ function parseInlineImpl(text, opts) {
642
1234
  }
643
1235
  const last = nodes[nodes.length - 1];
644
1236
  if (last?.type === 'text') {
645
- last.content += content;
1237
+ nodes[nodes.length - 1] = {
1238
+ ...last,
1239
+ value: last.value + content
1240
+ };
646
1241
  } else {
647
1242
  nodes.push({
648
1243
  type: 'text',
649
- content
1244
+ value: content
650
1245
  });
651
1246
  }
652
1247
  i = end;
@@ -750,7 +1345,7 @@ function scanAutolinksInText(text) {
750
1345
  while ((m = re.exec(text)) !== null) {
751
1346
  const url = m[1];
752
1347
  // Skip dangerous URL schemes (javascript:, vbscript:, data:text/html)
753
- if (!isSafeUrl(url)) {
1348
+ if (!isSafeMarkdownParserUrl(url)) {
754
1349
  continue;
755
1350
  }
756
1351
  matches.push({
@@ -875,7 +1470,7 @@ function splitTextOnAutolinks(content) {
875
1470
  if (matches.length === 0) {
876
1471
  return [{
877
1472
  type: 'text',
878
- content
1473
+ value: content
879
1474
  }];
880
1475
  }
881
1476
  const out = [];
@@ -884,15 +1479,15 @@ function splitTextOnAutolinks(content) {
884
1479
  if (m.start > cursor) {
885
1480
  out.push({
886
1481
  type: 'text',
887
- content: content.slice(cursor, m.start)
1482
+ value: content.slice(cursor, m.start)
888
1483
  });
889
1484
  }
890
1485
  out.push({
891
1486
  type: 'link',
892
- href: m.href,
1487
+ url: m.href,
893
1488
  children: [{
894
1489
  type: 'text',
895
- content: m.display
1490
+ value: m.display
896
1491
  }]
897
1492
  });
898
1493
  cursor = m.end;
@@ -900,7 +1495,7 @@ function splitTextOnAutolinks(content) {
900
1495
  if (cursor < content.length) {
901
1496
  out.push({
902
1497
  type: 'text',
903
- content: content.slice(cursor)
1498
+ value: content.slice(cursor)
904
1499
  });
905
1500
  }
906
1501
  return out;
@@ -918,16 +1513,19 @@ function transformAutolinks(nodes) {
918
1513
  const out = [];
919
1514
  for (const node of nodes) {
920
1515
  if (node.type === 'text') {
921
- const split = splitTextOnAutolinks(node.content);
1516
+ const split = splitTextOnAutolinks(node.value);
922
1517
  for (const seg of split) {
923
1518
  const last = out[out.length - 1];
924
1519
  if (seg.type === 'text' && last?.type === 'text') {
925
- last.content += seg.content;
1520
+ out[out.length - 1] = {
1521
+ ...last,
1522
+ value: last.value + seg.value
1523
+ };
926
1524
  } else {
927
1525
  out.push(seg);
928
1526
  }
929
1527
  }
930
- } else if (node.type === 'bold' || node.type === 'italic' || node.type === 'strikethrough') {
1528
+ } else if (node.type === 'strong' || node.type === 'emphasis' || node.type === 'delete') {
931
1529
  out.push({
932
1530
  ...node,
933
1531
  children: transformAutolinks(node.children)
@@ -1012,10 +1610,19 @@ function isTableSeparator(line) {
1012
1610
  * offset into it would not address the document.
1013
1611
  */
1014
1612
  function nested(opts) {
1015
- return opts.sourceRanges ? {
1613
+ const nestedOptions = opts.allowBlockSyntax === false ? opts : {
1016
1614
  ...opts,
1017
- sourceRanges: false
1018
- } : opts;
1615
+ allowBlockSyntax: false
1616
+ };
1617
+ return nestedOptions.sourceRanges || nestedOptions.astPositions ? {
1618
+ ...nestedOptions,
1619
+ sourceRanges: false,
1620
+ astPositions: false
1621
+ } : nestedOptions;
1622
+ }
1623
+ function blockExtensionColumn(line) {
1624
+ const indentation = line.length - line.trimStart().length;
1625
+ return indentation <= 3 ? indentation : null;
1019
1626
  }
1020
1627
 
1021
1628
  /**
@@ -1083,6 +1690,7 @@ function splitTableRow(line) {
1083
1690
  }
1084
1691
  function parseTable(lines, lineIndex, opts) {
1085
1692
  const headers = splitTableRow(lines[lineIndex]).map(cell => ({
1693
+ type: 'tableCell',
1086
1694
  children: parseInlineEntry(cell, opts)
1087
1695
  }));
1088
1696
  const alignments = splitTableRow(lines[lineIndex + 1]).map(cell => {
@@ -1094,17 +1702,24 @@ function parseTable(lines, lineIndex, opts) {
1094
1702
  const rows = [];
1095
1703
  let rowIndex = lineIndex + 2;
1096
1704
  while (rowIndex < lines.length && lines[rowIndex].includes('|') && lines[rowIndex].trim() !== '') {
1097
- rows.push(splitTableRow(lines[rowIndex]).map(cell => ({
1098
- children: parseInlineEntry(cell, opts)
1099
- })));
1705
+ rows.push({
1706
+ type: 'tableRow',
1707
+ children: splitTableRow(lines[rowIndex]).map(cell => ({
1708
+ type: 'tableCell',
1709
+ children: parseInlineEntry(cell, opts)
1710
+ }))
1711
+ });
1100
1712
  rowIndex++;
1101
1713
  }
1714
+ const header = {
1715
+ type: 'tableRow',
1716
+ children: headers
1717
+ };
1102
1718
  return {
1103
1719
  node: {
1104
1720
  type: 'table',
1105
- headers,
1106
- alignments,
1107
- rows
1721
+ align: alignments,
1722
+ children: [header, ...rows]
1108
1723
  },
1109
1724
  nextIndex: rowIndex
1110
1725
  };
@@ -1147,6 +1762,7 @@ function parseList(lines, startIndex, ordered, opts) {
1147
1762
  itemText += '\n' + deindented.join('\n');
1148
1763
  }
1149
1764
  items.push({
1765
+ type: 'listItem',
1150
1766
  checked,
1151
1767
  children: parseMarkdownImpl(itemText, nested(opts))
1152
1768
  });
@@ -1163,15 +1779,16 @@ function parseList(lines, startIndex, ordered, opts) {
1163
1779
  index = lookahead;
1164
1780
  }
1165
1781
  }
1782
+ const node = {
1783
+ type: 'list',
1784
+ ordered,
1785
+ start,
1786
+ delimiter: ordered ? delim : undefined,
1787
+ spread: loose || undefined,
1788
+ children: items
1789
+ };
1166
1790
  return {
1167
- node: {
1168
- type: 'list',
1169
- ordered,
1170
- start,
1171
- delimiter: ordered ? delim : undefined,
1172
- loose: loose || undefined,
1173
- items
1174
- },
1791
+ node,
1175
1792
  nextIndex: index
1176
1793
  };
1177
1794
  }
@@ -1181,7 +1798,33 @@ function parseList(lines, startIndex, ordered, opts) {
1181
1798
  // ---------------------------------------------------------------------------
1182
1799
 
1183
1800
  export function parseMarkdown(input, arg) {
1184
- return parseMarkdownImpl(input, resolveOptions(arg));
1801
+ return projectMarkdownRoot(parseMarkdownAstInternal(input, arg, true), wantsLegacyRanges(arg));
1802
+ }
1803
+
1804
+ /**
1805
+ * Parse block Markdown into the canonical immutable Astryx AST.
1806
+ *
1807
+ * Unlike `parseMarkdown`, this returns the MDAST-aligned root used by Markdown,
1808
+ * transforms, and Outline. Math nodes are present only when `math: true`, and
1809
+ * plugin extension nodes are inferred from the ordered `plugins` tuple.
1810
+ */
1811
+
1812
+ export function parseMarkdownAst(input, arg) {
1813
+ return parseMarkdownAstInternal(input, arg, true);
1814
+ }
1815
+
1816
+ /** @internal Full-parse entry point with streaming finality for Outline. */
1817
+ export function parseMarkdownAstInternal(input, arg, isFinal) {
1818
+ const resolved = resolveOptions(arg);
1819
+ const opts = isFinal ? resolved : {
1820
+ ...resolved,
1821
+ isFinal: false
1822
+ };
1823
+ const root = {
1824
+ type: 'root',
1825
+ children: parseMarkdownImpl(input, opts)
1826
+ };
1827
+ return applyMarkdownTransforms(root, opts.plugins, input, opts.isFinal, 'block');
1185
1828
  }
1186
1829
  function parseMarkdownImpl(input, baseOpts) {
1187
1830
  // Collect this input's link reference definitions and strip their lines,
@@ -1195,7 +1838,7 @@ function parseMarkdownImpl(input, baseOpts) {
1195
1838
  defs,
1196
1839
  cleaned,
1197
1840
  lineMap
1198
- } = extractLinkDefinitions(input);
1841
+ } = extractLinkDefinitions(input, baseOpts.math);
1199
1842
  const inherited = baseOpts.linkDefs;
1200
1843
  let linkDefs;
1201
1844
  if (defs.size === 0) {
@@ -1210,14 +1853,26 @@ function parseMarkdownImpl(input, baseOpts) {
1210
1853
  linkDefs
1211
1854
  } : baseOpts;
1212
1855
  const lines = cleaned.split('\n');
1856
+ const hasBlockExtensionSyntax = opts.allowBlockSyntax !== false && (opts.plugins?.blockByFirstCharacter.size ?? 0) > 0;
1857
+ // Derived from the already-split lines rather than a second character
1858
+ // scan: every line contributes its own length plus the newline it ended on.
1859
+ const lineOffsets = [0];
1860
+ if (opts.astPositions === true || hasBlockExtensionSyntax) {
1861
+ let offset = 0;
1862
+ for (let index = 0; index < lines.length; index++) {
1863
+ offset += lines[index].length + 1;
1864
+ lineOffsets.push(offset);
1865
+ }
1866
+ }
1867
+ const blockExtensionMatches = new Map();
1213
1868
  const blocks = [];
1214
1869
  // The line each block started on, parallel to `blocks`. Only collected when
1215
1870
  // ranges were asked for; a block's end is resolved after the loop, since the
1216
1871
  // branch that produced it has already moved `index` past whatever it read.
1217
- const blockStartLines = opts.sourceRanges ? [] : null;
1872
+ const blockStartLines = opts.astPositions === true ? [] : null;
1218
1873
  // Set only by a block that consumes blank lines as content, where the
1219
1874
  // positional end derivation would trim them away.
1220
- const blockEndLines = opts.sourceRanges ? [] : null;
1875
+ const blockEndLines = opts.astPositions === true ? [] : null;
1221
1876
  let blockStartLine = 0;
1222
1877
  const pushBlock = (node, endLine) => {
1223
1878
  blocks.push(node);
@@ -1234,10 +1889,13 @@ function parseMarkdownImpl(input, baseOpts) {
1234
1889
  }
1235
1890
 
1236
1891
  // --- Fenced code block ---
1237
- const fenceMatch = line.match(/^(`{3,}|~{3,})(\w*)/);
1892
+ const fenceMatch = line.match(/^(`{3,}|~{3,})/);
1238
1893
  if (fenceMatch) {
1239
1894
  const fence = fenceMatch[1];
1240
- const language = fenceMatch[2] || 'plaintext';
1895
+ const info = line.slice(fence.length);
1896
+ const language = info.match(/^(\S+)/)?.[1] ?? null;
1897
+ const legacyLanguage = info.match(/^(\w*)/)?.[1] || null;
1898
+ const meta = language == null ? undefined : info.slice(language.length).trim() || undefined;
1241
1899
  const codeLines = [];
1242
1900
  index++;
1243
1901
  while (index < lines.length && !lines[index].startsWith(fence)) {
@@ -1248,20 +1906,36 @@ function parseMarkdownImpl(input, baseOpts) {
1248
1906
  // A fence owns its blank lines, and an unterminated one (mid-stream)
1249
1907
  // can end on them, so it states its own end rather than letting the
1250
1908
  // positional derivation trim them off.
1251
- pushBlock({
1252
- type: 'codeblock',
1253
- language,
1254
- content: codeLines.join('\n')
1255
- }, Math.min(index, lines.length) - 1);
1909
+ pushBlock(markMarkdownAstLegacyCodeLanguage({
1910
+ type: 'code',
1911
+ lang: language,
1912
+ ...(meta == null ? {} : {
1913
+ meta
1914
+ }),
1915
+ value: codeLines.join('\n')
1916
+ }, legacyLanguage), Math.min(index, lines.length) - 1);
1256
1917
  continue;
1257
1918
  }
1258
1919
 
1920
+ // --- Display math (opt-in; fenced code takes precedence) ---
1921
+ if (opts.math) {
1922
+ const displayMath = matchDisplayMathBlock(lines, index);
1923
+ if (displayMath != null) {
1924
+ pushBlock({
1925
+ type: 'math',
1926
+ value: displayMath.value
1927
+ }, displayMath.endLine);
1928
+ index = displayMath.nextIndex;
1929
+ continue;
1930
+ }
1931
+ }
1932
+
1259
1933
  // --- Heading ---
1260
1934
  const headingMatch = line.match(/^(#{1,6}) +(.*)/);
1261
1935
  if (headingMatch) {
1262
1936
  pushBlock({
1263
1937
  type: 'heading',
1264
- level: headingMatch[1].length,
1938
+ depth: headingMatch[1].length,
1265
1939
  children: parseInlineEntry(headingMatch[2], opts)
1266
1940
  });
1267
1941
  index++;
@@ -1271,7 +1945,7 @@ function parseMarkdownImpl(input, baseOpts) {
1271
1945
  // --- HR (must precede list check to handle `- - -`, `* * *`, `_ _ _`) ---
1272
1946
  if (isHorizontalRule(line)) {
1273
1947
  pushBlock({
1274
- type: 'hr'
1948
+ type: 'thematicBreak'
1275
1949
  });
1276
1950
  index++;
1277
1951
  continue;
@@ -1281,11 +1955,11 @@ function parseMarkdownImpl(input, baseOpts) {
1281
1955
  // An unsafe src falls through to the paragraph path and renders as
1282
1956
  // literal text, the same rule the inline image path applies.
1283
1957
  const imageMatch = line.match(/^!\[([^\]]*)\]\(([^)]+)\)/);
1284
- if (imageMatch && line.trim() === imageMatch[0] && isSafeUrl(imageMatch[2])) {
1958
+ if (imageMatch && line.trim() === imageMatch[0] && isSafeMarkdownParserUrl(imageMatch[2])) {
1285
1959
  pushBlock({
1286
1960
  type: 'image',
1287
1961
  alt: imageMatch[1],
1288
- src: imageMatch[2]
1962
+ url: imageMatch[2]
1289
1963
  });
1290
1964
  index++;
1291
1965
  continue;
@@ -1329,11 +2003,52 @@ function parseMarkdownImpl(input, baseOpts) {
1329
2003
  continue;
1330
2004
  }
1331
2005
 
2006
+ // --- Extension block syntax (built-in blocks take precedence) ---
2007
+ if (hasBlockExtensionSyntax) {
2008
+ const extensionColumn = blockExtensionColumn(line);
2009
+ const extensionOffset = extensionColumn == null ? lineOffsets[index] : lineOffsets[index] + extensionColumn;
2010
+ const extension = blockExtensionMatches.get(extensionOffset) ?? (extensionColumn == null ? {
2011
+ status: 'none'
2012
+ } : matchExtensionSyntax(cleaned, extensionOffset, 'block', opts));
2013
+ blockExtensionMatches.delete(extensionOffset);
2014
+ if (extension.status === 'match') {
2015
+ const consumedEnd = extension.end;
2016
+ const consumed = cleaned.slice(lineOffsets[index], consumedEnd);
2017
+ if (consumedEnd < cleaned.length && cleaned[consumedEnd] !== '\n' && cleaned[consumedEnd - 1] !== '\n') {
2018
+ reportMarkdownPluginFailure(extension.node.plugin, 'syntax', new TypeError('Block syntax must consume complete lines'));
2019
+ } else {
2020
+ const newlineCount = consumed.split('\n').length - 1;
2021
+ const consumedLines = Math.max(1, newlineCount + (consumed.endsWith('\n') ? 0 : 1));
2022
+ pushBlock(extension.node, index + consumedLines - 1);
2023
+ index += consumedLines;
2024
+ continue;
2025
+ }
2026
+ }
2027
+ if (extension.status === 'defer') {
2028
+ break;
2029
+ }
2030
+ }
2031
+
1332
2032
  // --- Paragraph ---
1333
2033
  const paraLines = [line];
1334
2034
  index++;
1335
- while (index < lines.length && !isBlockStart(lines[index]) && lines[index].trim() !== '') {
1336
- paraLines.push(lines[index]);
2035
+ while (index < lines.length) {
2036
+ const nextLine = lines[index];
2037
+ if (isBlockStart(nextLine) || opts.math && matchDisplayMathBlock(lines, index) != null || nextLine.trim() === '') {
2038
+ break;
2039
+ }
2040
+ if (hasBlockExtensionSyntax) {
2041
+ const extensionColumn = blockExtensionColumn(nextLine);
2042
+ if (extensionColumn != null) {
2043
+ const extensionOffset = lineOffsets[index] + extensionColumn;
2044
+ const extension = matchExtensionSyntax(cleaned, extensionOffset, 'block', opts);
2045
+ if (extension.status !== 'none') {
2046
+ blockExtensionMatches.set(extensionOffset, extension);
2047
+ break;
2048
+ }
2049
+ }
2050
+ }
2051
+ paraLines.push(nextLine);
1337
2052
  index++;
1338
2053
  }
1339
2054
  pushBlock({
@@ -1385,9 +2100,13 @@ function stampSourceRanges(blocks, blockStartLines, blockEndLines, lines, lineMa
1385
2100
  const end = lineStart(endLine) + lines[endLine].length;
1386
2101
  blocks[i] = {
1387
2102
  ...blocks[i],
1388
- range: {
1389
- start: lineStart(startLine),
1390
- end
2103
+ position: {
2104
+ start: {
2105
+ offset: lineStart(startLine)
2106
+ },
2107
+ end: {
2108
+ offset: end
2109
+ }
1391
2110
  }
1392
2111
  };
1393
2112
  }
@@ -1401,13 +2120,18 @@ const incrementalCaches = new WeakMap();
1401
2120
  function makeIncrementalCache(state) {
1402
2121
  const {
1403
2122
  defs
1404
- } = extractLinkDefinitions(state.settledText);
2123
+ } = extractLinkDefinitions(state.settledText, state.math);
1405
2124
  const cache = {
1406
2125
  settledEnd: state.settledText.length,
1407
2126
  settledLinkDefs: new Map(defs),
1408
2127
  tailLinkDefs: new Map(),
1409
2128
  linkDefs: defs,
1410
2129
  linkDefsKey: linkDefsSignature(defs),
2130
+ settledAstBlocks: [],
2131
+ settledRevision: 0,
2132
+ projectedRevision: -1,
2133
+ projectedSettledBlocks: [],
2134
+ projectionCache: new WeakMap(),
1411
2135
  work: {
1412
2136
  splitCharacters: 0,
1413
2137
  boundaryLines: 0,
@@ -1418,6 +2142,11 @@ function makeIncrementalCache(state) {
1418
2142
  incrementalCaches.set(state, cache);
1419
2143
  return cache;
1420
2144
  }
2145
+
2146
+ /**
2147
+ * Create an incremental parser cache. Use the `<true>` type argument with
2148
+ * `MathParseOptions` so the cache and returned nodes share the math contract.
2149
+ */
1421
2150
  export function createIncrementalState() {
1422
2151
  const state = {
1423
2152
  prevInput: '',
@@ -1456,33 +2185,70 @@ export function getIncrementalParseWork(state) {
1456
2185
  * the boundary to -1 even though the content before the fence opened cannot be
1457
2186
  * changed by anything typed inside it.
1458
2187
  */
1459
- function findSettledBoundary(lines) {
2188
+ function findSettledBoundary(lines, math = false) {
1460
2189
  let inFence = false;
1461
2190
  let fenceMarker = '';
2191
+ let mathContainer = null;
2192
+ let suppressMathUntilBoundary = false;
1462
2193
  let lastBoundary = -1;
1463
2194
  let boundaryBeforeFence = -1;
2195
+ let boundaryBeforeMath = -1;
1464
2196
  for (let lineIndex = 0; lineIndex < lines.length; lineIndex++) {
1465
2197
  const line = lines[lineIndex];
1466
-
1467
- // Fence open / close — match the specific marker character and length
1468
- const fenceMatch = line.match(/^(`{3,}|~{3,})/);
1469
- if (fenceMatch) {
1470
- if (!inFence) {
1471
- inFence = true;
1472
- fenceMarker = fenceMatch[1];
1473
- boundaryBeforeFence = lastBoundary;
1474
- } else if (fenceMatch[1].startsWith(fenceMarker[0]) && fenceMatch[1].length >= fenceMarker.length) {
2198
+ if (inFence) {
2199
+ const fenceMatch = line.match(/^(`{3,}|~{3,})/);
2200
+ if (fenceMatch && fenceMatch[1].startsWith(fenceMarker[0]) && fenceMatch[1].length >= fenceMarker.length) {
1475
2201
  inFence = false;
1476
2202
  fenceMarker = '';
1477
2203
  }
2204
+ continue;
2205
+ }
2206
+ if (mathContainer != null) {
2207
+ const state = displayMathLineState(line, mathContainer);
2208
+ if (state === 'close') {
2209
+ mathContainer = null;
2210
+ continue;
2211
+ }
2212
+ if (state === 'inside') {
2213
+ continue;
2214
+ }
2215
+ // The list item or blockquote ended before a closer arrived. The parser
2216
+ // treats that unmatched opener literally, so resume ordinary boundary
2217
+ // detection on this first line outside the container.
2218
+ mathContainer = null;
2219
+ suppressMathUntilBoundary = true;
2220
+ }
2221
+
2222
+ // A complete same-line `$$…$$` expression never changes boundary state.
2223
+ // A standalone marker may belong to the top level, a blockquote, or one
2224
+ // list item; remember that container so its continuation marker closes the
2225
+ // same expression instead of opening a new one.
2226
+ if (math && !suppressMathUntilBoundary) {
2227
+ const container = displayMathContainer(line);
2228
+ if (container != null) {
2229
+ mathContainer = container;
2230
+ boundaryBeforeMath = lastBoundary;
2231
+ continue;
2232
+ }
1478
2233
  }
1479
- if (!inFence && line.trim() === '' && lineIndex > 0 && lineIndex < lines.length - 1) {
1480
- lastBoundary = lineIndex;
2234
+ const fenceMatch = line.match(/^(`{3,}|~{3,})/);
2235
+ if (fenceMatch) {
2236
+ inFence = true;
2237
+ fenceMarker = fenceMatch[1];
2238
+ boundaryBeforeFence = lastBoundary;
2239
+ continue;
2240
+ }
2241
+ if (line.trim() === '') {
2242
+ suppressMathUntilBoundary = false;
2243
+ if (lineIndex > 0 && lineIndex < lines.length - 1) {
2244
+ lastBoundary = lineIndex;
2245
+ }
1481
2246
  }
1482
2247
  }
1483
2248
  return {
1484
- boundary: inFence ? boundaryBeforeFence : lastBoundary,
1485
- openFence: inFence
2249
+ boundary: inFence ? boundaryBeforeFence : mathContainer != null ? boundaryBeforeMath : lastBoundary,
2250
+ openFence: inFence,
2251
+ openMath: mathContainer != null
1486
2252
  };
1487
2253
  }
1488
2254
 
@@ -1490,10 +2256,45 @@ function findSettledBoundary(lines) {
1490
2256
  * Strip trailing incomplete inline syntax that appears during streaming.
1491
2257
  * Only affects the tail of the last line — safe to apply to the full string.
1492
2258
  */
1493
- export function trimStreamingArtifacts(input) {
1494
- const lastNL = input.lastIndexOf('\n');
1495
- const prefix = lastNL === -1 ? '' : input.slice(0, lastNL + 1);
1496
- let tail = lastNL === -1 ? input : input.slice(lastNL + 1);
2259
+ export function trimStreamingArtifacts(input, options) {
2260
+ // First remove an incomplete display expression as one structural unit. This
2261
+ // full-input scan distinguishes a terminal nested closer from a new opener;
2262
+ // looking only at the final `$$` line cannot.
2263
+ const displayTrimmed = options?.math ? trimOpenDisplayMath(input) : input;
2264
+ const lastNL = displayTrimmed.lastIndexOf('\n');
2265
+ const prefix = lastNL === -1 ? '' : displayTrimmed.slice(0, lastNL + 1);
2266
+ let tail = lastNL === -1 ? displayTrimmed : displayTrimmed.slice(lastNL + 1);
2267
+ if (options?.math) {
2268
+ // Hold an unmatched inline opener so raw TeX syntax does not flash while
2269
+ // streaming. If another unescaped dollar is already present but fails the
2270
+ // closing-boundary rule, keep both literal (the currency case).
2271
+ for (let index = 0; index < tail.length; index++) {
2272
+ if (tail[index] === '$' && tail[index + 1] == null && tail[index - 1] !== '$' && !/\d/.test(tail[index - 1] ?? '') && !isEscaped(tail, index)) {
2273
+ tail = tail.slice(0, index);
2274
+ break;
2275
+ }
2276
+ if (!isInlineMathStart(tail, index)) {
2277
+ continue;
2278
+ }
2279
+ const close = findInlineMathEnd(tail, index);
2280
+ if (close !== -1) {
2281
+ index = close;
2282
+ continue;
2283
+ }
2284
+ let laterDollar = -1;
2285
+ for (let next = index + 1; next < tail.length; next++) {
2286
+ if (tail[next] === '$' && !isEscaped(tail, next)) {
2287
+ laterDollar = next;
2288
+ break;
2289
+ }
2290
+ }
2291
+ if (laterDollar === -1) {
2292
+ tail = tail.slice(0, index);
2293
+ break;
2294
+ }
2295
+ index = laterDollar;
2296
+ }
2297
+ }
1497
2298
 
1498
2299
  // Scan backwards for unclosed syntax markers — no regex to avoid ReDoS
1499
2300
  // Find the last unclosed [ or ![ (link/image start)
@@ -1650,6 +2451,54 @@ export function trimStreamingArtifacts(input) {
1650
2451
  * Once a table is established (header + separator exist), new data rows
1651
2452
  * render immediately — no suppression.
1652
2453
  */
2454
+ function trimOpenDisplayMath(text) {
2455
+ const lines = text.split('\n');
2456
+ let inFence = false;
2457
+ let fenceMarker = '';
2458
+ let mathContainer = null;
2459
+ let suppressMathUntilBoundary = false;
2460
+ let mathStartLine = -1;
2461
+ for (let index = 0; index < lines.length; index++) {
2462
+ const line = lines[index];
2463
+ if (inFence) {
2464
+ const fence = line.match(/^(`{3,}|~{3,})/);
2465
+ if (fence != null && fence[1].startsWith(fenceMarker[0]) && fence[1].length >= fenceMarker.length) {
2466
+ inFence = false;
2467
+ fenceMarker = '';
2468
+ }
2469
+ continue;
2470
+ }
2471
+ if (mathContainer != null) {
2472
+ const state = displayMathLineState(line, mathContainer);
2473
+ if (state === 'close') {
2474
+ mathContainer = null;
2475
+ mathStartLine = -1;
2476
+ continue;
2477
+ }
2478
+ if (state === 'inside') {
2479
+ continue;
2480
+ }
2481
+ mathContainer = null;
2482
+ mathStartLine = -1;
2483
+ suppressMathUntilBoundary = true;
2484
+ }
2485
+ if (line.trim() === '') {
2486
+ suppressMathUntilBoundary = false;
2487
+ }
2488
+ const fence = line.match(/^(`{3,}|~{3,})/);
2489
+ if (fence != null) {
2490
+ inFence = true;
2491
+ fenceMarker = fence[1];
2492
+ continue;
2493
+ }
2494
+ const container = suppressMathUntilBoundary ? null : displayMathContainer(line);
2495
+ if (container != null) {
2496
+ mathContainer = container;
2497
+ mathStartLine = index;
2498
+ }
2499
+ }
2500
+ return mathContainer != null && mathStartLine >= 0 ? lines.slice(0, mathStartLine).join('\n').trimEnd() : text;
2501
+ }
1653
2502
  function trimUnsettledStructural(text) {
1654
2503
  const lines = text.split('\n');
1655
2504
 
@@ -1715,7 +2564,7 @@ function trimUnsettledStructural(text) {
1715
2564
  * rather than into the slice.
1716
2565
  */
1717
2566
  function atOffset(opts, offset) {
1718
- return opts.sourceRanges ? {
2567
+ return opts.astPositions === true ? {
1719
2568
  ...opts,
1720
2569
  baseOffset: offset
1721
2570
  } : opts;
@@ -1740,14 +2589,13 @@ function mergeSettledBlocks(prev, delta) {
1740
2589
  ordered: prevLast.ordered,
1741
2590
  start: prevLast.start,
1742
2591
  delimiter: prevLast.delimiter,
1743
- loose: true,
1744
- items: [...prevLast.items, ...deltaFirst.items],
1745
- // One list now, so one range: from where the first half started to
1746
- // where the second half ended.
1747
- ...(prevLast.range != null && deltaFirst.range != null ? {
1748
- range: {
1749
- start: prevLast.range.start,
1750
- end: deltaFirst.range.end
2592
+ spread: true,
2593
+ children: [...prevLast.children, ...deltaFirst.children],
2594
+ // One list now, so one position spans both halves.
2595
+ ...(prevLast.position != null && deltaFirst.position != null ? {
2596
+ position: {
2597
+ start: prevLast.position.start,
2598
+ end: deltaFirst.position.end
1751
2599
  }
1752
2600
  } : null)
1753
2601
  };
@@ -1759,11 +2607,11 @@ function mergeSettledBlocks(prev, delta) {
1759
2607
  /** Append a newly-settled slice without copying the already-settled prefix. */
1760
2608
  function appendSettledBlocks(prev, delta) {
1761
2609
  if (delta.length === 0) {
1762
- return;
2610
+ return false;
1763
2611
  }
1764
2612
  if (prev.length === 0) {
1765
2613
  prev.push(...delta);
1766
- return;
2614
+ return false;
1767
2615
  }
1768
2616
  const prevLast = prev[prev.length - 1];
1769
2617
  const deltaFirst = delta[0];
@@ -1773,19 +2621,20 @@ function appendSettledBlocks(prev, delta) {
1773
2621
  ordered: prevLast.ordered,
1774
2622
  start: prevLast.start,
1775
2623
  delimiter: prevLast.delimiter,
1776
- loose: true,
1777
- items: [...prevLast.items, ...deltaFirst.items],
1778
- ...(prevLast.range != null && deltaFirst.range != null ? {
1779
- range: {
1780
- start: prevLast.range.start,
1781
- end: deltaFirst.range.end
2624
+ spread: true,
2625
+ children: [...prevLast.children, ...deltaFirst.children],
2626
+ ...(prevLast.position != null && deltaFirst.position != null ? {
2627
+ position: {
2628
+ start: prevLast.position.start,
2629
+ end: deltaFirst.position.end
1782
2630
  }
1783
2631
  } : null)
1784
2632
  };
1785
2633
  prev.push(...delta.slice(1));
1786
- return;
2634
+ return true;
1787
2635
  }
1788
2636
  prev.push(...delta);
2637
+ return false;
1789
2638
  }
1790
2639
  function sameUnsettledDefinitions(previous, next, settled) {
1791
2640
  for (const [label, destination] of previous) {
@@ -1806,11 +2655,19 @@ function resetIncrementalCache(state, cache) {
1806
2655
  state.settledBlocks = [];
1807
2656
  state.settledUpTo = 0;
1808
2657
  state.linkDefsKey = undefined;
2658
+ state.sourceIdsKey = undefined;
2659
+ state.pluginSyntaxIdentity = undefined;
2660
+ state.math = undefined;
1809
2661
  cache.settledEnd = 0;
1810
2662
  cache.settledLinkDefs.clear();
1811
2663
  cache.tailLinkDefs = new Map();
1812
2664
  cache.linkDefs = new Map();
1813
2665
  cache.linkDefsKey = '';
2666
+ cache.settledAstBlocks = [];
2667
+ cache.settledRevision++;
2668
+ cache.projectedRevision = -1;
2669
+ cache.projectedSettledBlocks = [];
2670
+ cache.projectionCache = new WeakMap();
1814
2671
  cache.work = {
1815
2672
  splitCharacters: 0,
1816
2673
  boundaryLines: 0,
@@ -1832,23 +2689,47 @@ function resetIncrementalCache(state, cache) {
1832
2689
  */
1833
2690
 
1834
2691
  export function parseMarkdownIncremental(input, state, arg) {
1835
- const opts = resolveOptions(arg);
2692
+ const withRanges = wantsLegacyRanges(arg);
2693
+ const root = parseMarkdownAstIncremental(input, state, arg);
1836
2694
  const cache = incrementalCaches.get(state) ?? makeIncrementalCache(state);
1837
- let reparseSettled = false;
2695
+ const projected = projectMarkdownRoot(root, withRanges, cache.projectionCache);
2696
+ if (cache.projectedRevision !== cache.settledRevision) {
2697
+ cache.projectedSettledBlocks = cache.settledAstBlocks.map(block => projectBlockNode(block, cache.projectionCache, withRanges));
2698
+ cache.projectedRevision = cache.settledRevision;
2699
+ }
2700
+ state.settledBlocks = cache.projectedSettledBlocks;
2701
+ return projected;
2702
+ }
1838
2703
 
1839
- // Invalidate cache when the autolink or sourceRanges option flips — cached
2704
+ /** @internal Canonical incremental parse used by Markdown rendering. */
2705
+ export function parseMarkdownAstIncremental(input, state, arg) {
2706
+ const opts = resolveOptions(arg, true);
2707
+ const root = {
2708
+ type: 'root',
2709
+ children: parseMarkdownIncrementalAstBlocks(input, state, opts)
2710
+ };
2711
+ return applyMarkdownTransforms(root, opts.plugins, input, opts.isFinal, 'block');
2712
+ }
2713
+ function parseMarkdownIncrementalAstBlocks(input, state, opts) {
2714
+ const cache = incrementalCaches.get(state) ?? makeIncrementalCache(state);
2715
+ let reparseSettled = false;
2716
+ const nextSourceIdsKey = sourceIdsSignature(opts.sourceIds);
2717
+ const nextPluginSyntaxIdentity = opts.plugins?.syntaxIdentity ?? '';
2718
+ // Invalidate cache when an option that changes parsed nodes flips — cached
1840
2719
  // settled blocks were parsed with the previous setting and would otherwise
1841
2720
  // be reused unchanged.
1842
- if (state.autolink !== opts.autolink || Boolean(state.sourceRanges) !== Boolean(opts.sourceRanges)) {
2721
+ if (state.autolink !== opts.autolink || state.math !== opts.math || Boolean(state.sourceRanges) !== Boolean(opts.sourceRanges) || state.sourceIdsKey !== nextSourceIdsKey || state.pluginSyntaxIdentity !== nextPluginSyntaxIdentity) {
1843
2722
  reparseSettled = true;
1844
2723
  state.autolink = opts.autolink;
2724
+ state.math = opts.math;
1845
2725
  state.sourceRanges = opts.sourceRanges;
2726
+ state.sourceIdsKey = nextSourceIdsKey;
2727
+ state.pluginSyntaxIdentity = nextPluginSyntaxIdentity;
1846
2728
  }
1847
2729
  if (input === '') {
1848
2730
  resetIncrementalCache(state, cache);
1849
2731
  return [];
1850
2732
  }
1851
-
1852
2733
  // The settled prefix is only reusable while the input still contains it
1853
2734
  // verbatim. Lengths alone cannot tell: a same-length or longer replacement
1854
2735
  // (new args after reusing a state) disagrees with the prefix without ever
@@ -1860,7 +2741,10 @@ export function parseMarkdownIncremental(input, state, arg) {
1860
2741
  if (state.settledText.length !== cache.settledEnd || !input.startsWith(state.settledText)) {
1861
2742
  resetIncrementalCache(state, cache);
1862
2743
  state.autolink = opts.autolink;
2744
+ state.math = opts.math;
1863
2745
  state.sourceRanges = opts.sourceRanges;
2746
+ state.sourceIdsKey = nextSourceIdsKey;
2747
+ state.pluginSyntaxIdentity = nextPluginSyntaxIdentity;
1864
2748
  }
1865
2749
 
1866
2750
  // The recurring parse costs are confined to the mutable suffix: splitting,
@@ -1871,8 +2755,9 @@ export function parseMarkdownIncremental(input, state, arg) {
1871
2755
  const tailLines = tailRaw.split('\n');
1872
2756
  const {
1873
2757
  boundary,
1874
- openFence
1875
- } = findSettledBoundary(tailLines);
2758
+ openFence,
2759
+ openMath
2760
+ } = findSettledBoundary(tailLines, opts.math);
1876
2761
  const settledDelta = boundary >= 0 ? tailLines.slice(0, boundary).join('\n') : '';
1877
2762
  const nextSettledEnd = oldSettledEnd + settledDelta.length;
1878
2763
  const unsettledInput = input.slice(nextSettledEnd);
@@ -1885,7 +2770,7 @@ export function parseMarkdownIncremental(input, state, arg) {
1885
2770
  if (settledDelta !== '') {
1886
2771
  const {
1887
2772
  defs: deltaDefs
1888
- } = extractLinkDefinitions(settledDelta);
2773
+ } = extractLinkDefinitions(settledDelta, opts.math);
1889
2774
  for (const [label, destination] of deltaDefs) {
1890
2775
  if (!cache.settledLinkDefs.has(label)) {
1891
2776
  cache.settledLinkDefs.set(label, destination);
@@ -1897,7 +2782,7 @@ export function parseMarkdownIncremental(input, state, arg) {
1897
2782
  }
1898
2783
  const {
1899
2784
  defs: tailLinkDefs
1900
- } = extractLinkDefinitions(unsettledInput);
2785
+ } = extractLinkDefinitions(unsettledInput, opts.math);
1901
2786
  if (!sameUnsettledDefinitions(cache.tailLinkDefs, tailLinkDefs, cache.settledLinkDefs)) {
1902
2787
  definitionsChanged = true;
1903
2788
  }
@@ -1921,27 +2806,47 @@ export function parseMarkdownIncremental(input, state, arg) {
1921
2806
  }
1922
2807
  cache.settledEnd = nextSettledEnd;
1923
2808
  }
1924
- const unsettledRaw = unsettledInput.trim();
2809
+ const trimmedUnsettledInput = unsettledInput.trim();
2810
+ // String#trim removes the CR that belongs to the final content line of a
2811
+ // CRLF snapshot along with trailing blank lines. Keep that one byte so
2812
+ // source ranges and delimiter content remain identical to a full parse.
2813
+ const unsettledRaw = trimmedUnsettledInput !== '' && /\r(?:\n[\s]*)?$/.test(unsettledInput) ? `${trimmedUnsettledInput}\r` : trimmedUnsettledInput;
1925
2814
  // Structural trimming holds back lines that look like an incomplete list or
1926
2815
  // table, which inside a fence is ordinary code: a TypeScript union or a `- `
1927
2816
  // would disappear from the code block as it streams.
1928
- const unsettledText = openFence ? unsettledRaw : trimUnsettledStructural(unsettledRaw);
2817
+ const unsettledText = openFence ? unsettledRaw : openMath ? trimOpenDisplayMath(unsettledRaw) : trimUnsettledStructural(unsettledRaw);
2818
+ const legacyRanges = opts.sourceRanges === true;
1929
2819
  let parsedSettledBlocks = 0;
1930
2820
  if (reparseSettled) {
1931
- state.settledBlocks = state.settledText ? parseMarkdownImpl(state.settledText, parseOpts) : [];
1932
- parsedSettledBlocks = state.settledBlocks.length;
2821
+ cache.settledAstBlocks = state.settledText ? parseMarkdownImpl(state.settledText, parseOpts) : [];
2822
+ parsedSettledBlocks = cache.settledAstBlocks.length;
2823
+ cache.settledRevision++;
2824
+ cache.projectedSettledBlocks = cache.settledAstBlocks.map(block => projectBlockNode(block, cache.projectionCache, legacyRanges));
2825
+ cache.projectedRevision = cache.settledRevision;
1933
2826
  } else if (settledDelta !== '') {
1934
2827
  const deltaBlocks = parseMarkdownImpl(settledDelta, atOffset(parseOpts, oldSettledEnd));
1935
- appendSettledBlocks(state.settledBlocks, deltaBlocks);
2828
+ const mergeIndex = cache.settledAstBlocks.length - 1;
2829
+ const mergedList = appendSettledBlocks(cache.settledAstBlocks, deltaBlocks);
1936
2830
  parsedSettledBlocks = deltaBlocks.length;
2831
+ cache.settledRevision++;
2832
+ const projectedDelta = deltaBlocks.map(block => projectBlockNode(block, cache.projectionCache, legacyRanges));
2833
+ if (mergedList) {
2834
+ const canonicalMerged = cache.settledAstBlocks[mergeIndex];
2835
+ const projectedMerged = projectBlockNode(canonicalMerged, cache.projectionCache, legacyRanges);
2836
+ cache.projectedSettledBlocks[mergeIndex] = projectedMerged;
2837
+ cache.projectedSettledBlocks.push(...projectedDelta.slice(1));
2838
+ } else {
2839
+ cache.projectedSettledBlocks.push(...projectedDelta);
2840
+ }
2841
+ cache.projectedRevision = cache.settledRevision;
1937
2842
  }
1938
2843
 
1939
2844
  // The unsettled tail is trimmed before parsing, so its offset in the
1940
2845
  // document is where that trimmed text actually starts — not the boundary,
1941
- // which is a line index. If it somehow can't be located, parse it without
1942
- // ranges rather than report wrong ones. Only worth searching for when
1943
- // ranges were asked for: this runs on every streamed chunk.
1944
- const unsettledStart = unsettledText && opts.sourceRanges ? input.indexOf(unsettledText, cache.settledEnd) : -1;
2846
+ // which is a line index. If it somehow cannot be located, parse it without
2847
+ // positions rather than report wrong ones. The search starts at the
2848
+ // settled end, so it scans the tail and never the prefix.
2849
+ const unsettledStart = unsettledText && opts.astPositions === true ? input.indexOf(unsettledText, cache.settledEnd) : -1;
1945
2850
  const unsettledBlocks = unsettledText ? parseMarkdownImpl(unsettledText, unsettledStart >= 0 ? atOffset(parseOpts, unsettledStart) : nested(parseOpts)) : [];
1946
2851
  state.prevInput = input;
1947
2852
 
@@ -1956,7 +2861,7 @@ export function parseMarkdownIncremental(input, state, arg) {
1956
2861
  definitionCharacters: settledDelta.length + unsettledInput.length,
1957
2862
  renderedBlocks: parsedSettledBlocks + unsettledBlocks.length
1958
2863
  };
1959
- return mergeSettledBlocks(state.settledBlocks, unsettledBlocks);
2864
+ return mergeSettledBlocks(cache.settledAstBlocks, unsettledBlocks);
1960
2865
  }
1961
2866
 
1962
2867
  // ---------------------------------------------------------------------------
@@ -1967,27 +2872,6 @@ export function parseMarkdownIncremental(input, state, arg) {
1967
2872
  // derives its item ids from the same functions, so outline hash links always
1968
2873
  // resolve to a rendered heading by construction.
1969
2874
 
1970
- /** Flatten inline nodes into their plain text content. */
1971
- export function inlineText(nodes) {
1972
- return nodes.map(node => {
1973
- switch (node.type) {
1974
- case 'text':
1975
- case 'code':
1976
- return node.content;
1977
- case 'bold':
1978
- case 'italic':
1979
- case 'strikethrough':
1980
- case 'link':
1981
- return inlineText(node.children);
1982
- case 'image':
1983
- return node.alt;
1984
- case 'citation':
1985
- case 'break':
1986
- return '';
1987
- }
1988
- }).join('');
1989
- }
1990
-
1991
2875
  /** Turn heading text into a URL-safe slug (lowercase, hyphen-separated). */
1992
2876
  export function slugify(value) {
1993
2877
  return value.trim().toLowerCase().replace(/['"]/g, '').replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');