@astryxdesign/core 0.6.2 → 0.6.3-canary.0899f21

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 (549) hide show
  1. package/CHANGELOG.md +81 -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/Button/Button.d.ts.map +1 -1
  9. package/dist/Button/Button.js +13 -28
  10. package/dist/Chat/ChatComposer.d.ts.map +1 -1
  11. package/dist/Chat/ChatComposer.js +10 -5
  12. package/dist/Chat/ChatComposerDrawer.d.ts +19 -5
  13. package/dist/Chat/ChatComposerDrawer.d.ts.map +1 -1
  14. package/dist/Chat/ChatComposerDrawer.js +48 -23
  15. package/dist/Chat/ChatComposerInput.d.ts +11 -3
  16. package/dist/Chat/ChatComposerInput.d.ts.map +1 -1
  17. package/dist/Chat/ChatComposerInput.js +63 -16
  18. package/dist/Chat/ChatDictationButton.d.ts +3 -2
  19. package/dist/Chat/ChatDictationButton.d.ts.map +1 -1
  20. package/dist/Chat/ChatDictationButton.js +6 -4
  21. package/dist/Chat/ChatLayout.d.ts +19 -0
  22. package/dist/Chat/ChatLayout.d.ts.map +1 -1
  23. package/dist/Chat/ChatLayout.js +19 -0
  24. package/dist/Chat/ChatLayoutScrollButton.d.ts.map +1 -1
  25. package/dist/Chat/ChatLayoutScrollButton.js +44 -34
  26. package/dist/Chat/ChatMessageBubble.d.ts +2 -0
  27. package/dist/Chat/ChatMessageBubble.d.ts.map +1 -1
  28. package/dist/Chat/ChatMessageBubble.js +5 -3
  29. package/dist/Chat/ChatMessageList.d.ts +2 -1
  30. package/dist/Chat/ChatMessageList.d.ts.map +1 -1
  31. package/dist/Chat/ChatMessageList.js +4 -3
  32. package/dist/Chat/ChatMessageMetadata.d.ts +5 -4
  33. package/dist/Chat/ChatMessageMetadata.d.ts.map +1 -1
  34. package/dist/Chat/ChatMessageMetadata.js +12 -9
  35. package/dist/Chat/ChatSendButton.js +1 -1
  36. package/dist/Chat/index.d.ts +1 -1
  37. package/dist/Chat/index.d.ts.map +1 -1
  38. package/dist/CheckboxInput/CheckboxInput.d.ts.map +1 -1
  39. package/dist/CheckboxInput/CheckboxInput.js +12 -2
  40. package/dist/Citation/Citation.d.ts +1 -1
  41. package/dist/Citation/Citation.d.ts.map +1 -1
  42. package/dist/Citation/Citation.js +3 -2
  43. package/dist/CodeBlock/highlightRanges.d.ts.map +1 -1
  44. package/dist/CodeBlock/highlightRanges.js +14 -3
  45. package/dist/Collapsible/Collapsible.d.ts.map +1 -1
  46. package/dist/Collapsible/Collapsible.js +6 -1
  47. package/dist/CommandPalette/CommandPaletteInput.d.ts.map +1 -1
  48. package/dist/CommandPalette/CommandPaletteInput.js +1 -1
  49. package/dist/ComplexSelector/ComplexSelector.d.ts.map +1 -1
  50. package/dist/ComplexSelector/ComplexSelector.js +1 -1
  51. package/dist/DateInput/DateInput.d.ts.map +1 -1
  52. package/dist/DateInput/DateInput.js +4 -4
  53. package/dist/DateInput/NativeDateField.d.ts.map +1 -1
  54. package/dist/DateInput/NativeDateField.js +16 -16
  55. package/dist/DateInput/TouchDateField.d.ts.map +1 -1
  56. package/dist/DateInput/TouchDateField.js +2 -2
  57. package/dist/DateRangeInput/DateRangeInput.d.ts.map +1 -1
  58. package/dist/DateRangeInput/DateRangeInput.js +4 -4
  59. package/dist/DateTimeInput/DateTimeInput.d.ts.map +1 -1
  60. package/dist/DateTimeInput/DateTimeInput.js +8 -8
  61. package/dist/DateTimeInput/TouchDateTimeField.d.ts.map +1 -1
  62. package/dist/DateTimeInput/TouchDateTimeField.js +4 -4
  63. package/dist/DateTimeInput/nativePickerSegmentStyles.d.ts +9 -3
  64. package/dist/DateTimeInput/nativePickerSegmentStyles.d.ts.map +1 -1
  65. package/dist/DateTimeInput/nativePickerSegmentStyles.js +3 -3
  66. package/dist/Dialog/DialogHeader.d.ts +8 -1
  67. package/dist/Dialog/DialogHeader.d.ts.map +1 -1
  68. package/dist/Dialog/DialogHeader.js +13 -4
  69. package/dist/Field/InputClearButton.d.ts +4 -2
  70. package/dist/Field/InputClearButton.d.ts.map +1 -1
  71. package/dist/Field/InputClearButton.js +4 -2
  72. package/dist/Field/PanelSearchInput.d.ts.map +1 -1
  73. package/dist/Field/PanelSearchInput.js +1 -1
  74. package/dist/FileInput/FileInput.d.ts.map +1 -1
  75. package/dist/FileInput/FileInput.js +7 -7
  76. package/dist/Icon/Icon.d.ts +6 -31
  77. package/dist/Icon/Icon.d.ts.map +1 -1
  78. package/dist/Icon/Icon.js +8 -71
  79. package/dist/Icon/IconDefaultSizeContext.d.ts +4 -0
  80. package/dist/Icon/IconDefaultSizeContext.d.ts.map +1 -0
  81. package/dist/Icon/IconDefaultSizeContext.js +18 -0
  82. package/dist/Icon/IconSize.stylex.d.ts +65 -0
  83. package/dist/Icon/IconSize.stylex.d.ts.map +1 -0
  84. package/dist/Icon/IconSize.stylex.js +75 -0
  85. package/dist/Item/Item.d.ts +1 -1
  86. package/dist/Item/Item.d.ts.map +1 -1
  87. package/dist/Item/Item.js +4 -3
  88. package/dist/Kbd/Kbd.d.ts +5 -3
  89. package/dist/Kbd/Kbd.d.ts.map +1 -1
  90. package/dist/Kbd/Kbd.js +36 -42
  91. package/dist/Link/Link.d.ts.map +1 -1
  92. package/dist/Link/Link.js +6 -2
  93. package/dist/Link/useLinkComponent.d.ts +5 -15
  94. package/dist/Link/useLinkComponent.d.ts.map +1 -1
  95. package/dist/Link/useLinkComponent.js +34 -51
  96. package/dist/List/List.d.ts +19 -1
  97. package/dist/List/List.d.ts.map +1 -1
  98. package/dist/List/List.js +9 -2
  99. package/dist/List/ListContext.d.ts +1 -0
  100. package/dist/List/ListContext.d.ts.map +1 -1
  101. package/dist/List/ListContext.js +2 -1
  102. package/dist/List/ListItem.d.ts +1 -1
  103. package/dist/List/ListItem.d.ts.map +1 -1
  104. package/dist/List/ListItem.js +9 -2
  105. package/dist/Markdown/Markdown.d.ts +8 -2
  106. package/dist/Markdown/Markdown.d.ts.map +1 -1
  107. package/dist/Markdown/Markdown.fr23.bench.d.ts +2 -0
  108. package/dist/Markdown/Markdown.fr23.bench.d.ts.map +1 -0
  109. package/dist/Markdown/Markdown.fr23.bench.js +150 -0
  110. package/dist/Markdown/Markdown.fr23.sampling.d.ts +2 -0
  111. package/dist/Markdown/Markdown.fr23.sampling.d.ts.map +1 -0
  112. package/dist/Markdown/Markdown.fr23.sampling.js +43 -0
  113. package/dist/Markdown/Markdown.js +262 -109
  114. package/dist/Markdown/ast.d.ts +162 -0
  115. package/dist/Markdown/ast.d.ts.map +1 -0
  116. package/dist/Markdown/ast.js +74 -0
  117. package/dist/Markdown/index.d.ts +5 -3
  118. package/dist/Markdown/index.d.ts.map +1 -1
  119. package/dist/Markdown/index.js +3 -2
  120. package/dist/Markdown/parser/index.d.ts +10 -0
  121. package/dist/Markdown/parser/index.d.ts.map +1 -0
  122. package/dist/Markdown/parser/index.js +10 -0
  123. package/dist/Markdown/parser.d.ts +126 -58
  124. package/dist/Markdown/parser.d.ts.map +1 -1
  125. package/dist/Markdown/parser.js +716 -167
  126. package/dist/Markdown/plugins/frontmatter.d.ts +32 -0
  127. package/dist/Markdown/plugins/frontmatter.d.ts.map +1 -0
  128. package/dist/Markdown/plugins/frontmatter.js +132 -0
  129. package/dist/Markdown/plugins/index.d.ts +17 -0
  130. package/dist/Markdown/plugins/index.d.ts.map +1 -0
  131. package/dist/Markdown/plugins/index.js +14 -0
  132. package/dist/Markdown/plugins/protocol.d.ts +191 -0
  133. package/dist/Markdown/plugins/protocol.d.ts.map +1 -0
  134. package/dist/Markdown/plugins/protocol.js +957 -0
  135. package/dist/Markdown/plugins/semanticFence.d.ts +36 -0
  136. package/dist/Markdown/plugins/semanticFence.d.ts.map +1 -0
  137. package/dist/Markdown/plugins/semanticFence.js +213 -0
  138. package/dist/Markdown/plugins/softBreaks.d.ts +3 -0
  139. package/dist/Markdown/plugins/softBreaks.d.ts.map +1 -0
  140. package/dist/Markdown/plugins/softBreaks.js +172 -0
  141. package/dist/Markdown/plugins/sourceDecoration.d.ts +76 -0
  142. package/dist/Markdown/plugins/sourceDecoration.d.ts.map +1 -0
  143. package/dist/Markdown/plugins/sourceDecoration.js +393 -0
  144. package/dist/Markdown/plugins/textTransform.d.ts +30 -0
  145. package/dist/Markdown/plugins/textTransform.d.ts.map +1 -0
  146. package/dist/Markdown/plugins/textTransform.js +374 -0
  147. package/dist/Markdown/remark.d.ts +196 -0
  148. package/dist/Markdown/remark.d.ts.map +1 -0
  149. package/dist/Markdown/remark.js +1062 -0
  150. package/dist/Markdown/url.d.ts +6 -0
  151. package/dist/Markdown/url.d.ts.map +1 -0
  152. package/dist/Markdown/url.js +31 -0
  153. package/dist/Markdown/utils.d.ts +4 -2
  154. package/dist/Markdown/utils.d.ts.map +1 -1
  155. package/dist/Markdown/utils.js +2 -1
  156. package/dist/MetadataList/MetadataList.js +3 -3
  157. package/dist/NumberInput/NumberInput.d.ts.map +1 -1
  158. package/dist/NumberInput/NumberInput.js +4 -4
  159. package/dist/Outline/index.d.ts +1 -0
  160. package/dist/Outline/index.d.ts.map +1 -1
  161. package/dist/Outline/parseOutlineFromMarkdown.d.ts +7 -1
  162. package/dist/Outline/parseOutlineFromMarkdown.d.ts.map +1 -1
  163. package/dist/Outline/parseOutlineFromMarkdown.js +14 -8
  164. package/dist/Outline/useOutlineFromMarkdown.d.ts +3 -1
  165. package/dist/Outline/useOutlineFromMarkdown.d.ts.map +1 -1
  166. package/dist/Outline/useOutlineFromMarkdown.js +8 -3
  167. package/dist/PowerSearch/PowerSearchEditPopover.d.ts.map +1 -1
  168. package/dist/PowerSearch/PowerSearchEditPopover.js +46 -30
  169. package/dist/PowerSearch/PowerSearchValueEditor.d.ts.map +1 -1
  170. package/dist/PowerSearch/PowerSearchValueEditor.js +5 -0
  171. package/dist/ProgressBar/ProgressBar.d.ts.map +1 -1
  172. package/dist/ProgressBar/ProgressBar.js +36 -31
  173. package/dist/RadioList/RadioListItem.d.ts.map +1 -1
  174. package/dist/RadioList/RadioListItem.js +13 -1
  175. package/dist/ScrollableArea/ScrollableArea.d.ts +1 -0
  176. package/dist/ScrollableArea/ScrollableArea.d.ts.map +1 -1
  177. package/dist/ScrollableArea/ScrollableArea.js +1 -0
  178. package/dist/SegmentedControl/SegmentedControlItem.d.ts.map +1 -1
  179. package/dist/SegmentedControl/SegmentedControlItem.js +5 -5
  180. package/dist/Selector/Selector.d.ts +9 -7
  181. package/dist/Selector/Selector.d.ts.map +1 -1
  182. package/dist/Selector/Selector.js +16 -7
  183. package/dist/SideNav/SideNav.d.ts +2 -1
  184. package/dist/SideNav/SideNav.d.ts.map +1 -1
  185. package/dist/SideNav/SideNav.js +9 -3
  186. package/dist/Slider/Slider.d.ts.map +1 -1
  187. package/dist/Slider/Slider.js +28 -12
  188. package/dist/Spinner/Spinner.d.ts.map +1 -1
  189. package/dist/Spinner/Spinner.js +23 -7
  190. package/dist/Switch/Switch.d.ts.map +1 -1
  191. package/dist/Switch/Switch.js +11 -0
  192. package/dist/TabList/Tab.d.ts +1 -1
  193. package/dist/TabList/Tab.d.ts.map +1 -1
  194. package/dist/TabList/Tab.js +20 -7
  195. package/dist/Table/Table.d.ts +1 -0
  196. package/dist/Table/Table.d.ts.map +1 -1
  197. package/dist/Table/Table.js +29 -13
  198. package/dist/TextArea/TextArea.d.ts.map +1 -1
  199. package/dist/TextArea/TextArea.js +1 -1
  200. package/dist/TextInput/TextInput.d.ts.map +1 -1
  201. package/dist/TextInput/TextInput.js +2 -2
  202. package/dist/TimeInput/TimeInput.d.ts.map +1 -1
  203. package/dist/TimeInput/TimeInput.js +4 -4
  204. package/dist/Timer/Timer.d.ts +49 -0
  205. package/dist/Timer/Timer.d.ts.map +1 -0
  206. package/dist/Timer/Timer.js +158 -0
  207. package/dist/Timer/index.d.ts +9 -0
  208. package/dist/Timer/index.d.ts.map +1 -0
  209. package/dist/Timer/index.js +10 -0
  210. package/dist/ToggleButton/ToggleButton.d.ts +9 -6
  211. package/dist/ToggleButton/ToggleButton.d.ts.map +1 -1
  212. package/dist/ToggleButton/ToggleButton.js +15 -27
  213. package/dist/TopNav/TopNavMenu.d.ts +1 -1
  214. package/dist/TopNav/TopNavMenu.d.ts.map +1 -1
  215. package/dist/TopNav/TopNavMenu.js +3 -2
  216. package/dist/TreeList/TreeList.d.ts.map +1 -1
  217. package/dist/TreeList/TreeList.js +3 -0
  218. package/dist/TreeList/TreeListItem.d.ts +6 -1
  219. package/dist/TreeList/TreeListItem.d.ts.map +1 -1
  220. package/dist/TreeList/TreeListItem.js +12 -2
  221. package/dist/TreeList/TreeListTypes.d.ts +8 -1
  222. package/dist/TreeList/TreeListTypes.d.ts.map +1 -1
  223. package/dist/Typeahead/BaseTypeahead.d.ts.map +1 -1
  224. package/dist/Typeahead/BaseTypeahead.js +16 -7
  225. package/dist/astryx.css +27 -11
  226. package/dist/hooks/scrollKeyboardDelegation.d.ts +3 -0
  227. package/dist/hooks/scrollKeyboardDelegation.d.ts.map +1 -0
  228. package/dist/hooks/scrollKeyboardDelegation.js +146 -0
  229. package/dist/hooks/useClickableContainer.d.ts +10 -1
  230. package/dist/hooks/useClickableContainer.d.ts.map +1 -1
  231. package/dist/hooks/useClickableContainer.js +15 -4
  232. package/dist/hooks/useScrollableArea.d.ts +6 -2
  233. package/dist/hooks/useScrollableArea.d.ts.map +1 -1
  234. package/dist/hooks/useScrollableArea.js +17 -5
  235. package/dist/index.d.ts +1 -0
  236. package/dist/index.d.ts.map +1 -1
  237. package/dist/index.js +3 -0
  238. package/dist/theme/declarationBoundary.d.ts +17 -0
  239. package/dist/theme/declarationBoundary.d.ts.map +1 -0
  240. package/dist/theme/declarationBoundary.js +343 -0
  241. package/dist/theme/derivedVarRegistry.d.ts.map +1 -1
  242. package/dist/theme/derivedVarRegistry.js +4 -0
  243. package/dist/theme/generateThemeRules.d.ts +13 -7
  244. package/dist/theme/generateThemeRules.d.ts.map +1 -1
  245. package/dist/theme/generateThemeRules.js +140 -34
  246. package/dist/utils/interactionOverlay.stylex.d.ts +8 -0
  247. package/dist/utils/interactionOverlay.stylex.d.ts.map +1 -1
  248. package/dist/utils/interactionOverlay.stylex.js +9 -0
  249. package/dist/utils/safeUrl.d.ts +11 -0
  250. package/dist/utils/safeUrl.d.ts.map +1 -0
  251. package/dist/utils/safeUrl.js +78 -0
  252. package/locales/af-ZA.json +530 -1
  253. package/locales/ar-SA.json +490 -1
  254. package/locales/ca-ES.json +514 -1
  255. package/locales/cs-CZ.json +498 -1
  256. package/locales/da-DK.json +522 -1
  257. package/locales/de-DE.json +522 -1
  258. package/locales/el-GR.json +498 -1
  259. package/locales/es-ES.json +510 -1
  260. package/locales/fi-FI.json +498 -1
  261. package/locales/fr-FR.json +534 -1
  262. package/locales/he-IL.json +498 -1
  263. package/locales/hu-HU.json +498 -1
  264. package/locales/it-IT.json +510 -1
  265. package/locales/ja-JP.json +490 -1
  266. package/locales/ko-KR.json +498 -1
  267. package/locales/nl-NL.json +534 -1
  268. package/locales/no-NO.json +510 -1
  269. package/locales/pl-PL.json +506 -1
  270. package/locales/pt-BR.json +506 -1
  271. package/locales/pt-PT.json +506 -1
  272. package/locales/ro-RO.json +506 -1
  273. package/locales/ru-RU.json +498 -1
  274. package/locales/sr-SP.json +498 -1
  275. package/locales/sv-SE.json +502 -1
  276. package/locales/tr-TR.json +498 -1
  277. package/locales/uk-UA.json +498 -1
  278. package/locales/vi-VN.json +502 -1
  279. package/locales/zh-CN.json +490 -1
  280. package/locales/zh-TW.json +490 -1
  281. package/package.json +24 -4
  282. package/src/BottomSheet/BottomSheet.doc.mjs +8 -1
  283. package/src/BottomSheet/BottomSheet.spec.md +46 -20
  284. package/src/BottomSheet/BottomSheet.test.tsx +6 -3
  285. package/src/BottomSheet/BottomSheet.tsx +3 -1
  286. package/src/BottomSheet/BottomSheetKeyboard.test.tsx +195 -0
  287. package/src/BottomSheet/BottomSheetPanel.test.tsx +11 -1
  288. package/src/BottomSheet/BottomSheetPanel.tsx +49 -16
  289. package/src/BottomSheet/__tests__/BottomSheetKeyboard.a11y.browser.spec.ts +344 -0
  290. package/src/Button/Button.doc.mjs +5 -5
  291. package/src/Button/Button.tsx +15 -13
  292. package/src/Button/__tests__/Button.a11y.chromium.spec.ts +17 -1
  293. package/src/Button/__tests__/Button.a11y.known-failures.ts +0 -29
  294. package/src/Button/__tests__/Button.a11y.renders.tsx +9 -2
  295. package/src/Button/__tests__/Button.a11y.states.ts +9 -0
  296. package/src/Chat/ChatComposer.doc.mjs +14 -0
  297. package/src/Chat/ChatComposer.spec.md +230 -0
  298. package/src/Chat/ChatComposer.test.tsx +57 -0
  299. package/src/Chat/ChatComposer.tsx +21 -9
  300. package/src/Chat/ChatComposerDrawer.doc.mjs +63 -5
  301. package/src/Chat/ChatComposerDrawer.spec.md +267 -0
  302. package/src/Chat/ChatComposerDrawer.test.tsx +81 -12
  303. package/src/Chat/ChatComposerDrawer.tsx +38 -9
  304. package/src/Chat/ChatComposerInput.doc.mjs +114 -26
  305. package/src/Chat/ChatComposerInput.spec.md +185 -0
  306. package/src/Chat/ChatComposerInput.test.tsx +113 -3
  307. package/src/Chat/ChatComposerInput.tsx +78 -13
  308. package/src/Chat/ChatComposerTokenElement.spec.md +152 -0
  309. package/src/Chat/ChatComposerTokenElement.test.tsx +85 -0
  310. package/src/Chat/ChatDictationButton.doc.mjs +3 -3
  311. package/src/Chat/ChatDictationButton.spec.md +160 -0
  312. package/src/Chat/ChatDictationButton.test.tsx +92 -27
  313. package/src/Chat/ChatDictationButton.tsx +8 -6
  314. package/src/Chat/ChatLayout.doc.mjs +12 -4
  315. package/src/Chat/ChatLayout.spec.md +271 -0
  316. package/src/Chat/ChatLayout.test.tsx +28 -0
  317. package/src/Chat/ChatLayout.tsx +19 -0
  318. package/src/Chat/ChatLayoutScrollButton.spec.md +338 -0
  319. package/src/Chat/ChatLayoutScrollButton.test.tsx +31 -0
  320. package/src/Chat/ChatLayoutScrollButton.tsx +31 -14
  321. package/src/Chat/ChatMessageBubble.doc.mjs +28 -15
  322. package/src/Chat/ChatMessageBubble.spec.md +263 -0
  323. package/src/Chat/ChatMessageBubble.test.tsx +93 -0
  324. package/src/Chat/ChatMessageBubble.tsx +8 -3
  325. package/src/Chat/ChatMessageList.spec.md +222 -0
  326. package/src/Chat/ChatMessageList.test.tsx +64 -2
  327. package/src/Chat/ChatMessageList.tsx +4 -3
  328. package/src/Chat/ChatMessageMetadata.doc.mjs +57 -3
  329. package/src/Chat/ChatMessageMetadata.spec.md +220 -0
  330. package/src/Chat/ChatMessageMetadata.test.tsx +34 -0
  331. package/src/Chat/ChatMessageMetadata.tsx +13 -13
  332. package/src/Chat/ChatSendButton.test.tsx +1 -4
  333. package/src/Chat/ChatSendButton.tsx +1 -1
  334. package/src/Chat/__tests__/ChatLayoutScrollButton.a11y.chromium.spec.ts +514 -0
  335. package/src/Chat/__tests__/ChatLayoutScrollButtonSurface.a11y.chromium.spec.ts +740 -0
  336. package/src/Chat/__tests__/ChatMessageBubble.a11y.chromium.spec.ts +457 -0
  337. package/src/Chat/__tests__/ChatMessageBubble.auditProvenance.test.ts +91 -0
  338. package/src/Chat/__tests__/ChatMessageBubble.auditProvenance.ts +91 -0
  339. package/src/Chat/__tests__/ChatMessageList.a11y.chromium.spec.ts +456 -0
  340. package/src/Chat/__tests__/ChatMessageMetadata.a11y.chromium.spec.ts +413 -0
  341. package/src/Chat/index.ts +1 -0
  342. package/src/CheckboxInput/CheckboxInput.doc.mjs +11 -0
  343. package/src/CheckboxInput/CheckboxInput.test.tsx +34 -0
  344. package/src/CheckboxInput/CheckboxInput.tsx +21 -1
  345. package/src/Citation/Citation.doc.mjs +2 -2
  346. package/src/Citation/Citation.test.tsx +17 -0
  347. package/src/Citation/Citation.tsx +4 -2
  348. package/src/ClickableCard/ClickableCard.doc.mjs +2 -2
  349. package/src/ClickableCard/ClickableCard.test.tsx +102 -5
  350. package/src/CodeBlock/highlightRanges.test.ts +143 -1
  351. package/src/CodeBlock/highlightRanges.ts +15 -5
  352. package/src/Collapsible/Collapsible.doc.mjs +11 -0
  353. package/src/Collapsible/Collapsible.test.tsx +21 -0
  354. package/src/Collapsible/Collapsible.tsx +5 -0
  355. package/src/CommandPalette/CommandPaletteInput.tsx +6 -1
  356. package/src/ComplexSelector/ComplexSelector.tsx +6 -1
  357. package/src/DateInput/DateInput.tsx +6 -1
  358. package/src/DateInput/NativeDateField.tsx +24 -8
  359. package/src/DateInput/TouchDateField.tsx +6 -2
  360. package/src/DateRangeInput/DateRangeInput.tsx +6 -1
  361. package/src/DateTimeInput/DateTimeInput.tsx +6 -1
  362. package/src/DateTimeInput/TouchDateTimeField.tsx +6 -1
  363. package/src/DateTimeInput/nativePickerSegmentStyles.ts +24 -8
  364. package/src/Dialog/Dialog.doc.mjs +2 -0
  365. package/src/Dialog/DialogHeader.doc.mjs +22 -0
  366. package/src/Dialog/DialogHeader.test.tsx +167 -3
  367. package/src/Dialog/DialogHeader.tsx +37 -6
  368. package/src/Dialog/modules/DialogHeader.spec.md +101 -42
  369. package/src/DropdownMenu/DropdownMenu.test.tsx +5 -2
  370. package/src/Field/InputClearButton.test.tsx +17 -3
  371. package/src/Field/InputClearButton.tsx +5 -2
  372. package/src/Field/PanelSearchInput.tsx +5 -2
  373. package/src/FieldStatus/__tests__/StatusMessage.a11y.states.ts +1 -1
  374. package/src/FileInput/FileInput.tsx +11 -2
  375. package/src/Icon/Icon.doc.mjs +7 -5
  376. package/src/Icon/Icon.spec.md +81 -40
  377. package/src/Icon/Icon.test.tsx +6 -3
  378. package/src/Icon/Icon.tsx +20 -68
  379. package/src/Icon/IconDefaultSizeContext.ts +23 -0
  380. package/src/Icon/IconSize.stylex.ts +70 -0
  381. package/src/IconButton/IconButton.doc.mjs +3 -3
  382. package/src/IconButton/IconButton.test.tsx +40 -0
  383. package/src/Item/Item.doc.mjs +6 -2
  384. package/src/Item/Item.tsx +9 -3
  385. package/src/Kbd/Kbd.doc.mjs +3 -3
  386. package/src/Kbd/Kbd.test.tsx +57 -1
  387. package/src/Kbd/Kbd.tsx +57 -37
  388. package/src/Layout/LayoutFooter.doc.mjs +17 -0
  389. package/src/Link/Link.doc.mjs +17 -4
  390. package/src/Link/Link.test.tsx +24 -0
  391. package/src/Link/Link.tsx +5 -0
  392. package/src/Link/LinkProvider.doc.mjs +2 -2
  393. package/src/Link/__tests__/Link.navigation.a11y.chromium.spec.ts +261 -0
  394. package/src/Link/useLinkComponent.test.tsx +333 -0
  395. package/src/Link/useLinkComponent.ts +29 -57
  396. package/src/List/List.doc.mjs +6 -0
  397. package/src/List/List.spec.md +200 -0
  398. package/src/List/List.test.tsx +117 -1
  399. package/src/List/List.tsx +22 -2
  400. package/src/List/ListContext.tsx +3 -1
  401. package/src/List/ListItem.doc.mjs +2 -2
  402. package/src/List/ListItem.source-build.test.mjs +41 -0
  403. package/src/List/ListItem.tsx +26 -2
  404. package/src/Markdown/Markdown.doc.mjs +300 -2
  405. package/src/Markdown/Markdown.fr23.bench.ts +174 -0
  406. package/src/Markdown/Markdown.fr23.perf.test.ts +249 -0
  407. package/src/Markdown/Markdown.fr23.sampling.ts +49 -0
  408. package/src/Markdown/Markdown.helpers.perf.test.ts +489 -0
  409. package/src/Markdown/Markdown.public.test.ts +130 -3
  410. package/src/Markdown/Markdown.renderBoundary.test.tsx +72 -0
  411. package/src/Markdown/Markdown.spec.md +203 -45
  412. package/src/Markdown/Markdown.test.tsx +63 -0
  413. package/src/Markdown/Markdown.tsx +414 -135
  414. package/src/Markdown/ast.test.ts +307 -0
  415. package/src/Markdown/ast.ts +337 -0
  416. package/src/Markdown/incremental.test.ts +12 -0
  417. package/src/Markdown/index.ts +37 -1
  418. package/src/Markdown/modules/remark.spec.md +269 -0
  419. package/src/Markdown/modules/softBreaks.spec.md +137 -0
  420. package/src/Markdown/parser/index.ts +64 -0
  421. package/src/Markdown/parser.public.test.ts +103 -0
  422. package/src/Markdown/parser.test.ts +20 -0
  423. package/src/Markdown/parser.ts +1180 -235
  424. package/src/Markdown/plugins/frontmatter.test.tsx +125 -0
  425. package/src/Markdown/plugins/frontmatter.ts +198 -0
  426. package/src/Markdown/plugins/index.ts +54 -0
  427. package/src/Markdown/plugins/protocol.test.tsx +1022 -0
  428. package/src/Markdown/plugins/protocol.ts +1618 -0
  429. package/src/Markdown/plugins/semanticFence.test.tsx +546 -0
  430. package/src/Markdown/plugins/semanticFence.ts +424 -0
  431. package/src/Markdown/plugins/softBreaks.test.tsx +172 -0
  432. package/src/Markdown/plugins/softBreaks.ts +169 -0
  433. package/src/Markdown/plugins/sourceDecoration.test.tsx +799 -0
  434. package/src/Markdown/plugins/sourceDecoration.ts +539 -0
  435. package/src/Markdown/plugins/textTransform.test.ts +467 -0
  436. package/src/Markdown/plugins/textTransform.ts +554 -0
  437. package/src/Markdown/remark.test.tsx +1877 -0
  438. package/src/Markdown/remark.ts +1574 -0
  439. package/src/Markdown/url.ts +36 -0
  440. package/src/Markdown/utils.ts +13 -1
  441. package/src/MetadataList/MetadataList.test.tsx +4 -2
  442. package/src/MetadataList/MetadataList.tsx +3 -3
  443. package/src/MultiSelector/MultiSelector.test.tsx +1 -35
  444. package/src/MultiSelector/__tests__/MultiSelector.listbox.a11y.test.tsx +128 -0
  445. package/src/NumberInput/NumberInput.tsx +6 -1
  446. package/src/Outline/Outline.doc.mjs +7 -5
  447. package/src/Outline/Outline.spec.md +1 -1
  448. package/src/Outline/index.ts +1 -0
  449. package/src/Outline/modules/parseOutlineFromMarkdown.spec.md +142 -0
  450. package/src/Outline/parseOutlineFromMarkdown.ts +40 -10
  451. package/src/Outline/useOutlineFromMarkdown.ts +15 -3
  452. package/src/PowerSearch/PowerSearch.doc.mjs +2 -2
  453. package/src/PowerSearch/PowerSearchEditPopover.test.tsx +150 -1
  454. package/src/PowerSearch/PowerSearchEditPopover.tsx +51 -28
  455. package/src/PowerSearch/PowerSearchValueEditor.test.tsx +51 -0
  456. package/src/PowerSearch/PowerSearchValueEditor.tsx +4 -0
  457. package/src/ProgressBar/ProgressBar.test.tsx +29 -11
  458. package/src/ProgressBar/ProgressBar.tsx +59 -72
  459. package/src/RadioList/RadioList.doc.mjs +11 -0
  460. package/src/RadioList/RadioList.test.tsx +32 -0
  461. package/src/RadioList/RadioListItem.tsx +25 -1
  462. package/src/ScrollableArea/ScrollableArea.tsx +1 -0
  463. package/src/ScrollableArea/modules/useScrollableArea.spec.md +50 -22
  464. package/src/SegmentedControl/SegmentedControl.doc.mjs +2 -2
  465. package/src/SegmentedControl/SegmentedControl.test.tsx +31 -0
  466. package/src/SegmentedControl/SegmentedControlItem.tsx +6 -9
  467. package/src/Selector/Selector.doc.mjs +2 -2
  468. package/src/Selector/Selector.spec.md +51 -49
  469. package/src/Selector/Selector.test.tsx +61 -55
  470. package/src/Selector/Selector.tsx +34 -15
  471. package/src/Selector/__tests__/Listbox.a11y.chromium.spec.ts +130 -0
  472. package/src/Selector/__tests__/Listbox.a11y.dom.ts +38 -0
  473. package/src/Selector/__tests__/Listbox.a11y.inventory.test.ts +63 -0
  474. package/src/Selector/__tests__/Listbox.a11y.known-failures.ts +37 -0
  475. package/src/Selector/__tests__/Listbox.a11y.states.ts +232 -0
  476. package/src/Selector/__tests__/Selector.a11y.chromium.spec.ts +45 -0
  477. package/src/Selector/__tests__/Selector.listbox.a11y.test.tsx +121 -0
  478. package/src/SideNav/SideNav.doc.mjs +1 -1
  479. package/src/SideNav/SideNav.test.tsx +10 -0
  480. package/src/SideNav/SideNav.tsx +14 -2
  481. package/src/Slider/Slider.doc.mjs +17 -3
  482. package/src/Slider/Slider.test.tsx +221 -0
  483. package/src/Slider/Slider.tsx +40 -9
  484. package/src/Spinner/Spinner.test.tsx +4 -0
  485. package/src/Spinner/Spinner.tsx +31 -18
  486. package/src/Switch/Switch.doc.mjs +11 -0
  487. package/src/Switch/Switch.test.tsx +16 -0
  488. package/src/Switch/Switch.tsx +28 -0
  489. package/src/TabList/Tab.tsx +35 -7
  490. package/src/TabList/TabList.doc.mjs +11 -0
  491. package/src/TabList/TabList.spec.md +204 -0
  492. package/src/TabList/TabList.test.tsx +66 -0
  493. package/src/TabList/__tests__/Tabs.a11y.known-failures.ts +1 -34
  494. package/src/Table/Table.doc.mjs +1 -1
  495. package/src/Table/Table.spec.md +54 -40
  496. package/src/Table/Table.test.tsx +83 -10
  497. package/src/Table/Table.tsx +30 -22
  498. package/src/Table/__tests__/TableScroll.a11y.chromium.spec.ts +137 -0
  499. package/src/TextArea/TextArea.tsx +6 -1
  500. package/src/TextInput/TextInput.test.tsx +20 -1
  501. package/src/TextInput/TextInput.tsx +6 -1
  502. package/src/TimeInput/TimeInput.tsx +6 -1
  503. package/src/Timer/Timer.doc.mjs +183 -0
  504. package/src/Timer/Timer.spec.md +215 -0
  505. package/src/Timer/Timer.test.tsx +302 -0
  506. package/src/Timer/Timer.tsx +258 -0
  507. package/src/Timer/index.ts +11 -0
  508. package/src/ToggleButton/ToggleButton.doc.mjs +3 -3
  509. package/src/ToggleButton/ToggleButton.test.tsx +299 -4
  510. package/src/ToggleButton/ToggleButton.tsx +26 -33
  511. package/src/ToggleButton/ToggleButtonGroup.doc.mjs +3 -3
  512. package/src/ToggleButton/__tests__/ToggleButton.a11y.chromium.spec.ts +209 -0
  513. package/src/Token/Token.doc.mjs +2 -2
  514. package/src/Tokenizer/Tokenizer.spec.md +142 -75
  515. package/src/TopNav/TopNavMenu.doc.mjs +2 -2
  516. package/src/TopNav/TopNavMenu.test.tsx +30 -0
  517. package/src/TopNav/TopNavMenu.tsx +7 -2
  518. package/src/TreeList/TreeList.doc.mjs +3 -3
  519. package/src/TreeList/TreeList.spec.md +123 -75
  520. package/src/TreeList/TreeList.test.tsx +29 -0
  521. package/src/TreeList/TreeList.tsx +3 -0
  522. package/src/TreeList/TreeListItem.tsx +15 -1
  523. package/src/TreeList/TreeListTypes.ts +11 -1
  524. package/src/Typeahead/BaseTypeahead.spec.md +4 -3
  525. package/src/Typeahead/BaseTypeahead.tsx +21 -7
  526. package/src/Typeahead/Typeahead.test.tsx +53 -0
  527. package/src/__tests__/PressedState.a11y.chromium.spec.ts +813 -0
  528. package/src/__tests__/pressState.ts +93 -0
  529. package/src/hooks/scrollKeyboardDelegation.test.ts +155 -0
  530. package/src/hooks/scrollKeyboardDelegation.ts +233 -0
  531. package/src/hooks/useClickableContainer.test.tsx +236 -0
  532. package/src/hooks/useClickableContainer.ts +20 -4
  533. package/src/hooks/useScrollableArea.doc.mjs +15 -3
  534. package/src/hooks/useScrollableArea.test.tsx +59 -1
  535. package/src/hooks/useScrollableArea.ts +34 -10
  536. package/src/index.ts +3 -0
  537. package/src/inputFontFloor.test.ts +106 -0
  538. package/src/tailwind-theme.css +0 -6
  539. package/src/theme/Theme.test.tsx +32 -0
  540. package/src/theme/declarationBoundary.test.ts +126 -0
  541. package/src/theme/declarationBoundary.ts +347 -0
  542. package/src/theme/derivedVarRegistry.test.ts +7 -0
  543. package/src/theme/derivedVarRegistry.ts +1 -0
  544. package/src/theme/generateThemeRules.test.ts +526 -1
  545. package/src/theme/generateThemeRules.ts +228 -50
  546. package/src/theme/tokenValueCompat.test.ts +4 -1
  547. package/src/utils/interactionOverlay.stylex.ts +20 -0
  548. package/src/utils/safeUrl.test.ts +88 -0
  549. package/src/utils/safeUrl.ts +85 -0
@@ -2,45 +2,79 @@
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 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
11
10
  */
12
11
 
12
+ import {
13
+ getMarkdownAstLegacyCodeLanguage,
14
+ markMarkdownAstLegacyCodeLanguage,
15
+ } from './ast';
16
+ import type {
17
+ MarkdownAstBlockContent,
18
+ MarkdownAstList,
19
+ MarkdownAstListItem,
20
+ MarkdownAstPhrasingContent,
21
+ MarkdownAstPosition,
22
+ MarkdownAstRoot,
23
+ MarkdownAstTableCell,
24
+ MarkdownAstTableRow,
25
+ } from './ast';
26
+ import {
27
+ applyMarkdownTransforms,
28
+ freezeMarkdownPluginData,
29
+ isMarkdownPluginData,
30
+ prepareMarkdownPlugins,
31
+ reportMarkdownPluginFailure,
32
+ } from './plugins/protocol';
33
+ import type {
34
+ MarkdownExtensionNode,
35
+ MarkdownExtensionsOf,
36
+ MarkdownPluginData,
37
+ MarkdownPluginEntry,
38
+ PreparedMarkdownPlugins,
39
+ PreparedSyntaxContribution,
40
+ } from './plugins/protocol';
41
+ import {isSafeMarkdownParserUrl} from './url';
42
+
13
43
  // ---------------------------------------------------------------------------
14
44
  // Types
15
45
  // ---------------------------------------------------------------------------
16
46
 
17
47
  /** Nodes returned by default and legacy parser calls. */
18
- export type InlineNode =
48
+ export type InlineNode<Extension extends MarkdownExtensionNode = never> =
19
49
  | {type: 'text'; content: string}
20
- | {type: 'bold'; children: InlineNode[]}
21
- | {type: 'italic'; children: InlineNode[]}
22
- | {type: 'strikethrough'; children: InlineNode[]}
50
+ | {type: 'bold'; children: InlineNode<Extension>[]}
51
+ | {type: 'italic'; children: InlineNode<Extension>[]}
52
+ | {type: 'strikethrough'; children: InlineNode<Extension>[]}
23
53
  | {type: 'code'; content: string}
24
- | {type: 'link'; href: string; children: InlineNode[]}
54
+ | {type: 'link'; href: string; children: InlineNode<Extension>[]}
25
55
  | {type: 'image'; src: string; alt: string}
26
56
  | {type: 'citation'; sourceId: string}
27
- | {type: 'break'};
57
+ | {type: 'break'}
58
+ | Extract<Extension, {display: 'inline'}>;
28
59
 
29
60
  /** The additional inline node returned only when parsing with `math: true`. */
30
61
  export type MathInlineNode = {type: 'math'; value: string};
31
62
 
32
63
  /** Nodes returned by an explicitly math-enabled inline parse. */
33
- export type InlineNodeWithMath =
64
+ export type InlineNodeWithMath<
65
+ Extension extends MarkdownExtensionNode = never,
66
+ > =
34
67
  | {type: 'text'; content: string}
35
- | {type: 'bold'; children: InlineNodeWithMath[]}
36
- | {type: 'italic'; children: InlineNodeWithMath[]}
37
- | {type: 'strikethrough'; children: InlineNodeWithMath[]}
68
+ | {type: 'bold'; children: InlineNodeWithMath<Extension>[]}
69
+ | {type: 'italic'; children: InlineNodeWithMath<Extension>[]}
70
+ | {type: 'strikethrough'; children: InlineNodeWithMath<Extension>[]}
38
71
  | {type: 'code'; content: string}
39
72
  | MathInlineNode
40
- | {type: 'link'; href: string; children: InlineNodeWithMath[]}
73
+ | {type: 'link'; href: string; children: InlineNodeWithMath<Extension>[]}
41
74
  | {type: 'image'; src: string; alt: string}
42
75
  | {type: 'citation'; sourceId: string}
43
- | {type: 'break'};
76
+ | {type: 'break'}
77
+ | Extract<Extension, {display: 'inline'}>;
44
78
 
45
79
  type BlockMetadata = {
46
80
  /**
@@ -50,11 +84,15 @@ type BlockMetadata = {
50
84
  range?: SourceRange;
51
85
  };
52
86
 
53
- type LegacyBlockNodeKind =
54
- | {type: 'heading'; level: 1 | 2 | 3 | 4 | 5 | 6; children: InlineNode[]}
55
- | {type: 'paragraph'; children: InlineNode[]}
87
+ type LegacyBlockNodeKind<Extension extends MarkdownExtensionNode = never> =
88
+ | {
89
+ type: 'heading';
90
+ level: 1 | 2 | 3 | 4 | 5 | 6;
91
+ children: InlineNode<Extension>[];
92
+ }
93
+ | {type: 'paragraph'; children: InlineNode<Extension>[]}
56
94
  | {type: 'codeblock'; language: string; content: string}
57
- | {type: 'blockquote'; children: BlockNode[]}
95
+ | {type: 'blockquote'; children: BlockNode<Extension>[]}
58
96
  | {
59
97
  type: 'list';
60
98
  ordered: boolean;
@@ -62,33 +100,35 @@ type LegacyBlockNodeKind =
62
100
  /** Ordered-list marker delimiter ('.' or ')'). Undefined for bullets. */
63
101
  delimiter?: '.' | ')';
64
102
  loose?: boolean;
65
- items: ListItemNode[];
103
+ items: ListItemNode<Extension>[];
66
104
  }
67
105
  | {
68
106
  type: 'table';
69
- headers: TableCellNode[];
107
+ headers: TableCellNode<Extension>[];
70
108
  alignments: TableAlignment[];
71
- rows: TableCellNode[][];
109
+ rows: TableCellNode<Extension>[][];
72
110
  }
73
111
  | {type: 'hr'}
74
- | {type: 'image'; src: string; alt: string};
112
+ | {type: 'image'; src: string; alt: string}
113
+ | Extract<Extension, {display: 'block'}>;
75
114
 
76
115
  /** Blocks returned by default and legacy parser calls. */
77
- export type BlockNode = LegacyBlockNodeKind & BlockMetadata;
116
+ export type BlockNode<Extension extends MarkdownExtensionNode = never> =
117
+ LegacyBlockNodeKind<Extension> & BlockMetadata;
78
118
 
79
119
  /** The additional block returned only when parsing with `math: true`. */
80
120
  export type MathBlockNode = {type: 'math'; value: string} & BlockMetadata;
81
121
 
82
- type MathEnabledBlockNodeKind =
122
+ type MathEnabledBlockNodeKind<Extension extends MarkdownExtensionNode = never> =
83
123
  | {
84
124
  type: 'heading';
85
125
  level: 1 | 2 | 3 | 4 | 5 | 6;
86
- children: InlineNodeWithMath[];
126
+ children: InlineNodeWithMath<Extension>[];
87
127
  }
88
- | {type: 'paragraph'; children: InlineNodeWithMath[]}
128
+ | {type: 'paragraph'; children: InlineNodeWithMath<Extension>[]}
89
129
  | {type: 'codeblock'; language: string; content: string}
90
130
  | MathBlockNode
91
- | {type: 'blockquote'; children: BlockNodeWithMath[]}
131
+ | {type: 'blockquote'; children: BlockNodeWithMath<Extension>[]}
92
132
  | {
93
133
  type: 'list';
94
134
  ordered: boolean;
@@ -96,19 +136,21 @@ type MathEnabledBlockNodeKind =
96
136
  /** Ordered-list marker delimiter ('.' or ')'). Undefined for bullets. */
97
137
  delimiter?: '.' | ')';
98
138
  loose?: boolean;
99
- items: ListItemNodeWithMath[];
139
+ items: ListItemNodeWithMath<Extension>[];
100
140
  }
101
141
  | {
102
142
  type: 'table';
103
- headers: TableCellNodeWithMath[];
143
+ headers: TableCellNodeWithMath<Extension>[];
104
144
  alignments: TableAlignment[];
105
- rows: TableCellNodeWithMath[][];
145
+ rows: TableCellNodeWithMath<Extension>[][];
106
146
  }
107
147
  | {type: 'hr'}
108
- | {type: 'image'; src: string; alt: string};
148
+ | {type: 'image'; src: string; alt: string}
149
+ | Extract<Extension, {display: 'block'}>;
109
150
 
110
151
  /** Blocks returned by an explicitly math-enabled block parse. */
111
- export type BlockNodeWithMath = MathEnabledBlockNodeKind & BlockMetadata;
152
+ export type BlockNodeWithMath<Extension extends MarkdownExtensionNode = never> =
153
+ MathEnabledBlockNodeKind<Extension> & BlockMetadata;
112
154
 
113
155
  /**
114
156
  * Where a block sits in the source string handed to `parseMarkdown`:
@@ -121,15 +163,288 @@ export type BlockNodeWithMath = MathEnabledBlockNodeKind & BlockMetadata;
121
163
  */
122
164
  export type SourceRange = {readonly start: number; readonly end: number};
123
165
 
124
- export type ListItemNode = {checked?: boolean; children: BlockNode[]};
125
- type ListItemNodeWithMath = {
166
+ export type ListItemNode<Extension extends MarkdownExtensionNode = never> = {
167
+ checked?: boolean;
168
+ children: BlockNode<Extension>[];
169
+ };
170
+ type ListItemNodeWithMath<Extension extends MarkdownExtensionNode = never> = {
126
171
  checked?: boolean;
127
- children: BlockNodeWithMath[];
172
+ children: BlockNodeWithMath<Extension>[];
173
+ };
174
+ export type TableCellNode<Extension extends MarkdownExtensionNode = never> = {
175
+ children: InlineNode<Extension>[];
176
+ };
177
+ type TableCellNodeWithMath<Extension extends MarkdownExtensionNode = never> = {
178
+ children: InlineNodeWithMath<Extension>[];
128
179
  };
129
- export type TableCellNode = {children: InlineNode[]};
130
- type TableCellNodeWithMath = {children: InlineNodeWithMath[]};
131
180
  export type TableAlignment = 'left' | 'center' | 'right' | null;
132
181
 
182
+ type RuntimeExtensionNode =
183
+ | MarkdownExtensionNode<string, string, MarkdownPluginData, 'inline'>
184
+ | MarkdownExtensionNode<string, string, MarkdownPluginData, 'block'>;
185
+ type RuntimeInlineNode = InlineNodeWithMath<RuntimeExtensionNode>;
186
+ type RuntimeBlockNode = BlockNodeWithMath<RuntimeExtensionNode>;
187
+
188
+ type LegacyProjectionCache = WeakMap<object, object>;
189
+
190
+ function projectRange(
191
+ position: MarkdownAstPosition | undefined,
192
+ ): BlockMetadata {
193
+ const start = position?.start.offset;
194
+ const end = position?.end.offset;
195
+ return start == null || end == null ? {} : {range: {start, end}};
196
+ }
197
+
198
+ /**
199
+ * Canonical nodes always carry Core-authored positions; the released block
200
+ * shape exposes them as `range` only when the caller passed `sourceRanges`,
201
+ * so this projection stays byte-for-byte what it has always returned.
202
+ *
203
+ * An extension node is the one kind the projection passes through instead of
204
+ * rebuilding field by field, so it is the one kind that could carry a
205
+ * canonical `position` out into released output. Strip it unless the caller
206
+ * asked for provenance, in which case the node keeps exactly the `position`
207
+ * it has always exposed.
208
+ */
209
+ function projectExtensionNode<Node extends RuntimeExtensionNode>(
210
+ node: Node,
211
+ withRanges: boolean,
212
+ ): Node {
213
+ if (withRanges || node.position === undefined) {
214
+ return node;
215
+ }
216
+ const {position: _canonicalPosition, ...released} = node;
217
+ return released as Node;
218
+ }
219
+
220
+ function projectInlineNode(
221
+ node: MarkdownAstPhrasingContent<RuntimeExtensionNode>,
222
+ cache: LegacyProjectionCache,
223
+ withRanges: boolean,
224
+ ): RuntimeInlineNode {
225
+ const cached = cache.get(node);
226
+ if (cached != null) {
227
+ return cached as RuntimeInlineNode;
228
+ }
229
+ let projected: RuntimeInlineNode;
230
+ switch (node.type) {
231
+ case 'text':
232
+ projected = {type: 'text', content: node.value};
233
+ break;
234
+ case 'strong':
235
+ projected = {
236
+ type: 'bold',
237
+ children: node.children.map(child =>
238
+ projectInlineNode(child, cache, withRanges),
239
+ ),
240
+ };
241
+ break;
242
+ case 'emphasis':
243
+ projected = {
244
+ type: 'italic',
245
+ children: node.children.map(child =>
246
+ projectInlineNode(child, cache, withRanges),
247
+ ),
248
+ };
249
+ break;
250
+ case 'delete':
251
+ projected = {
252
+ type: 'strikethrough',
253
+ children: node.children.map(child =>
254
+ projectInlineNode(child, cache, withRanges),
255
+ ),
256
+ };
257
+ break;
258
+ case 'inlineCode':
259
+ projected = {type: 'code', content: node.value};
260
+ break;
261
+ case 'inlineMath':
262
+ projected = {type: 'math', value: node.value};
263
+ break;
264
+ case 'link':
265
+ projected = {
266
+ type: 'link',
267
+ href: node.url,
268
+ children: node.children.map(child =>
269
+ projectInlineNode(child, cache, withRanges),
270
+ ),
271
+ };
272
+ break;
273
+ case 'image':
274
+ projected = {type: 'image', src: node.url, alt: node.alt};
275
+ break;
276
+ case 'citation':
277
+ projected = {type: 'citation', sourceId: node.sourceId};
278
+ break;
279
+ case 'break':
280
+ projected = {type: 'break'};
281
+ break;
282
+ case 'extension':
283
+ projected = projectExtensionNode(node, withRanges);
284
+ break;
285
+ }
286
+ cache.set(node, projected);
287
+ return projected;
288
+ }
289
+
290
+ function projectTableCell(
291
+ node: MarkdownAstTableCell<RuntimeExtensionNode>,
292
+ cache: LegacyProjectionCache,
293
+ withRanges: boolean,
294
+ ): TableCellNodeWithMath<RuntimeExtensionNode> {
295
+ const cached = cache.get(node);
296
+ if (cached != null) {
297
+ return cached as TableCellNodeWithMath<RuntimeExtensionNode>;
298
+ }
299
+ const projected = {
300
+ children: node.children.map(child =>
301
+ projectInlineNode(child, cache, withRanges),
302
+ ),
303
+ };
304
+ cache.set(node, projected);
305
+ return projected;
306
+ }
307
+
308
+ function projectListItem(
309
+ node: MarkdownAstListItem<RuntimeExtensionNode>,
310
+ cache: LegacyProjectionCache,
311
+ withRanges: boolean,
312
+ ): ListItemNodeWithMath<RuntimeExtensionNode> {
313
+ const cached = cache.get(node);
314
+ if (cached != null) {
315
+ return cached as ListItemNodeWithMath<RuntimeExtensionNode>;
316
+ }
317
+ const projected = {
318
+ checked: node.checked,
319
+ children: node.children.map(child =>
320
+ projectBlockNode(child, cache, withRanges),
321
+ ),
322
+ };
323
+ cache.set(node, projected);
324
+ return projected;
325
+ }
326
+
327
+ function projectBlockNode(
328
+ node: MarkdownAstBlockContent<RuntimeExtensionNode>,
329
+ cache: LegacyProjectionCache,
330
+ withRanges: boolean,
331
+ ): RuntimeBlockNode {
332
+ const cached = cache.get(node);
333
+ if (cached != null) {
334
+ return cached as RuntimeBlockNode;
335
+ }
336
+ const metadata = withRanges ? projectRange(node.position) : {};
337
+ let projected: RuntimeBlockNode;
338
+ switch (node.type) {
339
+ case 'heading':
340
+ projected = {
341
+ type: 'heading',
342
+ level: node.depth,
343
+ children: node.children.map(child =>
344
+ projectInlineNode(child, cache, withRanges),
345
+ ),
346
+ ...metadata,
347
+ };
348
+ break;
349
+ case 'paragraph':
350
+ projected = {
351
+ type: 'paragraph',
352
+ children: node.children.map(child =>
353
+ projectInlineNode(child, cache, withRanges),
354
+ ),
355
+ ...metadata,
356
+ };
357
+ break;
358
+ case 'code':
359
+ projected = {
360
+ type: 'codeblock',
361
+ language: getMarkdownAstLegacyCodeLanguage(node) ?? 'plaintext',
362
+ content: node.value,
363
+ ...metadata,
364
+ };
365
+ break;
366
+ case 'math':
367
+ projected = {type: 'math', value: node.value, ...metadata};
368
+ break;
369
+ case 'blockquote':
370
+ projected = {
371
+ type: 'blockquote',
372
+ children: node.children.map(child =>
373
+ projectBlockNode(child, cache, withRanges),
374
+ ),
375
+ ...metadata,
376
+ };
377
+ break;
378
+ case 'list':
379
+ projected = {
380
+ type: 'list',
381
+ ordered: node.ordered,
382
+ start: node.start,
383
+ delimiter: node.delimiter,
384
+ loose: node.spread,
385
+ items: node.children.map(item =>
386
+ projectListItem(item, cache, withRanges),
387
+ ),
388
+ ...metadata,
389
+ };
390
+ break;
391
+ case 'table': {
392
+ const [header = {type: 'tableRow' as const, children: []}, ...rows] =
393
+ node.children;
394
+ projected = {
395
+ type: 'table',
396
+ headers: header.children.map(cell =>
397
+ projectTableCell(cell, cache, withRanges),
398
+ ),
399
+ alignments: [...node.align],
400
+ rows: rows.map(row =>
401
+ row.children.map(cell => projectTableCell(cell, cache, withRanges)),
402
+ ),
403
+ ...metadata,
404
+ };
405
+ break;
406
+ }
407
+ case 'thematicBreak':
408
+ projected = {type: 'hr', ...metadata};
409
+ break;
410
+ case 'image':
411
+ projected = {type: 'image', alt: node.alt, src: node.url, ...metadata};
412
+ break;
413
+ case 'extension':
414
+ projected = projectExtensionNode(node, withRanges);
415
+ break;
416
+ }
417
+ cache.set(node, projected);
418
+ return projected;
419
+ }
420
+
421
+ function projectInlineNodes(
422
+ nodes: ReadonlyArray<MarkdownAstPhrasingContent<RuntimeExtensionNode>>,
423
+ withRanges: boolean,
424
+ cache: LegacyProjectionCache = new WeakMap(),
425
+ ): RuntimeInlineNode[] {
426
+ return nodes.map(node => projectInlineNode(node, cache, withRanges));
427
+ }
428
+
429
+ function projectMarkdownRoot(
430
+ root: MarkdownAstRoot<RuntimeExtensionNode>,
431
+ withRanges: boolean,
432
+ cache: LegacyProjectionCache = new WeakMap(),
433
+ ): RuntimeBlockNode[] {
434
+ return root.children.map(node => projectBlockNode(node, cache, withRanges));
435
+ }
436
+
437
+ /** Whether a caller asked for the released `range` field on its blocks. */
438
+ function wantsLegacyRanges(
439
+ arg: ReadonlySet<string> | RuntimeParseOptions | undefined,
440
+ ): boolean {
441
+ return (
442
+ arg != null &&
443
+ typeof (arg as {has?: unknown}).has !== 'function' &&
444
+ (arg as RuntimeParseOptions).sourceRanges === true
445
+ );
446
+ }
447
+
133
448
  // ---------------------------------------------------------------------------
134
449
  // Parse options
135
450
  // ---------------------------------------------------------------------------
@@ -141,10 +456,14 @@ export type TableAlignment = 'left' | 'center' | 'right' | null;
141
456
  * `parseMarkdownIncremental` functions also accept the legacy
142
457
  * `ReadonlySet<string>` shape as the second argument.
143
458
  */
144
- type CommonParseOptions = {
459
+ type CommonParseOptions<
460
+ Plugins extends ReadonlyArray<MarkdownPluginEntry> = readonly [],
461
+ > = {
145
462
  /** Set of citation source ids — `[id]` / `【id】` markers in this set
146
463
  * become citation nodes instead of plain text / links. */
147
464
  sourceIds?: ReadonlySet<string>;
465
+ /** Ordered opt-in Markdown extensions. */
466
+ plugins?: Plugins;
148
467
  /**
149
468
  * Autolink mode. When set to `'gfm'`, the parser turns bare
150
469
  * `https?://…` / `www.…` URLs, `<URL>` / `<email>` angle-bracket
@@ -170,22 +489,56 @@ type CommonParseOptions = {
170
489
  };
171
490
 
172
491
  /** Options for default and legacy parser results. */
173
- export type ParseOptions = CommonParseOptions & {math?: false | undefined};
492
+ export type ParseOptions<
493
+ Plugins extends ReadonlyArray<MarkdownPluginEntry> = readonly [],
494
+ > = CommonParseOptions<Plugins> & {math?: false | undefined};
174
495
 
175
496
  /**
176
497
  * Options that explicitly parse `$…$` and `$$…$$` into math-enabled result
177
498
  * unions. Keeping this separate prevents a legacy `ParseOptions` annotation
178
499
  * from silently widening an exhaustive node switch.
179
500
  */
180
- export type MathParseOptions = CommonParseOptions & {math: true};
501
+ export type MathParseOptions<
502
+ Plugins extends ReadonlyArray<MarkdownPluginEntry> = readonly [],
503
+ > = CommonParseOptions<Plugins> & {math: true};
504
+
505
+ export type IncrementalParseOptions<
506
+ Plugins extends ReadonlyArray<MarkdownPluginEntry> = readonly [],
507
+ > = ParseOptions<Plugins> & {
508
+ /** False while more source may arrive; true for the terminal snapshot. */
509
+ readonly isFinal?: boolean;
510
+ };
181
511
 
182
- type RuntimeParseOptions = CommonParseOptions & {math?: boolean};
512
+ export type IncrementalMathParseOptions<
513
+ Plugins extends ReadonlyArray<MarkdownPluginEntry> = readonly [],
514
+ > = MathParseOptions<Plugins> & {
515
+ /** False while more source may arrive; true for the terminal snapshot. */
516
+ readonly isFinal?: boolean;
517
+ };
518
+
519
+ type RuntimeParseOptions = CommonParseOptions<
520
+ ReadonlyArray<MarkdownPluginEntry>
521
+ > & {
522
+ math?: boolean;
523
+ isFinal?: boolean;
524
+ };
183
525
 
184
526
  type ResolvedOptions = {
185
527
  readonly sourceIds: ReadonlySet<string> | undefined;
186
528
  readonly autolink: 'gfm' | undefined;
187
529
  readonly math?: boolean;
530
+ /** Whether the caller asked for the released `range` projection. */
188
531
  readonly sourceRanges?: boolean;
532
+ /**
533
+ * Whether this parse authors canonical `position` offsets on its blocks.
534
+ * Internal only, and independent of `sourceRanges`: the canonical tree
535
+ * transforms observe always carries provenance, while `sourceRanges` alone
536
+ * decides whether the released projection exposes it as `range`.
537
+ */
538
+ readonly astPositions?: boolean;
539
+ readonly plugins?: PreparedMarkdownPlugins;
540
+ readonly isFinal: boolean;
541
+ readonly allowBlockSyntax?: boolean;
189
542
  /**
190
543
  * Offset of this parse's input within the document the ranges are reported
191
544
  * against. Internal only — the incremental parser parses slices and needs
@@ -201,13 +554,42 @@ type ResolvedOptions = {
201
554
  readonly linkDefs?: ReadonlyMap<string, string>;
202
555
  };
203
556
 
204
- const EMPTY_OPTS: ResolvedOptions = {sourceIds: undefined, autolink: undefined};
557
+ /**
558
+ * Every resolved options object is built here, so all of them share one key
559
+ * set in one order — and therefore one hidden class. The parser reads these
560
+ * fields in its hottest loops; letting an omitted-options parse and an
561
+ * options-bag parse produce differently-shaped objects makes those reads
562
+ * polymorphic in any process that does both, which is exactly what a caller
563
+ * comparing an empty pipeline against a configured one does.
564
+ */
565
+ function makeResolvedOptions(
566
+ fields: Partial<ResolvedOptions> & {readonly isFinal: boolean},
567
+ ): ResolvedOptions {
568
+ return {
569
+ sourceIds: fields.sourceIds,
570
+ autolink: fields.autolink,
571
+ math: fields.math,
572
+ sourceRanges: fields.sourceRanges,
573
+ astPositions: fields.astPositions ?? true,
574
+ plugins: fields.plugins,
575
+ isFinal: fields.isFinal,
576
+ allowBlockSyntax: fields.allowBlockSyntax ?? true,
577
+ baseOffset: fields.baseOffset,
578
+ linkDefs: fields.linkDefs,
579
+ };
580
+ }
581
+
582
+ const EMPTY_OPTS: ResolvedOptions = makeResolvedOptions({isFinal: true});
583
+ const EMPTY_INCREMENTAL_OPTS: ResolvedOptions = makeResolvedOptions({
584
+ isFinal: false,
585
+ });
205
586
 
206
587
  function resolveOptions(
207
588
  arg: ReadonlySet<string> | RuntimeParseOptions | undefined,
589
+ incremental = false,
208
590
  ): ResolvedOptions {
209
591
  if (arg == null) {
210
- return EMPTY_OPTS;
592
+ return incremental ? EMPTY_INCREMENTAL_OPTS : EMPTY_OPTS;
211
593
  }
212
594
  // Duck-type the legacy `ReadonlySet<string>` form: any object whose
213
595
  // `.has` is callable is treated as the legacy sourceIds set. This is
@@ -215,15 +597,23 @@ function resolveOptions(
215
597
  // or polyfilled `ReadonlySet` implementations as a `ParseOptions` bag
216
598
  // and silently lose citation resolution.
217
599
  if (typeof (arg as {has?: unknown}).has === 'function') {
218
- return {sourceIds: arg as ReadonlySet<string>, autolink: undefined};
600
+ return makeResolvedOptions({
601
+ sourceIds: arg as ReadonlySet<string>,
602
+ isFinal: !incremental,
603
+ });
219
604
  }
220
605
  const opts = arg as RuntimeParseOptions;
221
- return {
606
+ return makeResolvedOptions({
222
607
  sourceIds: opts.sourceIds,
223
608
  autolink: opts.autolink,
224
609
  math: opts.math === true ? true : undefined,
225
610
  sourceRanges: opts.sourceRanges,
226
- };
611
+ plugins:
612
+ opts.plugins != null && opts.plugins.length > 0
613
+ ? prepareMarkdownPlugins(opts.plugins)
614
+ : undefined,
615
+ isFinal: incremental ? opts.isFinal === true : true,
616
+ });
227
617
  }
228
618
 
229
619
  // ---------------------------------------------------------------------------
@@ -520,6 +910,15 @@ function extractLinkDefinitions(
520
910
  return {defs, cleaned, lineMap};
521
911
  }
522
912
 
913
+ /** Order-independent signature of a citation-source set, for cache checks. */
914
+ function sourceIdsSignature(
915
+ sourceIds: ReadonlySet<string> | undefined,
916
+ ): string {
917
+ return sourceIds == null || sourceIds.size === 0
918
+ ? ''
919
+ : [...sourceIds].sort().join('\u0000');
920
+ }
921
+
523
922
  /** Order-independent signature of a link-definition set, for cache checks. */
524
923
  function linkDefsSignature(defs: ReadonlyMap<string, string>): string {
525
924
  if (defs.size === 0) {
@@ -537,12 +936,19 @@ function linkDefsSignature(defs: ReadonlyMap<string, string>): string {
537
936
  * Returns the node plus the index just past the reference, or null when it is
538
937
  * not a resolvable reference (caller falls through to literal handling).
539
938
  */
939
+ function protectedInlineOptions(opts: ResolvedOptions): ResolvedOptions {
940
+ return opts.plugins == null ? opts : {...opts, plugins: undefined};
941
+ }
942
+
540
943
  function matchReferenceLink(
541
944
  text: string,
542
945
  start: number,
543
946
  linkDefs: ReadonlyMap<string, string>,
544
947
  opts: ResolvedOptions,
545
- ): {node: InlineNodeWithMath; end: number} | null {
948
+ ): {
949
+ node: MarkdownAstPhrasingContent<RuntimeExtensionNode>;
950
+ end: number;
951
+ } | null {
546
952
  const textClose = text.indexOf(']', start + 1);
547
953
  if (textClose === -1) {
548
954
  return null;
@@ -558,9 +964,13 @@ function matchReferenceLink(
558
964
  // matches nothing.
559
965
  const label = rawLabel === '' ? linkText : rawLabel;
560
966
  const href = linkDefs.get(normalizeLinkLabel(label));
561
- if (href != null && isSafeUrl(href)) {
967
+ if (href != null && isSafeMarkdownParserUrl(href)) {
562
968
  return {
563
- node: {type: 'link', href, children: parseInlineImpl(linkText, opts)},
969
+ node: {
970
+ type: 'link',
971
+ url: href,
972
+ children: parseInlineImpl(linkText, protectedInlineOptions(opts)),
973
+ },
564
974
  end: labelClose + 1,
565
975
  };
566
976
  }
@@ -573,11 +983,15 @@ function matchReferenceLink(
573
983
  return null;
574
984
  }
575
985
  const href = linkDefs.get(normalizeLinkLabel(linkText));
576
- if (href == null || !isSafeUrl(href)) {
986
+ if (href == null || !isSafeMarkdownParserUrl(href)) {
577
987
  return null;
578
988
  }
579
989
  return {
580
- node: {type: 'link', href, children: parseInlineImpl(linkText, opts)},
990
+ node: {
991
+ type: 'link',
992
+ url: href,
993
+ children: parseInlineImpl(linkText, protectedInlineOptions(opts)),
994
+ },
581
995
  end: textClose + 1,
582
996
  };
583
997
  }
@@ -587,7 +1001,10 @@ function matchReferenceImage(
587
1001
  text: string,
588
1002
  start: number,
589
1003
  linkDefs: ReadonlyMap<string, string>,
590
- ): {node: InlineNodeWithMath; end: number} | null {
1004
+ ): {
1005
+ node: MarkdownAstPhrasingContent<RuntimeExtensionNode>;
1006
+ end: number;
1007
+ } | null {
591
1008
  const altClose = text.indexOf(']', start + 2);
592
1009
  if (altClose === -1) {
593
1010
  return null;
@@ -599,8 +1016,8 @@ function matchReferenceImage(
599
1016
  const rawLabel = text.slice(altClose + 2, labelClose);
600
1017
  const label = rawLabel === '' ? alt : rawLabel;
601
1018
  const src = linkDefs.get(normalizeLinkLabel(label));
602
- if (src != null && isSafeUrl(src)) {
603
- return {node: {type: 'image', src, alt}, end: labelClose + 1};
1019
+ if (src != null && isSafeMarkdownParserUrl(src)) {
1020
+ return {node: {type: 'image', url: src, alt}, end: labelClose + 1};
604
1021
  }
605
1022
  // No match — fall back to a shortcut `![alt]`.
606
1023
  }
@@ -609,10 +1026,10 @@ function matchReferenceImage(
609
1026
  return null;
610
1027
  }
611
1028
  const src = linkDefs.get(normalizeLinkLabel(alt));
612
- if (src == null || !isSafeUrl(src)) {
1029
+ if (src == null || !isSafeMarkdownParserUrl(src)) {
613
1030
  return null;
614
1031
  }
615
- return {node: {type: 'image', src, alt}, end: altClose + 1};
1032
+ return {node: {type: 'image', url: src, alt}, end: altClose + 1};
616
1033
  }
617
1034
 
618
1035
  // ---------------------------------------------------------------------------
@@ -701,30 +1118,6 @@ function findInlineMathEnd(text: string, start: number): number {
701
1118
  }
702
1119
 
703
1120
  // ---------------------------------------------------------------------------
704
- // URL scheme sanitization
705
- // ---------------------------------------------------------------------------
706
-
707
- /**
708
- * Reject URLs with dangerous schemes (javascript:, vbscript:, data:) that
709
- * could execute arbitrary code when rendered as link hrefs or image srcs.
710
- * Returns true if the URL is safe to use, false otherwise.
711
- */
712
- function isSafeUrl(url: string): boolean {
713
- // Trim and collapse whitespace/control chars that browsers tolerate but
714
- // could bypass a naive prefix check (e.g. "java\nscript:alert(1)").
715
- // eslint-disable-next-line no-control-regex -- control chars are the bypass
716
- const normalized = url.replace(/[\x00-\x1f\x7f]/g, '').trim();
717
- const lower = normalized.toLowerCase();
718
- if (
719
- lower.startsWith('javascript:') ||
720
- lower.startsWith('vbscript:') ||
721
- lower.startsWith('data:text/html')
722
- ) {
723
- return false;
724
- }
725
- return true;
726
- }
727
-
728
1121
  // ---------------------------------------------------------------------------
729
1122
  // Inline parser
730
1123
  // ---------------------------------------------------------------------------
@@ -778,20 +1171,246 @@ function matchBracketCitation(
778
1171
  return {sourceId: id, end: closeIndex + 1};
779
1172
  }
780
1173
 
1174
+ type ExtensionMatch =
1175
+ | {readonly status: 'none'}
1176
+ | {readonly status: 'defer'}
1177
+ | {
1178
+ readonly status: 'match';
1179
+ readonly node: RuntimeExtensionNode;
1180
+ readonly end: number;
1181
+ };
1182
+
1183
+ function currentLineStart(source: string, offset: number): number {
1184
+ return source.lastIndexOf('\n', offset - 1) + 1;
1185
+ }
1186
+
1187
+ function matchExtensionSyntax(
1188
+ source: string,
1189
+ offset: number,
1190
+ context: 'inline' | 'block',
1191
+ opts: ResolvedOptions,
1192
+ ): ExtensionMatch {
1193
+ const byFirstCharacter =
1194
+ context === 'inline'
1195
+ ? opts.plugins?.inlineByFirstCharacter
1196
+ : opts.allowBlockSyntax === false
1197
+ ? undefined
1198
+ : opts.plugins?.blockByFirstCharacter;
1199
+ const candidates = byFirstCharacter?.get(source[offset]);
1200
+ if (candidates == null) {
1201
+ return {status: 'none'};
1202
+ }
1203
+
1204
+ const seen = new Set<PreparedSyntaxContribution>();
1205
+ for (const candidate of candidates) {
1206
+ if (seen.has(candidate)) {
1207
+ continue;
1208
+ }
1209
+ seen.add(candidate);
1210
+ let fullPrefix = false;
1211
+ for (const prefix of candidate.contribution.startsWith) {
1212
+ if (source.startsWith(prefix, offset)) {
1213
+ fullPrefix = true;
1214
+ break;
1215
+ }
1216
+ if (!opts.isFinal) {
1217
+ const remaining = source.slice(offset);
1218
+ if (remaining.length < prefix.length && prefix.startsWith(remaining)) {
1219
+ return {status: 'defer'};
1220
+ }
1221
+ }
1222
+ }
1223
+ if (!fullPrefix) {
1224
+ continue;
1225
+ }
1226
+
1227
+ const end = Math.min(
1228
+ source.length,
1229
+ offset + candidate.contribution.maxSpan,
1230
+ );
1231
+ let result: ReturnType<typeof candidate.contribution.tokenize>;
1232
+ try {
1233
+ result = candidate.contribution.tokenize({
1234
+ source: end === source.length ? source : source.slice(0, end),
1235
+ offset,
1236
+ end,
1237
+ isFinal: opts.isFinal,
1238
+ context,
1239
+ lineStart: currentLineStart(source, offset),
1240
+ column: offset - currentLineStart(source, offset),
1241
+ });
1242
+ } catch (error) {
1243
+ reportMarkdownPluginFailure(candidate.pluginName, 'syntax', error);
1244
+ continue;
1245
+ }
1246
+ if (
1247
+ result == null ||
1248
+ typeof result !== 'object' ||
1249
+ typeof (result as {then?: unknown}).then === 'function'
1250
+ ) {
1251
+ reportMarkdownPluginFailure(
1252
+ candidate.pluginName,
1253
+ 'syntax',
1254
+ new TypeError('Tokenizer returned an invalid result'),
1255
+ );
1256
+ continue;
1257
+ }
1258
+ if (result.status === 'no-match') {
1259
+ continue;
1260
+ }
1261
+ if (result.status === 'defer') {
1262
+ return !opts.isFinal &&
1263
+ end === source.length &&
1264
+ source.length - offset < candidate.contribution.maxSpan
1265
+ ? {status: 'defer'}
1266
+ : {status: 'none'};
1267
+ }
1268
+ if (
1269
+ result.status !== 'match' ||
1270
+ !Number.isInteger(result.end) ||
1271
+ result.end <= offset ||
1272
+ result.end > end ||
1273
+ result.node.type !== 'extension' ||
1274
+ result.node.plugin !== candidate.pluginName ||
1275
+ result.node.display !== context ||
1276
+ result.node.name.trim() === '' ||
1277
+ !isMarkdownPluginData(result.node.data) ||
1278
+ !opts.plugins?.renderers.has(
1279
+ `${candidate.pluginName}\0${result.node.name}`,
1280
+ )
1281
+ ) {
1282
+ reportMarkdownPluginFailure(
1283
+ candidate.pluginName,
1284
+ 'syntax',
1285
+ new TypeError('Tokenizer returned an invalid extension node'),
1286
+ );
1287
+ continue;
1288
+ }
1289
+ const rawNode = result.node as typeof result.node & {
1290
+ readonly source?: unknown;
1291
+ readonly position?: unknown;
1292
+ };
1293
+ const {
1294
+ source: _ignoredSource,
1295
+ position: _ignoredPosition,
1296
+ ...safeNode
1297
+ } = rawNode;
1298
+ const absoluteStart = (opts.baseOffset ?? 0) + offset;
1299
+ const absoluteEnd = (opts.baseOffset ?? 0) + result.end;
1300
+ return {
1301
+ status: 'match',
1302
+ end: result.end,
1303
+ node: Object.freeze({
1304
+ ...safeNode,
1305
+ data: freezeMarkdownPluginData(result.node.data),
1306
+ source: source.slice(offset, result.end),
1307
+ ...(opts.sourceRanges && context === 'block'
1308
+ ? {
1309
+ position: Object.freeze({
1310
+ start: Object.freeze({offset: absoluteStart}),
1311
+ end: Object.freeze({offset: absoluteEnd}),
1312
+ }),
1313
+ }
1314
+ : null),
1315
+ }) as RuntimeExtensionNode,
1316
+ };
1317
+ }
1318
+ return {status: 'none'};
1319
+ }
1320
+
1321
+ type ParseOptionsWithoutPlugins = Omit<ParseOptions, 'plugins'>;
1322
+ type MathParseOptionsWithoutPlugins = Omit<MathParseOptions, 'plugins'>;
1323
+ type IncrementalParseOptionsWithoutPlugins = Omit<
1324
+ IncrementalParseOptions,
1325
+ 'plugins'
1326
+ >;
1327
+ type IncrementalMathParseOptionsWithoutPlugins = Omit<
1328
+ IncrementalMathParseOptions,
1329
+ 'plugins'
1330
+ >;
1331
+
781
1332
  export function parseInline(
782
1333
  text: string,
783
1334
  sourceIds?: ReadonlySet<string>,
784
1335
  ): InlineNode[];
785
1336
  export function parseInline(
786
1337
  text: string,
787
- options: MathParseOptions,
1338
+ options: MathParseOptionsWithoutPlugins,
788
1339
  ): InlineNodeWithMath[];
789
- export function parseInline(text: string, options: ParseOptions): InlineNode[];
1340
+ export function parseInline(
1341
+ text: string,
1342
+ options: ParseOptionsWithoutPlugins,
1343
+ ): InlineNode[];
1344
+ export function parseInline<
1345
+ const Plugins extends ReadonlyArray<MarkdownPluginEntry>,
1346
+ >(
1347
+ text: string,
1348
+ options: ParseOptionsWithoutPlugins & {plugins: Plugins},
1349
+ ): InlineNode<MarkdownExtensionsOf<Plugins>>[];
1350
+ export function parseInline<
1351
+ const Plugins extends ReadonlyArray<MarkdownPluginEntry>,
1352
+ >(
1353
+ text: string,
1354
+ options: MathParseOptionsWithoutPlugins & {plugins: Plugins},
1355
+ ): InlineNodeWithMath<MarkdownExtensionsOf<Plugins>>[];
790
1356
  export function parseInline(
791
1357
  text: string,
792
1358
  arg?: ReadonlySet<string> | RuntimeParseOptions,
793
- ): InlineNodeWithMath[] {
794
- return parseInlineEntry(text, resolveOptions(arg));
1359
+ ): RuntimeInlineNode[] {
1360
+ return projectInlineNodes(
1361
+ parseInlineAstRuntime(text, arg),
1362
+ wantsLegacyRanges(arg),
1363
+ );
1364
+ }
1365
+
1366
+ /**
1367
+ * Parse inline Markdown into the canonical immutable Astryx AST.
1368
+ *
1369
+ * Unlike `parseInline`, this returns MDAST-aligned node names and fields. Math
1370
+ * nodes are present only when `math: true`, and plugin extension nodes are
1371
+ * inferred from the ordered `plugins` tuple.
1372
+ */
1373
+ export function parseInlineAst(
1374
+ text: string,
1375
+ sourceIds?: ReadonlySet<string>,
1376
+ ): MarkdownAstPhrasingContent[];
1377
+ export function parseInlineAst(
1378
+ text: string,
1379
+ options: ParseOptionsWithoutPlugins | MathParseOptionsWithoutPlugins,
1380
+ ): MarkdownAstPhrasingContent[];
1381
+ export function parseInlineAst<
1382
+ const Plugins extends ReadonlyArray<MarkdownPluginEntry>,
1383
+ >(
1384
+ text: string,
1385
+ options: (ParseOptionsWithoutPlugins | MathParseOptionsWithoutPlugins) & {
1386
+ plugins: Plugins;
1387
+ },
1388
+ ): MarkdownAstPhrasingContent<MarkdownExtensionsOf<Plugins>>[];
1389
+ export function parseInlineAst(
1390
+ text: string,
1391
+ arg?: ReadonlySet<string> | RuntimeParseOptions,
1392
+ ): MarkdownAstPhrasingContent<RuntimeExtensionNode>[] {
1393
+ return parseInlineAstRuntime(text, arg);
1394
+ }
1395
+
1396
+ function parseInlineAstRuntime(
1397
+ text: string,
1398
+ arg?: ReadonlySet<string> | RuntimeParseOptions,
1399
+ ): MarkdownAstPhrasingContent<RuntimeExtensionNode>[] {
1400
+ const opts = resolveOptions(arg);
1401
+ const nodes = parseInlineEntry(text, opts);
1402
+ if ((opts.plugins?.transforms.length ?? 0) === 0) {
1403
+ return nodes;
1404
+ }
1405
+ const transformed = applyMarkdownTransforms(
1406
+ {type: 'root', children: [{type: 'paragraph', children: nodes}]},
1407
+ opts.plugins,
1408
+ text,
1409
+ opts.isFinal,
1410
+ 'inline',
1411
+ );
1412
+ const paragraph = transformed.children[0];
1413
+ return paragraph?.type === 'paragraph' ? [...paragraph.children] : nodes;
795
1414
  }
796
1415
 
797
1416
  /**
@@ -806,7 +1425,7 @@ export function parseInline(
806
1425
  function parseInlineEntry(
807
1426
  text: string,
808
1427
  opts: ResolvedOptions,
809
- ): InlineNodeWithMath[] {
1428
+ ): MarkdownAstPhrasingContent<RuntimeExtensionNode>[] {
810
1429
  const nodes = parseInlineImpl(text, opts);
811
1430
  return opts.autolink === 'gfm' ? transformAutolinks(nodes) : nodes;
812
1431
  }
@@ -814,14 +1433,22 @@ function parseInlineEntry(
814
1433
  function parseInlineImpl(
815
1434
  text: string,
816
1435
  opts: ResolvedOptions,
817
- ): InlineNodeWithMath[] {
818
- const nodes: InlineNodeWithMath[] = [];
1436
+ ): MarkdownAstPhrasingContent<RuntimeExtensionNode>[] {
1437
+ const nodes: MarkdownAstPhrasingContent<RuntimeExtensionNode>[] = [];
1438
+ // Only a plugin that actually contributes INLINE syntax may cost anything
1439
+ // per source position. A transform-only list contributes none, so it takes
1440
+ // the same path as an omitted or empty one: no candidate probe per
1441
+ // position, and no map lookup per character in the plain-text scan below.
1442
+ const inlineExtensionStarts =
1443
+ opts.plugins != null && opts.plugins.inlineByFirstCharacter.size > 0
1444
+ ? opts.plugins.inlineByFirstCharacter
1445
+ : undefined;
819
1446
  let i = 0;
820
1447
 
821
1448
  while (i < text.length) {
822
1449
  // --- Escape ---
823
1450
  if (text[i] === '\\' && i + 1 < text.length) {
824
- nodes.push({type: 'text', content: text[i + 1]});
1451
+ nodes.push({type: 'text', value: text[i + 1]});
825
1452
  i += 2;
826
1453
  continue;
827
1454
  }
@@ -832,7 +1459,10 @@ function parseInlineImpl(
832
1459
  const openIndex = i + tickCount;
833
1460
  const closeIndex = text.indexOf('`'.repeat(tickCount), openIndex);
834
1461
  if (closeIndex !== -1) {
835
- nodes.push({type: 'code', content: text.slice(openIndex, closeIndex)});
1462
+ nodes.push({
1463
+ type: 'inlineCode',
1464
+ value: text.slice(openIndex, closeIndex),
1465
+ });
836
1466
  i = closeIndex + tickCount;
837
1467
  continue;
838
1468
  }
@@ -842,7 +1472,10 @@ function parseInlineImpl(
842
1472
  if (opts.math && text[i] === '$') {
843
1473
  const closeIndex = findInlineMathEnd(text, i);
844
1474
  if (closeIndex !== -1) {
845
- nodes.push({type: 'math', value: text.slice(i + 1, closeIndex)});
1475
+ nodes.push({
1476
+ type: 'inlineMath',
1477
+ value: text.slice(i + 1, closeIndex),
1478
+ });
846
1479
  i = closeIndex + 1;
847
1480
  continue;
848
1481
  }
@@ -865,13 +1498,13 @@ function parseInlineImpl(
865
1498
  const srcClose = findClosingParen(text, altClose + 2);
866
1499
  if (srcClose !== -1) {
867
1500
  const src = text.slice(altClose + 2, srcClose);
868
- if (!isSafeUrl(src)) {
1501
+ if (!isSafeMarkdownParserUrl(src)) {
869
1502
  // Dangerous scheme — emit as plain text.
870
- nodes.push({type: 'text', content: text.slice(i, srcClose + 1)});
1503
+ nodes.push({type: 'text', value: text.slice(i, srcClose + 1)});
871
1504
  } else {
872
1505
  nodes.push({
873
1506
  type: 'image',
874
- src,
1507
+ url: src,
875
1508
  alt: text.slice(i + 2, altClose),
876
1509
  });
877
1510
  }
@@ -908,14 +1541,17 @@ function parseInlineImpl(
908
1541
  const urlClose = findClosingParen(text, textClose + 2);
909
1542
  if (urlClose !== -1) {
910
1543
  const href = text.slice(textClose + 2, urlClose);
911
- if (!isSafeUrl(href)) {
1544
+ if (!isSafeMarkdownParserUrl(href)) {
912
1545
  // Dangerous scheme — emit as plain text instead of a link.
913
- nodes.push({type: 'text', content: text.slice(i, urlClose + 1)});
1546
+ nodes.push({type: 'text', value: text.slice(i, urlClose + 1)});
914
1547
  } else {
915
1548
  nodes.push({
916
1549
  type: 'link',
917
- href,
918
- children: parseInlineImpl(text.slice(i + 1, textClose), opts),
1550
+ url: href,
1551
+ children: parseInlineImpl(
1552
+ text.slice(i + 1, textClose),
1553
+ protectedInlineOptions(opts),
1554
+ ),
919
1555
  });
920
1556
  }
921
1557
  i = urlClose + 1;
@@ -950,10 +1586,10 @@ function parseInlineImpl(
950
1586
  (!isUnderscore || !isWordChar(text[closeIndex + 3]))
951
1587
  ) {
952
1588
  nodes.push({
953
- type: 'bold',
1589
+ type: 'strong',
954
1590
  children: [
955
1591
  {
956
- type: 'italic',
1592
+ type: 'emphasis',
957
1593
  children: parseInlineImpl(text.slice(i + 3, closeIndex), opts),
958
1594
  },
959
1595
  ],
@@ -980,7 +1616,7 @@ function parseInlineImpl(
980
1616
  (!isUnderscore || !isWordChar(text[closeIndex + 2]))
981
1617
  ) {
982
1618
  nodes.push({
983
- type: 'bold',
1619
+ type: 'strong',
984
1620
  children: parseInlineImpl(text.slice(i + 2, closeIndex), opts),
985
1621
  });
986
1622
  i = closeIndex + 2;
@@ -994,7 +1630,7 @@ function parseInlineImpl(
994
1630
  const closeIndex = text.indexOf('~~', i + 2);
995
1631
  if (closeIndex !== -1) {
996
1632
  nodes.push({
997
- type: 'strikethrough',
1633
+ type: 'delete',
998
1634
  children: parseInlineImpl(text.slice(i + 2, closeIndex), opts),
999
1635
  });
1000
1636
  i = closeIndex + 2;
@@ -1015,7 +1651,7 @@ function parseInlineImpl(
1015
1651
  (!isUnderscore || !isWordChar(text[closeIndex + 1]))
1016
1652
  ) {
1017
1653
  nodes.push({
1018
- type: 'italic',
1654
+ type: 'emphasis',
1019
1655
  children: parseInlineImpl(text.slice(i + 1, closeIndex), opts),
1020
1656
  });
1021
1657
  i = closeIndex + 1;
@@ -1024,12 +1660,29 @@ function parseInlineImpl(
1024
1660
  }
1025
1661
  }
1026
1662
 
1663
+ // --- Extension syntax (built-ins and protected contexts win) ---
1664
+ if (inlineExtensionStarts !== undefined) {
1665
+ const extension = matchExtensionSyntax(text, i, 'inline', opts);
1666
+ if (extension.status === 'match') {
1667
+ nodes.push(
1668
+ extension.node as Extract<RuntimeExtensionNode, {display: 'inline'}>,
1669
+ );
1670
+ i = extension.end;
1671
+ continue;
1672
+ }
1673
+ if (extension.status === 'defer') {
1674
+ break;
1675
+ }
1676
+ }
1677
+
1027
1678
  // --- Plain text (with line-break detection) ---
1028
1679
  let end = i + 1;
1029
1680
  while (
1030
1681
  end < text.length &&
1031
1682
  !'*_~`[!\\\n\u3010'.includes(text[end]) &&
1032
- !(opts.math && text[end] === '$')
1683
+ !(opts.math && text[end] === '$') &&
1684
+ (inlineExtensionStarts === undefined ||
1685
+ !inlineExtensionStarts.has(text[end]))
1033
1686
  ) {
1034
1687
  end++;
1035
1688
  }
@@ -1043,9 +1696,12 @@ function parseInlineImpl(
1043
1696
  if (trimmed.length > 0) {
1044
1697
  const last = nodes[nodes.length - 1];
1045
1698
  if (last?.type === 'text') {
1046
- last.content += trimmed;
1699
+ nodes[nodes.length - 1] = {
1700
+ ...last,
1701
+ value: last.value + trimmed,
1702
+ };
1047
1703
  } else {
1048
- nodes.push({type: 'text', content: trimmed});
1704
+ nodes.push({type: 'text', value: trimmed});
1049
1705
  }
1050
1706
  }
1051
1707
  nodes.push({type: 'break'});
@@ -1056,9 +1712,9 @@ function parseInlineImpl(
1056
1712
 
1057
1713
  const last = nodes[nodes.length - 1];
1058
1714
  if (last?.type === 'text') {
1059
- last.content += content;
1715
+ nodes[nodes.length - 1] = {...last, value: last.value + content};
1060
1716
  } else {
1061
- nodes.push({type: 'text', content});
1717
+ nodes.push({type: 'text', value: content});
1062
1718
  }
1063
1719
  i = end;
1064
1720
  }
@@ -1183,7 +1839,7 @@ function scanAutolinksInText(text: string): AutolinkMatch[] {
1183
1839
  while ((m = re.exec(text)) !== null) {
1184
1840
  const url = m[1];
1185
1841
  // Skip dangerous URL schemes (javascript:, vbscript:, data:text/html)
1186
- if (!isSafeUrl(url)) {
1842
+ if (!isSafeMarkdownParserUrl(url)) {
1187
1843
  continue;
1188
1844
  }
1189
1845
  matches.push({
@@ -1303,26 +1959,28 @@ function scanAutolinksInText(text: string): AutolinkMatch[] {
1303
1959
  * Split a text-node `content` string into a sequence of text + link nodes
1304
1960
  * based on autolink matches.
1305
1961
  */
1306
- function splitTextOnAutolinks(content: string): InlineNodeWithMath[] {
1962
+ function splitTextOnAutolinks(
1963
+ content: string,
1964
+ ): MarkdownAstPhrasingContent<RuntimeExtensionNode>[] {
1307
1965
  const matches = scanAutolinksInText(content);
1308
1966
  if (matches.length === 0) {
1309
- return [{type: 'text', content}];
1967
+ return [{type: 'text', value: content}];
1310
1968
  }
1311
- const out: InlineNodeWithMath[] = [];
1969
+ const out: MarkdownAstPhrasingContent<RuntimeExtensionNode>[] = [];
1312
1970
  let cursor = 0;
1313
1971
  for (const m of matches) {
1314
1972
  if (m.start > cursor) {
1315
- out.push({type: 'text', content: content.slice(cursor, m.start)});
1973
+ out.push({type: 'text', value: content.slice(cursor, m.start)});
1316
1974
  }
1317
1975
  out.push({
1318
1976
  type: 'link',
1319
- href: m.href,
1320
- children: [{type: 'text', content: m.display}],
1977
+ url: m.href,
1978
+ children: [{type: 'text', value: m.display}],
1321
1979
  });
1322
1980
  cursor = m.end;
1323
1981
  }
1324
1982
  if (cursor < content.length) {
1325
- out.push({type: 'text', content: content.slice(cursor)});
1983
+ out.push({type: 'text', value: content.slice(cursor)});
1326
1984
  }
1327
1985
  return out;
1328
1986
  }
@@ -1335,23 +1993,25 @@ function splitTextOnAutolinks(content: string): InlineNodeWithMath[] {
1335
1993
  * `image` alt text, `citation`, or `break`. Runs only on the outermost
1336
1994
  * block's inline tree (see `parseInlineEntry`).
1337
1995
  */
1338
- function transformAutolinks(nodes: InlineNodeWithMath[]): InlineNodeWithMath[] {
1339
- const out: InlineNodeWithMath[] = [];
1996
+ function transformAutolinks(
1997
+ nodes: ReadonlyArray<MarkdownAstPhrasingContent<RuntimeExtensionNode>>,
1998
+ ): MarkdownAstPhrasingContent<RuntimeExtensionNode>[] {
1999
+ const out: MarkdownAstPhrasingContent<RuntimeExtensionNode>[] = [];
1340
2000
  for (const node of nodes) {
1341
2001
  if (node.type === 'text') {
1342
- const split = splitTextOnAutolinks(node.content);
2002
+ const split = splitTextOnAutolinks(node.value);
1343
2003
  for (const seg of split) {
1344
2004
  const last = out[out.length - 1];
1345
2005
  if (seg.type === 'text' && last?.type === 'text') {
1346
- last.content += seg.content;
2006
+ out[out.length - 1] = {...last, value: last.value + seg.value};
1347
2007
  } else {
1348
2008
  out.push(seg);
1349
2009
  }
1350
2010
  }
1351
2011
  } else if (
1352
- node.type === 'bold' ||
1353
- node.type === 'italic' ||
1354
- node.type === 'strikethrough'
2012
+ node.type === 'strong' ||
2013
+ node.type === 'emphasis' ||
2014
+ node.type === 'delete'
1355
2015
  ) {
1356
2016
  out.push({...node, children: transformAutolinks(node.children)});
1357
2017
  } else {
@@ -1434,7 +2094,16 @@ function isTableSeparator(line: string): boolean {
1434
2094
  * offset into it would not address the document.
1435
2095
  */
1436
2096
  function nested(opts: ResolvedOptions): ResolvedOptions {
1437
- return opts.sourceRanges ? {...opts, sourceRanges: false} : opts;
2097
+ const nestedOptions =
2098
+ opts.allowBlockSyntax === false ? opts : {...opts, allowBlockSyntax: false};
2099
+ return nestedOptions.sourceRanges || nestedOptions.astPositions
2100
+ ? {...nestedOptions, sourceRanges: false, astPositions: false}
2101
+ : nestedOptions;
2102
+ }
2103
+
2104
+ function blockExtensionColumn(line: string): number | null {
2105
+ const indentation = line.length - line.trimStart().length;
2106
+ return indentation <= 3 ? indentation : null;
1438
2107
  }
1439
2108
 
1440
2109
  /**
@@ -1510,10 +2179,10 @@ function parseTable(
1510
2179
  lines: string[],
1511
2180
  lineIndex: number,
1512
2181
  opts: ResolvedOptions,
1513
- ): {node: BlockNodeWithMath; nextIndex: number} {
1514
- const headers: TableCellNodeWithMath[] = splitTableRow(lines[lineIndex]).map(
1515
- cell => ({children: parseInlineEntry(cell, opts)}),
1516
- );
2182
+ ): {node: MarkdownAstBlockContent<RuntimeExtensionNode>; nextIndex: number} {
2183
+ const headers: MarkdownAstTableCell<RuntimeExtensionNode>[] = splitTableRow(
2184
+ lines[lineIndex],
2185
+ ).map(cell => ({type: 'tableCell', children: parseInlineEntry(cell, opts)}));
1517
2186
  const alignments: TableAlignment[] = splitTableRow(lines[lineIndex + 1]).map(
1518
2187
  cell => {
1519
2188
  const trimmed = cell.trim();
@@ -1528,22 +2197,32 @@ function parseTable(
1528
2197
  : null;
1529
2198
  },
1530
2199
  );
1531
- const rows: TableCellNodeWithMath[][] = [];
2200
+ const rows: MarkdownAstTableRow<RuntimeExtensionNode>[] = [];
1532
2201
  let rowIndex = lineIndex + 2;
1533
2202
  while (
1534
2203
  rowIndex < lines.length &&
1535
2204
  lines[rowIndex].includes('|') &&
1536
2205
  lines[rowIndex].trim() !== ''
1537
2206
  ) {
1538
- rows.push(
1539
- splitTableRow(lines[rowIndex]).map(cell => ({
2207
+ rows.push({
2208
+ type: 'tableRow',
2209
+ children: splitTableRow(lines[rowIndex]).map(cell => ({
2210
+ type: 'tableCell',
1540
2211
  children: parseInlineEntry(cell, opts),
1541
2212
  })),
1542
- );
2213
+ });
1543
2214
  rowIndex++;
1544
2215
  }
2216
+ const header: MarkdownAstTableRow<RuntimeExtensionNode> = {
2217
+ type: 'tableRow',
2218
+ children: headers,
2219
+ };
1545
2220
  return {
1546
- node: {type: 'table', headers, alignments, rows},
2221
+ node: {
2222
+ type: 'table',
2223
+ align: alignments,
2224
+ children: [header, ...rows],
2225
+ },
1547
2226
  nextIndex: rowIndex,
1548
2227
  };
1549
2228
  }
@@ -1553,8 +2232,8 @@ function parseList(
1553
2232
  startIndex: number,
1554
2233
  ordered: boolean,
1555
2234
  opts: ResolvedOptions,
1556
- ): {node: BlockNodeWithMath; nextIndex: number} {
1557
- const items: ListItemNodeWithMath[] = [];
2235
+ ): {node: MarkdownAstBlockContent<RuntimeExtensionNode>; nextIndex: number} {
2236
+ const items: MarkdownAstListItem<RuntimeExtensionNode>[] = [];
1558
2237
  const baseIndent = getIndent(lines[startIndex]);
1559
2238
  // Ordered lists may use either '.' or ')' as the marker delimiter
1560
2239
  // (CommonMark 5.2). Capture which one this list starts with so its items
@@ -1608,7 +2287,11 @@ function parseList(
1608
2287
  itemText += '\n' + deindented.join('\n');
1609
2288
  }
1610
2289
 
1611
- items.push({checked, children: parseMarkdownImpl(itemText, nested(opts))});
2290
+ items.push({
2291
+ type: 'listItem',
2292
+ checked,
2293
+ children: parseMarkdownImpl(itemText, nested(opts)),
2294
+ });
1612
2295
 
1613
2296
  // CommonMark loose list: blank line(s) between items of the same style
1614
2297
  // and indent still form one list. Skip the blanks and continue if the
@@ -1626,17 +2309,15 @@ function parseList(
1626
2309
  index = lookahead;
1627
2310
  }
1628
2311
  }
1629
- return {
1630
- node: {
1631
- type: 'list',
1632
- ordered,
1633
- start,
1634
- delimiter: ordered ? (delim as '.' | ')') : undefined,
1635
- loose: loose || undefined,
1636
- items,
1637
- },
1638
- nextIndex: index,
2312
+ const node: MarkdownAstList<RuntimeExtensionNode> = {
2313
+ type: 'list',
2314
+ ordered,
2315
+ start,
2316
+ delimiter: ordered ? (delim as '.' | ')') : undefined,
2317
+ spread: loose || undefined,
2318
+ children: items,
1639
2319
  };
2320
+ return {node, nextIndex: index};
1640
2321
  }
1641
2322
 
1642
2323
  // ---------------------------------------------------------------------------
@@ -1649,23 +2330,89 @@ export function parseMarkdown(
1649
2330
  ): BlockNode[];
1650
2331
  export function parseMarkdown(
1651
2332
  input: string,
1652
- options: MathParseOptions,
2333
+ options: MathParseOptionsWithoutPlugins,
1653
2334
  ): BlockNodeWithMath[];
1654
2335
  export function parseMarkdown(
1655
2336
  input: string,
1656
- options: ParseOptions,
2337
+ options: ParseOptionsWithoutPlugins,
1657
2338
  ): BlockNode[];
2339
+ export function parseMarkdown<
2340
+ const Plugins extends ReadonlyArray<MarkdownPluginEntry>,
2341
+ >(
2342
+ input: string,
2343
+ options: ParseOptionsWithoutPlugins & {plugins: Plugins},
2344
+ ): BlockNode<MarkdownExtensionsOf<Plugins>>[];
2345
+ export function parseMarkdown<
2346
+ const Plugins extends ReadonlyArray<MarkdownPluginEntry>,
2347
+ >(
2348
+ input: string,
2349
+ options: MathParseOptionsWithoutPlugins & {plugins: Plugins},
2350
+ ): BlockNodeWithMath<MarkdownExtensionsOf<Plugins>>[];
1658
2351
  export function parseMarkdown(
1659
2352
  input: string,
1660
2353
  arg?: ReadonlySet<string> | RuntimeParseOptions,
1661
- ): BlockNodeWithMath[] {
1662
- return parseMarkdownImpl(input, resolveOptions(arg));
2354
+ ): RuntimeBlockNode[] {
2355
+ return projectMarkdownRoot(
2356
+ parseMarkdownAstInternal(input, arg, true),
2357
+ wantsLegacyRanges(arg),
2358
+ );
2359
+ }
2360
+
2361
+ /**
2362
+ * Parse block Markdown into the canonical immutable Astryx AST.
2363
+ *
2364
+ * Unlike `parseMarkdown`, this returns the MDAST-aligned root used by Markdown,
2365
+ * transforms, and Outline. Math nodes are present only when `math: true`, and
2366
+ * plugin extension nodes are inferred from the ordered `plugins` tuple.
2367
+ */
2368
+ export function parseMarkdownAst(
2369
+ input: string,
2370
+ sourceIds?: ReadonlySet<string>,
2371
+ ): MarkdownAstRoot;
2372
+ export function parseMarkdownAst(
2373
+ input: string,
2374
+ options: ParseOptionsWithoutPlugins | MathParseOptionsWithoutPlugins,
2375
+ ): MarkdownAstRoot;
2376
+ export function parseMarkdownAst<
2377
+ const Plugins extends ReadonlyArray<MarkdownPluginEntry>,
2378
+ >(
2379
+ input: string,
2380
+ options: (ParseOptionsWithoutPlugins | MathParseOptionsWithoutPlugins) & {
2381
+ plugins: Plugins;
2382
+ },
2383
+ ): MarkdownAstRoot<MarkdownExtensionsOf<Plugins>>;
2384
+ export function parseMarkdownAst(
2385
+ input: string,
2386
+ arg?: ReadonlySet<string> | RuntimeParseOptions,
2387
+ ): MarkdownAstRoot<RuntimeExtensionNode> {
2388
+ return parseMarkdownAstInternal(input, arg, true);
2389
+ }
2390
+
2391
+ /** @internal Full-parse entry point with streaming finality for Outline. */
2392
+ export function parseMarkdownAstInternal(
2393
+ input: string,
2394
+ arg: ReadonlySet<string> | RuntimeParseOptions | undefined,
2395
+ isFinal: boolean,
2396
+ ): MarkdownAstRoot<RuntimeExtensionNode> {
2397
+ const resolved = resolveOptions(arg);
2398
+ const opts = isFinal ? resolved : {...resolved, isFinal: false};
2399
+ const root: MarkdownAstRoot<RuntimeExtensionNode> = {
2400
+ type: 'root',
2401
+ children: parseMarkdownImpl(input, opts),
2402
+ };
2403
+ return applyMarkdownTransforms(
2404
+ root,
2405
+ opts.plugins,
2406
+ input,
2407
+ opts.isFinal,
2408
+ 'block',
2409
+ );
1663
2410
  }
1664
2411
 
1665
2412
  function parseMarkdownImpl(
1666
2413
  input: string,
1667
2414
  baseOpts: ResolvedOptions,
1668
- ): BlockNodeWithMath[] {
2415
+ ): MarkdownAstBlockContent<RuntimeExtensionNode>[] {
1669
2416
  // Collect this input's link reference definitions and strip their lines,
1670
2417
  // then merge them with any definitions inherited from an enclosing parse
1671
2418
  // (the incremental parser passes the whole document's definitions in; a
@@ -1686,18 +2433,35 @@ function parseMarkdownImpl(
1686
2433
  const opts: ResolvedOptions =
1687
2434
  linkDefs != null ? {...baseOpts, linkDefs} : baseOpts;
1688
2435
  const lines = cleaned.split('\n');
1689
- const blocks: BlockNodeWithMath[] = [];
2436
+ const hasBlockExtensionSyntax =
2437
+ opts.allowBlockSyntax !== false &&
2438
+ (opts.plugins?.blockByFirstCharacter.size ?? 0) > 0;
2439
+ // Derived from the already-split lines rather than a second character
2440
+ // scan: every line contributes its own length plus the newline it ended on.
2441
+ const lineOffsets = [0];
2442
+ if (opts.astPositions === true || hasBlockExtensionSyntax) {
2443
+ let offset = 0;
2444
+ for (let index = 0; index < lines.length; index++) {
2445
+ offset += lines[index].length + 1;
2446
+ lineOffsets.push(offset);
2447
+ }
2448
+ }
2449
+ const blockExtensionMatches = new Map<number, ExtensionMatch>();
2450
+ const blocks: MarkdownAstBlockContent<RuntimeExtensionNode>[] = [];
1690
2451
  // The line each block started on, parallel to `blocks`. Only collected when
1691
2452
  // ranges were asked for; a block's end is resolved after the loop, since the
1692
2453
  // branch that produced it has already moved `index` past whatever it read.
1693
- const blockStartLines: number[] | null = opts.sourceRanges ? [] : null;
2454
+ const blockStartLines: number[] | null =
2455
+ opts.astPositions === true ? [] : null;
1694
2456
  // Set only by a block that consumes blank lines as content, where the
1695
2457
  // positional end derivation would trim them away.
1696
- const blockEndLines: (number | undefined)[] | null = opts.sourceRanges
1697
- ? []
1698
- : null;
2458
+ const blockEndLines: (number | undefined)[] | null =
2459
+ opts.astPositions === true ? [] : null;
1699
2460
  let blockStartLine = 0;
1700
- const pushBlock = (node: BlockNodeWithMath, endLine?: number) => {
2461
+ const pushBlock = (
2462
+ node: MarkdownAstBlockContent<RuntimeExtensionNode>,
2463
+ endLine?: number,
2464
+ ) => {
1701
2465
  blocks.push(node);
1702
2466
  blockStartLines?.push(blockStartLine);
1703
2467
  blockEndLines?.push(endLine);
@@ -1713,10 +2477,16 @@ function parseMarkdownImpl(
1713
2477
  }
1714
2478
 
1715
2479
  // --- Fenced code block ---
1716
- const fenceMatch = line.match(/^(`{3,}|~{3,})(\w*)/);
2480
+ const fenceMatch = line.match(/^(`{3,}|~{3,})/);
1717
2481
  if (fenceMatch) {
1718
2482
  const fence = fenceMatch[1];
1719
- const language = fenceMatch[2] || 'plaintext';
2483
+ const info = line.slice(fence.length);
2484
+ const language = info.match(/^(\S+)/)?.[1] ?? null;
2485
+ const legacyLanguage = info.match(/^(\w*)/)?.[1] || null;
2486
+ const meta =
2487
+ language == null
2488
+ ? undefined
2489
+ : info.slice(language.length).trim() || undefined;
1720
2490
  const codeLines: string[] = [];
1721
2491
  index++;
1722
2492
  while (index < lines.length && !lines[index].startsWith(fence)) {
@@ -1728,7 +2498,15 @@ function parseMarkdownImpl(
1728
2498
  // can end on them, so it states its own end rather than letting the
1729
2499
  // positional derivation trim them off.
1730
2500
  pushBlock(
1731
- {type: 'codeblock', language, content: codeLines.join('\n')},
2501
+ markMarkdownAstLegacyCodeLanguage(
2502
+ {
2503
+ type: 'code',
2504
+ lang: language,
2505
+ ...(meta == null ? {} : {meta}),
2506
+ value: codeLines.join('\n'),
2507
+ },
2508
+ legacyLanguage,
2509
+ ),
1732
2510
  Math.min(index, lines.length) - 1,
1733
2511
  );
1734
2512
  continue;
@@ -1752,7 +2530,7 @@ function parseMarkdownImpl(
1752
2530
  if (headingMatch) {
1753
2531
  pushBlock({
1754
2532
  type: 'heading',
1755
- level: headingMatch[1].length as 1 | 2 | 3 | 4 | 5 | 6,
2533
+ depth: headingMatch[1].length as 1 | 2 | 3 | 4 | 5 | 6,
1756
2534
  children: parseInlineEntry(headingMatch[2], opts),
1757
2535
  });
1758
2536
  index++;
@@ -1761,7 +2539,7 @@ function parseMarkdownImpl(
1761
2539
 
1762
2540
  // --- HR (must precede list check to handle `- - -`, `* * *`, `_ _ _`) ---
1763
2541
  if (isHorizontalRule(line)) {
1764
- pushBlock({type: 'hr'});
2542
+ pushBlock({type: 'thematicBreak'});
1765
2543
  index++;
1766
2544
  continue;
1767
2545
  }
@@ -1773,9 +2551,9 @@ function parseMarkdownImpl(
1773
2551
  if (
1774
2552
  imageMatch &&
1775
2553
  line.trim() === imageMatch[0] &&
1776
- isSafeUrl(imageMatch[2])
2554
+ isSafeMarkdownParserUrl(imageMatch[2])
1777
2555
  ) {
1778
- pushBlock({type: 'image', alt: imageMatch[1], src: imageMatch[2]});
2556
+ pushBlock({type: 'image', alt: imageMatch[1], url: imageMatch[2]});
1779
2557
  index++;
1780
2558
  continue;
1781
2559
  }
@@ -1825,16 +2603,80 @@ function parseMarkdownImpl(
1825
2603
  continue;
1826
2604
  }
1827
2605
 
2606
+ // --- Extension block syntax (built-in blocks take precedence) ---
2607
+ if (hasBlockExtensionSyntax) {
2608
+ const extensionColumn = blockExtensionColumn(line);
2609
+ const extensionOffset =
2610
+ extensionColumn == null
2611
+ ? lineOffsets[index]
2612
+ : lineOffsets[index] + extensionColumn;
2613
+ const extension =
2614
+ blockExtensionMatches.get(extensionOffset) ??
2615
+ (extensionColumn == null
2616
+ ? {status: 'none' as const}
2617
+ : matchExtensionSyntax(cleaned, extensionOffset, 'block', opts));
2618
+ blockExtensionMatches.delete(extensionOffset);
2619
+ if (extension.status === 'match') {
2620
+ const consumedEnd = extension.end;
2621
+ const consumed = cleaned.slice(lineOffsets[index], consumedEnd);
2622
+ if (
2623
+ consumedEnd < cleaned.length &&
2624
+ cleaned[consumedEnd] !== '\n' &&
2625
+ cleaned[consumedEnd - 1] !== '\n'
2626
+ ) {
2627
+ reportMarkdownPluginFailure(
2628
+ extension.node.plugin,
2629
+ 'syntax',
2630
+ new TypeError('Block syntax must consume complete lines'),
2631
+ );
2632
+ } else {
2633
+ const newlineCount = consumed.split('\n').length - 1;
2634
+ const consumedLines = Math.max(
2635
+ 1,
2636
+ newlineCount + (consumed.endsWith('\n') ? 0 : 1),
2637
+ );
2638
+ pushBlock(
2639
+ extension.node as Extract<RuntimeExtensionNode, {display: 'block'}>,
2640
+ index + consumedLines - 1,
2641
+ );
2642
+ index += consumedLines;
2643
+ continue;
2644
+ }
2645
+ }
2646
+ if (extension.status === 'defer') {
2647
+ break;
2648
+ }
2649
+ }
2650
+
1828
2651
  // --- Paragraph ---
1829
2652
  const paraLines: string[] = [line];
1830
2653
  index++;
1831
- while (
1832
- index < lines.length &&
1833
- !isBlockStart(lines[index]) &&
1834
- !(opts.math && matchDisplayMathBlock(lines, index) != null) &&
1835
- lines[index].trim() !== ''
1836
- ) {
1837
- paraLines.push(lines[index]);
2654
+ while (index < lines.length) {
2655
+ const nextLine = lines[index];
2656
+ if (
2657
+ isBlockStart(nextLine) ||
2658
+ (opts.math && matchDisplayMathBlock(lines, index) != null) ||
2659
+ nextLine.trim() === ''
2660
+ ) {
2661
+ break;
2662
+ }
2663
+ if (hasBlockExtensionSyntax) {
2664
+ const extensionColumn = blockExtensionColumn(nextLine);
2665
+ if (extensionColumn != null) {
2666
+ const extensionOffset = lineOffsets[index] + extensionColumn;
2667
+ const extension = matchExtensionSyntax(
2668
+ cleaned,
2669
+ extensionOffset,
2670
+ 'block',
2671
+ opts,
2672
+ );
2673
+ if (extension.status !== 'none') {
2674
+ blockExtensionMatches.set(extensionOffset, extension);
2675
+ break;
2676
+ }
2677
+ }
2678
+ }
2679
+ paraLines.push(nextLine);
1838
2680
  index++;
1839
2681
  }
1840
2682
  pushBlock({
@@ -1866,7 +2708,7 @@ function parseMarkdownImpl(
1866
2708
  * `lineMap` says which input line each surviving line came from.
1867
2709
  */
1868
2710
  function stampSourceRanges(
1869
- blocks: BlockNodeWithMath[],
2711
+ blocks: MarkdownAstBlockContent<RuntimeExtensionNode>[],
1870
2712
  blockStartLines: number[],
1871
2713
  blockEndLines: (number | undefined)[],
1872
2714
  lines: string[],
@@ -1903,7 +2745,13 @@ function stampSourceRanges(
1903
2745
  // range that dropped it would slice to something that re-parses
1904
2746
  // differently.
1905
2747
  const end = lineStart(endLine) + lines[endLine].length;
1906
- blocks[i] = {...blocks[i], range: {start: lineStart(startLine), end}};
2748
+ blocks[i] = {
2749
+ ...blocks[i],
2750
+ position: {
2751
+ start: {offset: lineStart(startLine)},
2752
+ end: {offset: end},
2753
+ },
2754
+ };
1907
2755
  }
1908
2756
  }
1909
2757
 
@@ -1938,6 +2786,10 @@ export interface IncrementalState<MathEnabled extends boolean = false> {
1938
2786
  * lack the ranges the caller now asks for, or carry ones it did not.
1939
2787
  */
1940
2788
  sourceRanges?: boolean;
2789
+ /** Identity of citation sources used by settled nodes. */
2790
+ sourceIdsKey?: string;
2791
+ /** Ordered syntax-only identity used by settled nodes. */
2792
+ pluginSyntaxIdentity?: string;
1941
2793
  /**
1942
2794
  * Signature of the link reference definitions the cached `settledBlocks`
1943
2795
  * were parsed with. Definitions are document-global and typically arrive
@@ -1968,6 +2820,13 @@ type IncrementalCache = {
1968
2820
  /** The effective document-global definitions used by slice parses. */
1969
2821
  linkDefs: ReadonlyMap<string, string>;
1970
2822
  linkDefsKey: string;
2823
+ /** Canonical settled blocks shared by rendering and compatibility projection. */
2824
+ settledAstBlocks: MarkdownAstBlockContent<RuntimeExtensionNode>[];
2825
+ settledRevision: number;
2826
+ projectedRevision: number;
2827
+ projectedSettledBlocks: RuntimeBlockNode[];
2828
+ /** Preserves released settled-node identity across projected snapshots. */
2829
+ projectionCache: LegacyProjectionCache;
1971
2830
  work: IncrementalWork;
1972
2831
  };
1973
2832
 
@@ -1986,6 +2845,11 @@ function makeIncrementalCache(
1986
2845
  tailLinkDefs: new Map(),
1987
2846
  linkDefs: defs,
1988
2847
  linkDefsKey: linkDefsSignature(defs),
2848
+ settledAstBlocks: [],
2849
+ settledRevision: 0,
2850
+ projectedRevision: -1,
2851
+ projectedSettledBlocks: [],
2852
+ projectionCache: new WeakMap(),
1989
2853
  work: {
1990
2854
  splitCharacters: 0,
1991
2855
  boundaryLines: 0,
@@ -2470,7 +3334,7 @@ function trimUnsettledStructural(text: string): string {
2470
3334
  * rather than into the slice.
2471
3335
  */
2472
3336
  function atOffset(opts: ResolvedOptions, offset: number): ResolvedOptions {
2473
- return opts.sourceRanges ? {...opts, baseOffset: offset} : opts;
3337
+ return opts.astPositions === true ? {...opts, baseOffset: offset} : opts;
2474
3338
  }
2475
3339
 
2476
3340
  /**
@@ -2481,9 +3345,9 @@ function atOffset(opts: ResolvedOptions, offset: number): ResolvedOptions {
2481
3345
  * lists even though the full-text parser joins them per CommonMark §5.3.
2482
3346
  */
2483
3347
  function mergeSettledBlocks(
2484
- prev: BlockNodeWithMath[],
2485
- delta: BlockNodeWithMath[],
2486
- ): BlockNodeWithMath[] {
3348
+ prev: MarkdownAstBlockContent<RuntimeExtensionNode>[],
3349
+ delta: MarkdownAstBlockContent<RuntimeExtensionNode>[],
3350
+ ): MarkdownAstBlockContent<RuntimeExtensionNode>[] {
2487
3351
  if (prev.length === 0 || delta.length === 0) {
2488
3352
  return [...prev, ...delta];
2489
3353
  }
@@ -2495,17 +3359,21 @@ function mergeSettledBlocks(
2495
3359
  prevLast.ordered === deltaFirst.ordered &&
2496
3360
  prevLast.delimiter === deltaFirst.delimiter
2497
3361
  ) {
2498
- const merged: BlockNodeWithMath = {
3362
+ const merged: MarkdownAstBlockContent<RuntimeExtensionNode> = {
2499
3363
  type: 'list',
2500
3364
  ordered: prevLast.ordered,
2501
3365
  start: prevLast.start,
2502
3366
  delimiter: prevLast.delimiter,
2503
- loose: true,
2504
- items: [...prevLast.items, ...deltaFirst.items],
2505
- // One list now, so one range: from where the first half started to
2506
- // where the second half ended.
2507
- ...(prevLast.range != null && deltaFirst.range != null
2508
- ? {range: {start: prevLast.range.start, end: deltaFirst.range.end}}
3367
+ spread: true,
3368
+ children: [...prevLast.children, ...deltaFirst.children],
3369
+ // One list now, so one position spans both halves.
3370
+ ...(prevLast.position != null && deltaFirst.position != null
3371
+ ? {
3372
+ position: {
3373
+ start: prevLast.position.start,
3374
+ end: deltaFirst.position.end,
3375
+ },
3376
+ }
2509
3377
  : null),
2510
3378
  };
2511
3379
  return [...prev.slice(0, -1), merged, ...delta.slice(1)];
@@ -2515,15 +3383,15 @@ function mergeSettledBlocks(
2515
3383
 
2516
3384
  /** Append a newly-settled slice without copying the already-settled prefix. */
2517
3385
  function appendSettledBlocks(
2518
- prev: BlockNodeWithMath[],
2519
- delta: BlockNodeWithMath[],
2520
- ): void {
3386
+ prev: MarkdownAstBlockContent<RuntimeExtensionNode>[],
3387
+ delta: MarkdownAstBlockContent<RuntimeExtensionNode>[],
3388
+ ): boolean {
2521
3389
  if (delta.length === 0) {
2522
- return;
3390
+ return false;
2523
3391
  }
2524
3392
  if (prev.length === 0) {
2525
3393
  prev.push(...delta);
2526
- return;
3394
+ return false;
2527
3395
  }
2528
3396
  const prevLast = prev[prev.length - 1];
2529
3397
  const deltaFirst = delta[0];
@@ -2538,16 +3406,22 @@ function appendSettledBlocks(
2538
3406
  ordered: prevLast.ordered,
2539
3407
  start: prevLast.start,
2540
3408
  delimiter: prevLast.delimiter,
2541
- loose: true,
2542
- items: [...prevLast.items, ...deltaFirst.items],
2543
- ...(prevLast.range != null && deltaFirst.range != null
2544
- ? {range: {start: prevLast.range.start, end: deltaFirst.range.end}}
3409
+ spread: true,
3410
+ children: [...prevLast.children, ...deltaFirst.children],
3411
+ ...(prevLast.position != null && deltaFirst.position != null
3412
+ ? {
3413
+ position: {
3414
+ start: prevLast.position.start,
3415
+ end: deltaFirst.position.end,
3416
+ },
3417
+ }
2545
3418
  : null),
2546
3419
  };
2547
3420
  prev.push(...delta.slice(1));
2548
- return;
3421
+ return true;
2549
3422
  }
2550
3423
  prev.push(...delta);
3424
+ return false;
2551
3425
  }
2552
3426
 
2553
3427
  function sameUnsettledDefinitions(
@@ -2577,12 +3451,19 @@ function resetIncrementalCache(
2577
3451
  state.settledBlocks = [];
2578
3452
  state.settledUpTo = 0;
2579
3453
  state.linkDefsKey = undefined;
3454
+ state.sourceIdsKey = undefined;
3455
+ state.pluginSyntaxIdentity = undefined;
2580
3456
  state.math = undefined;
2581
3457
  cache.settledEnd = 0;
2582
3458
  cache.settledLinkDefs.clear();
2583
3459
  cache.tailLinkDefs = new Map();
2584
3460
  cache.linkDefs = new Map();
2585
3461
  cache.linkDefsKey = '';
3462
+ cache.settledAstBlocks = [];
3463
+ cache.settledRevision++;
3464
+ cache.projectedRevision = -1;
3465
+ cache.projectedSettledBlocks = [];
3466
+ cache.projectionCache = new WeakMap();
2586
3467
  cache.work = {
2587
3468
  splitCharacters: 0,
2588
3469
  boundaryLines: 0,
@@ -2610,40 +3491,102 @@ export function parseMarkdownIncremental(
2610
3491
  export function parseMarkdownIncremental(
2611
3492
  input: string,
2612
3493
  state: IncrementalState<true>,
2613
- options: MathParseOptions,
3494
+ options: IncrementalMathParseOptionsWithoutPlugins,
2614
3495
  ): BlockNodeWithMath[];
2615
3496
  export function parseMarkdownIncremental(
2616
3497
  input: string,
2617
3498
  state: IncrementalState<false>,
2618
- options: ParseOptions,
3499
+ options: IncrementalParseOptionsWithoutPlugins,
2619
3500
  ): BlockNode[];
3501
+ export function parseMarkdownIncremental<
3502
+ const Plugins extends ReadonlyArray<MarkdownPluginEntry>,
3503
+ >(
3504
+ input: string,
3505
+ state: IncrementalState<false>,
3506
+ options: IncrementalParseOptionsWithoutPlugins & {plugins: Plugins},
3507
+ ): BlockNode<MarkdownExtensionsOf<Plugins>>[];
3508
+ export function parseMarkdownIncremental<
3509
+ const Plugins extends ReadonlyArray<MarkdownPluginEntry>,
3510
+ >(
3511
+ input: string,
3512
+ state: IncrementalState<true>,
3513
+ options: IncrementalMathParseOptionsWithoutPlugins & {plugins: Plugins},
3514
+ ): BlockNodeWithMath<MarkdownExtensionsOf<Plugins>>[];
2620
3515
  export function parseMarkdownIncremental(
2621
3516
  input: string,
2622
3517
  state: IncrementalState<boolean>,
2623
3518
  arg?: ReadonlySet<string> | RuntimeParseOptions,
2624
- ): BlockNodeWithMath[] {
2625
- const opts = resolveOptions(arg);
3519
+ ): RuntimeBlockNode[] {
3520
+ const withRanges = wantsLegacyRanges(arg);
3521
+ const root = parseMarkdownAstIncremental(input, state, arg);
3522
+ const cache = incrementalCaches.get(state) ?? makeIncrementalCache(state);
3523
+ const projected = projectMarkdownRoot(
3524
+ root,
3525
+ withRanges,
3526
+ cache.projectionCache,
3527
+ );
3528
+ if (cache.projectedRevision !== cache.settledRevision) {
3529
+ cache.projectedSettledBlocks = cache.settledAstBlocks.map(block =>
3530
+ projectBlockNode(block, cache.projectionCache, withRanges),
3531
+ );
3532
+ cache.projectedRevision = cache.settledRevision;
3533
+ }
3534
+ state.settledBlocks =
3535
+ cache.projectedSettledBlocks as typeof state.settledBlocks;
3536
+ return projected;
3537
+ }
3538
+
3539
+ /** @internal Canonical incremental parse used by Markdown rendering. */
3540
+ export function parseMarkdownAstIncremental(
3541
+ input: string,
3542
+ state: IncrementalState<boolean>,
3543
+ arg?: ReadonlySet<string> | RuntimeParseOptions,
3544
+ ): MarkdownAstRoot<RuntimeExtensionNode> {
3545
+ const opts = resolveOptions(arg, true);
3546
+ const root: MarkdownAstRoot<RuntimeExtensionNode> = {
3547
+ type: 'root',
3548
+ children: parseMarkdownIncrementalAstBlocks(input, state, opts),
3549
+ };
3550
+ return applyMarkdownTransforms(
3551
+ root,
3552
+ opts.plugins,
3553
+ input,
3554
+ opts.isFinal,
3555
+ 'block',
3556
+ );
3557
+ }
3558
+
3559
+ function parseMarkdownIncrementalAstBlocks(
3560
+ input: string,
3561
+ state: IncrementalState<boolean>,
3562
+ opts: ResolvedOptions,
3563
+ ): MarkdownAstBlockContent<RuntimeExtensionNode>[] {
2626
3564
  const cache = incrementalCaches.get(state) ?? makeIncrementalCache(state);
2627
3565
  let reparseSettled = false;
2628
3566
 
3567
+ const nextSourceIdsKey = sourceIdsSignature(opts.sourceIds);
3568
+ const nextPluginSyntaxIdentity = opts.plugins?.syntaxIdentity ?? '';
2629
3569
  // Invalidate cache when an option that changes parsed nodes flips — cached
2630
3570
  // settled blocks were parsed with the previous setting and would otherwise
2631
3571
  // be reused unchanged.
2632
3572
  if (
2633
3573
  state.autolink !== opts.autolink ||
2634
3574
  state.math !== opts.math ||
2635
- Boolean(state.sourceRanges) !== Boolean(opts.sourceRanges)
3575
+ Boolean(state.sourceRanges) !== Boolean(opts.sourceRanges) ||
3576
+ state.sourceIdsKey !== nextSourceIdsKey ||
3577
+ state.pluginSyntaxIdentity !== nextPluginSyntaxIdentity
2636
3578
  ) {
2637
3579
  reparseSettled = true;
2638
3580
  state.autolink = opts.autolink;
2639
3581
  state.math = opts.math;
2640
3582
  state.sourceRanges = opts.sourceRanges;
3583
+ state.sourceIdsKey = nextSourceIdsKey;
3584
+ state.pluginSyntaxIdentity = nextPluginSyntaxIdentity;
2641
3585
  }
2642
3586
  if (input === '') {
2643
3587
  resetIncrementalCache(state, cache);
2644
3588
  return [];
2645
3589
  }
2646
-
2647
3590
  // The settled prefix is only reusable while the input still contains it
2648
3591
  // verbatim. Lengths alone cannot tell: a same-length or longer replacement
2649
3592
  // (new args after reusing a state) disagrees with the prefix without ever
@@ -2660,6 +3603,8 @@ export function parseMarkdownIncremental(
2660
3603
  state.autolink = opts.autolink;
2661
3604
  state.math = opts.math;
2662
3605
  state.sourceRanges = opts.sourceRanges;
3606
+ state.sourceIdsKey = nextSourceIdsKey;
3607
+ state.pluginSyntaxIdentity = nextPluginSyntaxIdentity;
2663
3608
  }
2664
3609
 
2665
3610
  // The recurring parse costs are confined to the mutable suffix: splitting,
@@ -2744,28 +3689,52 @@ export function parseMarkdownIncremental(
2744
3689
  ? trimOpenDisplayMath(unsettledRaw)
2745
3690
  : trimUnsettledStructural(unsettledRaw);
2746
3691
 
3692
+ const legacyRanges = opts.sourceRanges === true;
2747
3693
  let parsedSettledBlocks = 0;
2748
3694
  if (reparseSettled) {
2749
- state.settledBlocks = state.settledText
3695
+ cache.settledAstBlocks = state.settledText
2750
3696
  ? parseMarkdownImpl(state.settledText, parseOpts)
2751
3697
  : [];
2752
- parsedSettledBlocks = state.settledBlocks.length;
3698
+ parsedSettledBlocks = cache.settledAstBlocks.length;
3699
+ cache.settledRevision++;
3700
+ cache.projectedSettledBlocks = cache.settledAstBlocks.map(block =>
3701
+ projectBlockNode(block, cache.projectionCache, legacyRanges),
3702
+ );
3703
+ cache.projectedRevision = cache.settledRevision;
2753
3704
  } else if (settledDelta !== '') {
2754
3705
  const deltaBlocks = parseMarkdownImpl(
2755
3706
  settledDelta,
2756
3707
  atOffset(parseOpts, oldSettledEnd),
2757
3708
  );
2758
- appendSettledBlocks(state.settledBlocks, deltaBlocks);
3709
+ const mergeIndex = cache.settledAstBlocks.length - 1;
3710
+ const mergedList = appendSettledBlocks(cache.settledAstBlocks, deltaBlocks);
2759
3711
  parsedSettledBlocks = deltaBlocks.length;
3712
+ cache.settledRevision++;
3713
+ const projectedDelta = deltaBlocks.map(block =>
3714
+ projectBlockNode(block, cache.projectionCache, legacyRanges),
3715
+ );
3716
+ if (mergedList) {
3717
+ const canonicalMerged = cache.settledAstBlocks[mergeIndex];
3718
+ const projectedMerged = projectBlockNode(
3719
+ canonicalMerged,
3720
+ cache.projectionCache,
3721
+ legacyRanges,
3722
+ );
3723
+ cache.projectedSettledBlocks[mergeIndex] = projectedMerged;
3724
+ cache.projectedSettledBlocks.push(...projectedDelta.slice(1));
3725
+ } else {
3726
+ cache.projectedSettledBlocks.push(...projectedDelta);
3727
+ }
3728
+ cache.projectedRevision = cache.settledRevision;
2760
3729
  }
2761
3730
 
2762
3731
  // The unsettled tail is trimmed before parsing, so its offset in the
2763
3732
  // document is where that trimmed text actually starts — not the boundary,
2764
- // which is a line index. If it somehow can't be located, parse it without
2765
- // ranges rather than report wrong ones. Only worth searching for when
2766
- // ranges were asked for: this runs on every streamed chunk.
3733
+ // which is a line index. If it somehow cannot be located, parse it without
3734
+ // positions rather than report wrong ones. The search starts at the
3735
+ // settled end, so it scans the tail and never the prefix.
2767
3736
  const unsettledStart =
2768
- unsettledText && opts.sourceRanges
3737
+ unsettledText && opts.astPositions === true
2769
3738
  ? input.indexOf(unsettledText, cache.settledEnd)
2770
3739
  : -1;
2771
3740
  const unsettledBlocks = unsettledText
@@ -2790,7 +3759,7 @@ export function parseMarkdownIncremental(
2790
3759
  definitionCharacters: settledDelta.length + unsettledInput.length,
2791
3760
  renderedBlocks: parsedSettledBlocks + unsettledBlocks.length,
2792
3761
  };
2793
- return mergeSettledBlocks(state.settledBlocks, unsettledBlocks);
3762
+ return mergeSettledBlocks(cache.settledAstBlocks, unsettledBlocks);
2794
3763
  }
2795
3764
 
2796
3765
  // ---------------------------------------------------------------------------
@@ -2801,30 +3770,6 @@ export function parseMarkdownIncremental(
2801
3770
  // derives its item ids from the same functions, so outline hash links always
2802
3771
  // resolve to a rendered heading by construction.
2803
3772
 
2804
- /** Flatten inline nodes into their plain text content. */
2805
- export function inlineText(nodes: ReadonlyArray<InlineNodeWithMath>): string {
2806
- return nodes
2807
- .map(node => {
2808
- switch (node.type) {
2809
- case 'text':
2810
- case 'code':
2811
- case 'math':
2812
- return node.type === 'math' ? node.value : node.content;
2813
- case 'bold':
2814
- case 'italic':
2815
- case 'strikethrough':
2816
- case 'link':
2817
- return inlineText(node.children);
2818
- case 'image':
2819
- return node.alt;
2820
- case 'citation':
2821
- case 'break':
2822
- return '';
2823
- }
2824
- })
2825
- .join('');
2826
- }
2827
-
2828
3773
  /** Turn heading text into a URL-safe slug (lowercase, hyphen-separated). */
2829
3774
  export function slugify(value: string): string {
2830
3775
  return value