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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (305) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/dist/BottomSheet/BottomSheet.d.ts +1 -1
  3. package/dist/BottomSheet/BottomSheet.d.ts.map +1 -1
  4. package/dist/BottomSheet/BottomSheet.js +3 -1
  5. package/dist/BottomSheet/BottomSheetPanel.d.ts +7 -5
  6. package/dist/BottomSheet/BottomSheetPanel.d.ts.map +1 -1
  7. package/dist/BottomSheet/BottomSheetPanel.js +56 -19
  8. package/dist/Chat/ChatComposerInput.d.ts.map +1 -1
  9. package/dist/Chat/ChatComposerInput.js +2 -2
  10. package/dist/CheckboxInput/CheckboxInput.d.ts.map +1 -1
  11. package/dist/CheckboxInput/CheckboxInput.js +12 -2
  12. package/dist/Collapsible/Collapsible.d.ts.map +1 -1
  13. package/dist/Collapsible/Collapsible.js +6 -1
  14. package/dist/CommandPalette/CommandPaletteInput.d.ts.map +1 -1
  15. package/dist/CommandPalette/CommandPaletteInput.js +1 -1
  16. package/dist/ComplexSelector/ComplexSelector.d.ts.map +1 -1
  17. package/dist/ComplexSelector/ComplexSelector.js +1 -1
  18. package/dist/DateInput/DateInput.d.ts.map +1 -1
  19. package/dist/DateInput/DateInput.js +4 -4
  20. package/dist/DateInput/NativeDateField.d.ts.map +1 -1
  21. package/dist/DateInput/NativeDateField.js +16 -16
  22. package/dist/DateInput/TouchDateField.d.ts.map +1 -1
  23. package/dist/DateInput/TouchDateField.js +2 -2
  24. package/dist/DateRangeInput/DateRangeInput.d.ts +3 -0
  25. package/dist/DateRangeInput/DateRangeInput.d.ts.map +1 -1
  26. package/dist/DateRangeInput/DateRangeInput.js +20 -13
  27. package/dist/DateTimeInput/DateTimeInput.d.ts.map +1 -1
  28. package/dist/DateTimeInput/DateTimeInput.js +8 -8
  29. package/dist/DateTimeInput/TouchDateTimeField.d.ts.map +1 -1
  30. package/dist/DateTimeInput/TouchDateTimeField.js +4 -4
  31. package/dist/DateTimeInput/nativePickerSegmentStyles.d.ts +9 -3
  32. package/dist/DateTimeInput/nativePickerSegmentStyles.d.ts.map +1 -1
  33. package/dist/DateTimeInput/nativePickerSegmentStyles.js +3 -3
  34. package/dist/Dialog/DialogHeader.d.ts +1 -1
  35. package/dist/Dialog/DialogHeader.d.ts.map +1 -1
  36. package/dist/Dialog/DialogHeader.js +10 -7
  37. package/dist/Field/PanelSearchInput.d.ts.map +1 -1
  38. package/dist/Field/PanelSearchInput.js +1 -1
  39. package/dist/FileInput/FileInput.d.ts.map +1 -1
  40. package/dist/FileInput/FileInput.js +16 -10
  41. package/dist/Kbd/Kbd.d.ts +5 -3
  42. package/dist/Kbd/Kbd.d.ts.map +1 -1
  43. package/dist/Kbd/Kbd.js +36 -42
  44. package/dist/Link/Link.d.ts.map +1 -1
  45. package/dist/Link/Link.js +6 -2
  46. package/dist/Markdown/Markdown.d.ts +18 -4
  47. package/dist/Markdown/Markdown.d.ts.map +1 -1
  48. package/dist/Markdown/Markdown.fr23.bench.d.ts +2 -0
  49. package/dist/Markdown/Markdown.fr23.bench.d.ts.map +1 -0
  50. package/dist/Markdown/Markdown.fr23.bench.js +150 -0
  51. package/dist/Markdown/Markdown.fr23.sampling.d.ts +2 -0
  52. package/dist/Markdown/Markdown.fr23.sampling.d.ts.map +1 -0
  53. package/dist/Markdown/Markdown.fr23.sampling.js +43 -0
  54. package/dist/Markdown/Markdown.js +311 -114
  55. package/dist/Markdown/ast.d.ts +162 -0
  56. package/dist/Markdown/ast.d.ts.map +1 -0
  57. package/dist/Markdown/ast.js +74 -0
  58. package/dist/Markdown/index.d.ts +5 -3
  59. package/dist/Markdown/index.d.ts.map +1 -1
  60. package/dist/Markdown/index.js +3 -2
  61. package/dist/Markdown/parser/index.d.ts +10 -0
  62. package/dist/Markdown/parser/index.d.ts.map +1 -0
  63. package/dist/Markdown/parser/index.js +10 -0
  64. package/dist/Markdown/parser.d.ts +220 -38
  65. package/dist/Markdown/parser.d.ts.map +1 -1
  66. package/dist/Markdown/parser.js +1072 -188
  67. package/dist/Markdown/plugins/frontmatter.d.ts +32 -0
  68. package/dist/Markdown/plugins/frontmatter.d.ts.map +1 -0
  69. package/dist/Markdown/plugins/frontmatter.js +132 -0
  70. package/dist/Markdown/plugins/index.d.ts +16 -0
  71. package/dist/Markdown/plugins/index.d.ts.map +1 -0
  72. package/dist/Markdown/plugins/index.js +13 -0
  73. package/dist/Markdown/plugins/protocol.d.ts +191 -0
  74. package/dist/Markdown/plugins/protocol.d.ts.map +1 -0
  75. package/dist/Markdown/plugins/protocol.js +957 -0
  76. package/dist/Markdown/plugins/semanticFence.d.ts +36 -0
  77. package/dist/Markdown/plugins/semanticFence.d.ts.map +1 -0
  78. package/dist/Markdown/plugins/semanticFence.js +213 -0
  79. package/dist/Markdown/plugins/sourceDecoration.d.ts +76 -0
  80. package/dist/Markdown/plugins/sourceDecoration.d.ts.map +1 -0
  81. package/dist/Markdown/plugins/sourceDecoration.js +393 -0
  82. package/dist/Markdown/plugins/textTransform.d.ts +30 -0
  83. package/dist/Markdown/plugins/textTransform.d.ts.map +1 -0
  84. package/dist/Markdown/plugins/textTransform.js +374 -0
  85. package/dist/Markdown/remark.d.ts +196 -0
  86. package/dist/Markdown/remark.d.ts.map +1 -0
  87. package/dist/Markdown/remark.js +1062 -0
  88. package/dist/Markdown/url.d.ts +4 -0
  89. package/dist/Markdown/url.d.ts.map +1 -0
  90. package/dist/Markdown/url.js +25 -0
  91. package/dist/Markdown/utils.d.ts +4 -2
  92. package/dist/Markdown/utils.d.ts.map +1 -1
  93. package/dist/Markdown/utils.js +2 -1
  94. package/dist/NumberInput/NumberInput.d.ts.map +1 -1
  95. package/dist/NumberInput/NumberInput.js +4 -4
  96. package/dist/Outline/index.d.ts +1 -0
  97. package/dist/Outline/index.d.ts.map +1 -1
  98. package/dist/Outline/parseOutlineFromMarkdown.d.ts +7 -1
  99. package/dist/Outline/parseOutlineFromMarkdown.d.ts.map +1 -1
  100. package/dist/Outline/parseOutlineFromMarkdown.js +14 -8
  101. package/dist/Outline/useOutlineFromMarkdown.d.ts +3 -1
  102. package/dist/Outline/useOutlineFromMarkdown.d.ts.map +1 -1
  103. package/dist/Outline/useOutlineFromMarkdown.js +8 -3
  104. package/dist/PowerSearch/PowerSearchEditPopover.d.ts.map +1 -1
  105. package/dist/PowerSearch/PowerSearchEditPopover.js +46 -30
  106. package/dist/ProgressBar/ProgressBar.d.ts.map +1 -1
  107. package/dist/ProgressBar/ProgressBar.js +36 -31
  108. package/dist/RadioList/RadioListItem.d.ts.map +1 -1
  109. package/dist/RadioList/RadioListItem.js +13 -1
  110. package/dist/SegmentedControl/SegmentedControlItem.d.ts.map +1 -1
  111. package/dist/SegmentedControl/SegmentedControlItem.js +5 -5
  112. package/dist/Selector/Selector.d.ts.map +1 -1
  113. package/dist/Selector/Selector.js +1 -1
  114. package/dist/SideNav/SideNav.d.ts +2 -1
  115. package/dist/SideNav/SideNav.d.ts.map +1 -1
  116. package/dist/SideNav/SideNav.js +9 -3
  117. package/dist/Slider/Slider.d.ts.map +1 -1
  118. package/dist/Slider/Slider.js +19 -10
  119. package/dist/Spinner/Spinner.d.ts +1 -1
  120. package/dist/Spinner/Spinner.d.ts.map +1 -1
  121. package/dist/Spinner/Spinner.js +45 -21
  122. package/dist/Switch/Switch.d.ts.map +1 -1
  123. package/dist/Switch/Switch.js +11 -0
  124. package/dist/TabList/Tab.d.ts +1 -1
  125. package/dist/TabList/Tab.d.ts.map +1 -1
  126. package/dist/TabList/Tab.js +20 -7
  127. package/dist/TextArea/TextArea.d.ts.map +1 -1
  128. package/dist/TextArea/TextArea.js +1 -1
  129. package/dist/TextInput/TextInput.d.ts.map +1 -1
  130. package/dist/TextInput/TextInput.js +2 -2
  131. package/dist/TimeInput/TimeInput.d.ts.map +1 -1
  132. package/dist/TimeInput/TimeInput.js +4 -4
  133. package/dist/ToggleButton/ToggleButton.d.ts +2 -1
  134. package/dist/ToggleButton/ToggleButton.d.ts.map +1 -1
  135. package/dist/ToggleButton/ToggleButton.js +7 -1
  136. package/dist/Typeahead/BaseTypeahead.d.ts.map +1 -1
  137. package/dist/Typeahead/BaseTypeahead.js +16 -7
  138. package/dist/astryx.css +19 -8
  139. package/dist/hooks/scrollKeyboardDelegation.d.ts +3 -0
  140. package/dist/hooks/scrollKeyboardDelegation.d.ts.map +1 -0
  141. package/dist/hooks/scrollKeyboardDelegation.js +146 -0
  142. package/dist/hooks/useScrollableArea.d.ts +6 -2
  143. package/dist/hooks/useScrollableArea.d.ts.map +1 -1
  144. package/dist/hooks/useScrollableArea.js +17 -5
  145. package/dist/utils/interactionOverlay.stylex.d.ts +8 -0
  146. package/dist/utils/interactionOverlay.stylex.d.ts.map +1 -1
  147. package/dist/utils/interactionOverlay.stylex.js +9 -0
  148. package/locales/en.json +16 -0
  149. package/locales/pseudo.json +12 -0
  150. package/package.json +22 -5
  151. package/scripts/agent-doc-state.mjs +1 -1
  152. package/src/BottomSheet/BottomSheet.doc.mjs +8 -1
  153. package/src/BottomSheet/BottomSheet.spec.md +46 -20
  154. package/src/BottomSheet/BottomSheet.test.tsx +6 -3
  155. package/src/BottomSheet/BottomSheet.tsx +3 -1
  156. package/src/BottomSheet/BottomSheetKeyboard.test.tsx +195 -0
  157. package/src/BottomSheet/BottomSheetPanel.test.tsx +11 -1
  158. package/src/BottomSheet/BottomSheetPanel.tsx +49 -16
  159. package/src/BottomSheet/__tests__/BottomSheetKeyboard.a11y.browser.spec.ts +344 -0
  160. package/src/Button/__tests__/Button.a11y.chromium.spec.ts +17 -1
  161. package/src/Button/__tests__/Button.a11y.known-failures.ts +0 -29
  162. package/src/Button/__tests__/Button.a11y.renders.tsx +9 -2
  163. package/src/Button/__tests__/Button.a11y.states.ts +9 -0
  164. package/src/Chat/ChatComposerInput.doc.mjs +3 -3
  165. package/src/Chat/ChatComposerInput.tsx +10 -2
  166. package/src/CheckboxInput/CheckboxInput.doc.mjs +11 -0
  167. package/src/CheckboxInput/CheckboxInput.test.tsx +34 -0
  168. package/src/CheckboxInput/CheckboxInput.tsx +21 -1
  169. package/src/ClickableCard/ClickableCard.test.tsx +102 -5
  170. package/src/Collapsible/Collapsible.doc.mjs +11 -0
  171. package/src/Collapsible/Collapsible.test.tsx +21 -0
  172. package/src/Collapsible/Collapsible.tsx +5 -0
  173. package/src/CommandPalette/CommandPaletteInput.tsx +6 -1
  174. package/src/ComplexSelector/ComplexSelector.tsx +6 -1
  175. package/src/DateInput/DateInput.tsx +6 -1
  176. package/src/DateInput/NativeDateField.tsx +24 -8
  177. package/src/DateInput/TouchDateField.tsx +6 -2
  178. package/src/DateRangeInput/DateRangeInput.doc.mjs +35 -7
  179. package/src/DateRangeInput/DateRangeInput.spec.md +203 -0
  180. package/src/DateRangeInput/DateRangeInput.test.tsx +100 -4
  181. package/src/DateRangeInput/DateRangeInput.tsx +35 -21
  182. package/src/DateTimeInput/DateTimeInput.tsx +6 -1
  183. package/src/DateTimeInput/TouchDateTimeField.tsx +6 -1
  184. package/src/DateTimeInput/nativePickerSegmentStyles.ts +24 -8
  185. package/src/Dialog/Dialog.doc.mjs +3 -0
  186. package/src/Dialog/Dialog.spec.md +1 -1
  187. package/src/Dialog/DialogHeader.doc.mjs +38 -0
  188. package/src/Dialog/DialogHeader.test.tsx +49 -0
  189. package/src/Dialog/DialogHeader.tsx +23 -4
  190. package/src/Dialog/modules/DialogHeader.spec.md +152 -0
  191. package/src/Field/PanelSearchInput.tsx +5 -2
  192. package/src/FieldStatus/__tests__/StatusMessage.a11y.states.ts +1 -1
  193. package/src/FileInput/FileInput.doc.mjs +2 -0
  194. package/src/FileInput/FileInput.spec.md +199 -0
  195. package/src/FileInput/FileInput.test.tsx +14 -0
  196. package/src/FileInput/FileInput.tsx +24 -5
  197. package/src/Kbd/Kbd.doc.mjs +3 -3
  198. package/src/Kbd/Kbd.test.tsx +57 -1
  199. package/src/Kbd/Kbd.tsx +57 -37
  200. package/src/Layout/LayoutFooter.doc.mjs +17 -0
  201. package/src/Link/Link.doc.mjs +11 -0
  202. package/src/Link/Link.test.tsx +24 -0
  203. package/src/Link/Link.tsx +5 -0
  204. package/src/Markdown/Markdown.doc.mjs +439 -42
  205. package/src/Markdown/Markdown.fr23.bench.ts +174 -0
  206. package/src/Markdown/Markdown.fr23.perf.test.ts +249 -0
  207. package/src/Markdown/Markdown.fr23.sampling.ts +49 -0
  208. package/src/Markdown/Markdown.helpers.perf.test.ts +489 -0
  209. package/src/Markdown/Markdown.public.test.ts +280 -0
  210. package/src/Markdown/Markdown.spec.md +304 -72
  211. package/src/Markdown/Markdown.test.tsx +118 -3
  212. package/src/Markdown/Markdown.tsx +489 -129
  213. package/src/Markdown/ast.test.ts +307 -0
  214. package/src/Markdown/ast.ts +337 -0
  215. package/src/Markdown/incremental.test.ts +187 -7
  216. package/src/Markdown/index.ts +43 -1
  217. package/src/Markdown/modules/remark.spec.md +269 -0
  218. package/src/Markdown/parser/index.ts +64 -0
  219. package/src/Markdown/parser.perf.test.ts +3 -1
  220. package/src/Markdown/parser.public.test.ts +103 -0
  221. package/src/Markdown/parser.test.ts +122 -0
  222. package/src/Markdown/parser.ts +1721 -248
  223. package/src/Markdown/plugins/frontmatter.test.tsx +125 -0
  224. package/src/Markdown/plugins/frontmatter.ts +198 -0
  225. package/src/Markdown/plugins/index.ts +53 -0
  226. package/src/Markdown/plugins/protocol.test.tsx +1022 -0
  227. package/src/Markdown/plugins/protocol.ts +1618 -0
  228. package/src/Markdown/plugins/semanticFence.test.tsx +546 -0
  229. package/src/Markdown/plugins/semanticFence.ts +424 -0
  230. package/src/Markdown/plugins/sourceDecoration.test.tsx +799 -0
  231. package/src/Markdown/plugins/sourceDecoration.ts +539 -0
  232. package/src/Markdown/plugins/textTransform.test.ts +467 -0
  233. package/src/Markdown/plugins/textTransform.ts +554 -0
  234. package/src/Markdown/remark.test.tsx +1877 -0
  235. package/src/Markdown/remark.ts +1574 -0
  236. package/src/Markdown/url.ts +30 -0
  237. package/src/Markdown/utils.ts +19 -1
  238. package/src/MultiSelector/MultiSelector.test.tsx +1 -35
  239. package/src/MultiSelector/__tests__/MultiSelector.listbox.a11y.test.tsx +128 -0
  240. package/src/NumberInput/NumberInput.tsx +6 -1
  241. package/src/Outline/Outline.doc.mjs +7 -5
  242. package/src/Outline/Outline.spec.md +1 -1
  243. package/src/Outline/index.ts +1 -0
  244. package/src/Outline/modules/parseOutlineFromMarkdown.spec.md +142 -0
  245. package/src/Outline/parseOutlineFromMarkdown.ts +40 -10
  246. package/src/Outline/useOutlineFromMarkdown.ts +15 -3
  247. package/src/PowerSearch/PowerSearch.doc.mjs +2 -2
  248. package/src/PowerSearch/PowerSearchEditPopover.test.tsx +150 -1
  249. package/src/PowerSearch/PowerSearchEditPopover.tsx +51 -28
  250. package/src/ProgressBar/ProgressBar.test.tsx +29 -11
  251. package/src/ProgressBar/ProgressBar.tsx +59 -72
  252. package/src/RadioList/RadioList.doc.mjs +11 -0
  253. package/src/RadioList/RadioList.test.tsx +32 -0
  254. package/src/RadioList/RadioListItem.tsx +25 -1
  255. package/src/ScrollableArea/modules/useScrollableArea.spec.md +50 -22
  256. package/src/SegmentedControl/SegmentedControl.doc.mjs +2 -2
  257. package/src/SegmentedControl/SegmentedControl.test.tsx +31 -0
  258. package/src/SegmentedControl/SegmentedControlItem.tsx +6 -9
  259. package/src/Selector/Selector.spec.md +12 -9
  260. package/src/Selector/Selector.tsx +6 -1
  261. package/src/Selector/__tests__/Listbox.a11y.chromium.spec.ts +130 -0
  262. package/src/Selector/__tests__/Listbox.a11y.dom.ts +38 -0
  263. package/src/Selector/__tests__/Listbox.a11y.inventory.test.ts +63 -0
  264. package/src/Selector/__tests__/Listbox.a11y.known-failures.ts +37 -0
  265. package/src/Selector/__tests__/Listbox.a11y.states.ts +232 -0
  266. package/src/Selector/__tests__/Selector.listbox.a11y.test.tsx +121 -0
  267. package/src/SideNav/SideNav.doc.mjs +1 -1
  268. package/src/SideNav/SideNav.test.tsx +10 -0
  269. package/src/SideNav/SideNav.tsx +14 -2
  270. package/src/Slider/Slider.doc.mjs +27 -0
  271. package/src/Slider/Slider.spec.md +61 -47
  272. package/src/Slider/Slider.test.tsx +146 -0
  273. package/src/Slider/Slider.tsx +37 -13
  274. package/src/Spinner/Spinner.doc.mjs +6 -3
  275. package/src/Spinner/Spinner.test.tsx +41 -0
  276. package/src/Spinner/Spinner.tsx +62 -32
  277. package/src/Switch/Switch.doc.mjs +11 -0
  278. package/src/Switch/Switch.test.tsx +16 -0
  279. package/src/Switch/Switch.tsx +28 -0
  280. package/src/TabList/Tab.tsx +35 -7
  281. package/src/TabList/TabList.doc.mjs +11 -0
  282. package/src/TabList/TabList.test.tsx +66 -0
  283. package/src/TabList/__tests__/Tabs.a11y.known-failures.ts +1 -34
  284. package/src/TextArea/TextArea.tsx +6 -1
  285. package/src/TextInput/TextInput.test.tsx +20 -1
  286. package/src/TextInput/TextInput.tsx +6 -1
  287. package/src/TimeInput/TimeInput.tsx +6 -1
  288. package/src/ToggleButton/ToggleButton.test.tsx +133 -0
  289. package/src/ToggleButton/ToggleButton.tsx +9 -2
  290. package/src/ToggleButton/__tests__/ToggleButton.a11y.chromium.spec.ts +209 -0
  291. package/src/Tokenizer/Tokenizer.spec.md +142 -75
  292. package/src/TreeList/TreeList.spec.md +123 -75
  293. package/src/Typeahead/BaseTypeahead.spec.md +4 -3
  294. package/src/Typeahead/BaseTypeahead.tsx +21 -7
  295. package/src/Typeahead/Typeahead.test.tsx +53 -0
  296. package/src/__tests__/PressedState.a11y.chromium.spec.ts +813 -0
  297. package/src/__tests__/pressState.ts +93 -0
  298. package/src/hooks/scrollKeyboardDelegation.test.ts +155 -0
  299. package/src/hooks/scrollKeyboardDelegation.ts +233 -0
  300. package/src/hooks/useScrollableArea.doc.mjs +15 -3
  301. package/src/hooks/useScrollableArea.test.tsx +59 -1
  302. package/src/hooks/useScrollableArea.ts +34 -10
  303. package/src/inputFontFloor.test.ts +106 -0
  304. package/src/theme/derivedVarRegistry.test.ts +6 -4
  305. package/src/utils/interactionOverlay.stylex.ts +20 -0
@@ -4,12 +4,12 @@
4
4
 
5
5
  /**
6
6
  * @file Markdown.tsx
7
- * @input Markdown string, parser AST types
8
- * @output Exports Markdown component and MarkdownProps
7
+ * @input Markdown string, canonical AST, optional plugins and custom renderers
8
+ * @output Exports Markdown component, MarkdownProps, and renderer contracts
9
9
  * @position Core implementation; renders markdown as Astryx components
10
10
  */
11
11
 
12
- import {useMemo, useRef} from 'react';
12
+ import {Component, Suspense, useMemo, useRef} from 'react';
13
13
  import type React from 'react';
14
14
  import {Fragment} from 'react';
15
15
  import * as stylex from '@stylexjs/stylex';
@@ -48,20 +48,43 @@ import type {CitationSource} from '../Citation/Citation';
48
48
  import {useLinkComponent} from '../Link/useLinkComponent';
49
49
  import type {LinkComponentType} from '../Link/types';
50
50
  import {
51
- parseInline,
52
- parseMarkdown,
53
- parseMarkdownIncremental,
51
+ parseInlineAst,
52
+ parseMarkdownAst,
53
+ parseMarkdownAstIncremental,
54
54
  createIncrementalState,
55
55
  trimStreamingArtifacts,
56
- inlineText,
57
56
  slugify,
58
57
  uniqueSlug,
59
58
  } from './parser';
60
- import type {BlockNode, InlineNode, IncrementalState} from './parser';
59
+ import type {IncrementalState, MathParseOptions, ParseOptions} from './parser';
60
+ import {getMarkdownAstLegacyCodeLanguage, markdownAstText} from './ast';
61
+ import type {
62
+ MarkdownAstBlockContent,
63
+ MarkdownAstPhrasingContent,
64
+ MarkdownAstTable,
65
+ } from './ast';
66
+ import {
67
+ applyMarkdownTransforms,
68
+ getMarkdownExtensionRenderer,
69
+ markdownExtensionText,
70
+ prepareMarkdownPlugins,
71
+ reportMarkdownPluginFailure,
72
+ } from './plugins/protocol';
73
+ import {getMarkdownFenceProposal} from './plugins/semanticFence';
74
+ import type {
75
+ MarkdownExtensionNode,
76
+ MarkdownPluginEntry,
77
+ PreparedMarkdownPlugins,
78
+ } from './plugins/protocol';
79
+ import {sanitizeMarkdownUrl} from './url';
61
80
  import {themeProps} from '../utils/themeProps';
62
81
  import {useTranslator, type TranslatorFn} from '../i18n';
63
82
 
64
83
  type SyncReactNode = Exclude<React.ReactNode, Promise<unknown>>;
84
+ type RenderExtensionNode = MarkdownExtensionNode;
85
+ type RenderInlineNode = MarkdownAstPhrasingContent<RenderExtensionNode>;
86
+ type RenderBlockNode = MarkdownAstBlockContent<RenderExtensionNode>;
87
+ type RenderTable = MarkdownAstTable<RenderExtensionNode>;
65
88
 
66
89
  // ---------------------------------------------------------------------------
67
90
  // Props
@@ -70,7 +93,7 @@ type SyncReactNode = Exclude<React.ReactNode, Promise<unknown>>;
70
93
  /**
71
94
  * A plugin that transforms text patterns into custom React elements
72
95
  * inside Markdown. Applied to parsed text nodes only — code blocks,
73
- * inline code, and other non-prose contexts are unaffected.
96
+ * inline code, math, and other non-prose contexts are unaffected.
74
97
  *
75
98
  * Follows Lexical's TextMatchTransformer architecture:
76
99
  * - `pattern` for initial regex matching
@@ -102,6 +125,14 @@ export type MarkdownSource = CitationSource;
102
125
  export interface MarkdownComponents {
103
126
  code?: React.ComponentType<{code: string; language?: string}>;
104
127
  inlineCode?: React.ComponentType<{children: string}>;
128
+ /**
129
+ * Renders opt-in `$…$` and `$$…$$` math. Supplying this renderer enables
130
+ * math parsing; Astryx passes the raw expression and does not execute it.
131
+ */
132
+ math?: React.ComponentType<{
133
+ value: string;
134
+ display: 'inline' | 'block';
135
+ }>;
105
136
  citation?: React.ComponentType<{
106
137
  source: CitationSource;
107
138
  number: number;
@@ -126,7 +157,9 @@ export interface MarkdownComponents {
126
157
  hr?: React.ComponentType<object>;
127
158
  }
128
159
 
129
- export interface MarkdownProps extends BaseProps<HTMLElement> {
160
+ export interface MarkdownProps<
161
+ Plugins extends ReadonlyArray<MarkdownPluginEntry> = readonly [],
162
+ > extends BaseProps<HTMLElement> {
130
163
  ref?: React.Ref<HTMLDivElement> | React.Ref<HTMLSpanElement>;
131
164
  children: string;
132
165
  /**
@@ -183,10 +216,15 @@ export interface MarkdownProps extends BaseProps<HTMLElement> {
183
216
  components?: Partial<MarkdownComponents>;
184
217
  /**
185
218
  * Plugins that transform text patterns into custom React elements.
186
- * Applied to text nodes after parsing — code blocks and inline code
219
+ * Applied to text nodes after parsing — code blocks, inline code, and math
187
220
  * are unaffected. Patterns are matched in order; first match wins
188
221
  * for overlapping ranges.
189
222
  */
223
+ /** Ordered syntax, immutable transforms, and typed extension renderers. */
224
+ plugins?: Plugins;
225
+ /**
226
+ * Legacy text-match renderers. Unchanged and independent from `plugins`.
227
+ */
190
228
  inlinePlugins?: MarkdownInlinePlugin[];
191
229
  /**
192
230
  * Opt-in autolinking of bare URLs and emails inside text.
@@ -473,22 +511,25 @@ interface StreamingCursor {
473
511
  * Count the total text characters in inline nodes without rendering.
474
512
  * Used to advance the cursor past a block that will be faded as a whole unit.
475
513
  */
476
- function countInlineTextLength(nodes: InlineNode[]): number {
514
+ function countInlineTextLength(nodes: ReadonlyArray<RenderInlineNode>): number {
477
515
  let len = 0;
478
516
  for (const node of nodes) {
479
517
  switch (node.type) {
480
518
  case 'text':
481
- len += node.content.length;
519
+ len += node.value.length;
482
520
  break;
483
- case 'code':
484
- len += node.content.length;
521
+ case 'inlineCode':
522
+ len += node.value.length;
523
+ break;
524
+ case 'inlineMath':
525
+ len += node.value.length;
485
526
  break;
486
527
  case 'image':
487
528
  len += node.alt.length;
488
529
  break;
489
- case 'bold':
490
- case 'italic':
491
- case 'strikethrough':
530
+ case 'strong':
531
+ case 'emphasis':
532
+ case 'delete':
492
533
  case 'link':
493
534
  len += countInlineTextLength(node.children);
494
535
  break;
@@ -498,6 +539,9 @@ function countInlineTextLength(nodes: InlineNode[]): number {
498
539
  case 'citation':
499
540
  len += 1;
500
541
  break;
542
+ case 'extension':
543
+ len += node.source?.length ?? 0;
544
+ break;
501
545
  }
502
546
  }
503
547
  return len;
@@ -506,7 +550,7 @@ function countInlineTextLength(nodes: InlineNode[]): number {
506
550
  /**
507
551
  * Count total text characters in a block node tree.
508
552
  */
509
- function countBlockTextLength(nodes: BlockNode[]): number {
553
+ function countBlockTextLength(nodes: ReadonlyArray<RenderBlockNode>): number {
510
554
  let len = 0;
511
555
  for (const node of nodes) {
512
556
  switch (node.type) {
@@ -514,28 +558,31 @@ function countBlockTextLength(nodes: BlockNode[]): number {
514
558
  case 'paragraph':
515
559
  len += countInlineTextLength(node.children);
516
560
  break;
517
- case 'codeblock':
518
- len += node.content.length;
561
+ case 'code':
562
+ len += node.value.length;
563
+ break;
564
+ case 'math':
565
+ len += node.value.length;
519
566
  break;
520
567
  case 'blockquote':
521
568
  len += countBlockTextLength(node.children);
522
569
  break;
523
570
  case 'list':
524
- for (const item of node.items) {
571
+ for (const item of node.children) {
525
572
  len += countBlockTextLength(item.children);
526
573
  }
527
574
  break;
528
575
  case 'table':
529
- for (const h of node.headers) {
530
- len += countInlineTextLength(h.children);
531
- }
532
- for (const row of node.rows) {
533
- for (const cell of row) {
576
+ for (const row of node.children) {
577
+ for (const cell of row.children) {
534
578
  len += countInlineTextLength(cell.children);
535
579
  }
536
580
  }
537
581
  break;
538
- case 'hr':
582
+ case 'extension':
583
+ len += node.source?.length ?? 0;
584
+ break;
585
+ case 'thematicBreak':
539
586
  break;
540
587
  case 'image':
541
588
  len += node.alt.length;
@@ -555,27 +602,6 @@ const headingStyles = {
555
602
  } as const;
556
603
 
557
604
  // ---------------------------------------------------------------------------
558
- // URL sanitization — block dangerous protocols
559
- // ---------------------------------------------------------------------------
560
-
561
- const DANGEROUS_URL_PATTERN = /^(javascript|data|vbscript):/i;
562
-
563
- function sanitizeUrl(url: string): string | null {
564
- // Strip control characters before testing, the same normalization the
565
- // parser's isSafeUrl applies — the anchored pattern must see the URL the
566
- // way a browser will. Return the normalized value so the stripped
567
- // characters don't ride along into an attribute or component override.
568
- // eslint-disable-next-line no-control-regex -- control chars are the bypass
569
- const normalized = url.replace(/[\x00-\x1f\x7f]/g, '').trim();
570
- if (normalized.length === 0) {
571
- return null;
572
- }
573
- if (DANGEROUS_URL_PATTERN.test(normalized)) {
574
- return null;
575
- }
576
- return normalized;
577
- }
578
-
579
605
  // ---------------------------------------------------------------------------
580
606
  // Inline plugin matching
581
607
  // ---------------------------------------------------------------------------
@@ -681,6 +707,81 @@ function applyInlinePlugins(
681
707
  return segments;
682
708
  }
683
709
 
710
+ interface MarkdownPluginBoundaryProps {
711
+ children: React.ReactNode;
712
+ fallback: React.ReactNode;
713
+ pluginName: string;
714
+ resetKey: unknown;
715
+ resetRenderer: unknown;
716
+ }
717
+
718
+ interface MarkdownPluginBoundaryState {
719
+ failed: boolean;
720
+ resetKey: unknown;
721
+ resetRenderer: unknown;
722
+ }
723
+
724
+ class MarkdownPluginBoundary extends Component<
725
+ MarkdownPluginBoundaryProps,
726
+ MarkdownPluginBoundaryState
727
+ > {
728
+ state: MarkdownPluginBoundaryState = {
729
+ failed: false,
730
+ resetKey: this.props.resetKey,
731
+ resetRenderer: this.props.resetRenderer,
732
+ };
733
+
734
+ static getDerivedStateFromError(): Partial<MarkdownPluginBoundaryState> {
735
+ return {failed: true};
736
+ }
737
+
738
+ static getDerivedStateFromProps(
739
+ props: MarkdownPluginBoundaryProps,
740
+ state: MarkdownPluginBoundaryState,
741
+ ): Partial<MarkdownPluginBoundaryState> | null {
742
+ return props.resetKey === state.resetKey &&
743
+ props.resetRenderer === state.resetRenderer
744
+ ? null
745
+ : {
746
+ failed: false,
747
+ resetKey: props.resetKey,
748
+ resetRenderer: props.resetRenderer,
749
+ };
750
+ }
751
+
752
+ componentDidCatch(error: unknown): void {
753
+ reportMarkdownPluginFailure(this.props.pluginName, 'render', error);
754
+ }
755
+
756
+ render(): React.ReactNode {
757
+ return this.state.failed ? this.props.fallback : this.props.children;
758
+ }
759
+ }
760
+
761
+ function useStableMarkdownSyntaxEntries(
762
+ plugins: PreparedMarkdownPlugins | undefined,
763
+ ): ReadonlyArray<MarkdownPluginEntry> | undefined {
764
+ const identity = plugins?.syntaxIdentity ?? '';
765
+ const syntaxEntriesRef = useRef<
766
+ | {
767
+ identity: string;
768
+ entries: ReadonlyArray<MarkdownPluginEntry> | undefined;
769
+ }
770
+ | undefined
771
+ >(undefined);
772
+ if (
773
+ syntaxEntriesRef.current == null ||
774
+ syntaxEntriesRef.current.identity !== identity
775
+ ) {
776
+ syntaxEntriesRef.current = {
777
+ identity,
778
+ entries:
779
+ plugins == null || identity === '' ? undefined : plugins.syntaxEntries,
780
+ };
781
+ }
782
+ return syntaxEntriesRef.current.entries;
783
+ }
784
+
684
785
  // ---------------------------------------------------------------------------
685
786
  // Inline renderer
686
787
  // ---------------------------------------------------------------------------
@@ -748,7 +849,7 @@ function getCitationNumber(ctx: CitationContext, sourceId: string): number {
748
849
  }
749
850
 
750
851
  function renderInline(
751
- node: InlineNode,
852
+ node: RenderInlineNode,
752
853
  index: number,
753
854
  onLinkClick: MarkdownProps['onLinkClick'] | undefined,
754
855
  cursor: StreamingCursor,
@@ -756,11 +857,12 @@ function renderInline(
756
857
  linkComponent: LinkComponentType = 'a',
757
858
  inlinePlugins?: MarkdownInlinePlugin[],
758
859
  components?: Partial<MarkdownComponents>,
860
+ preparedPlugins?: PreparedMarkdownPlugins,
759
861
  ): SyncReactNode {
760
862
  switch (node.type) {
761
863
  case 'text': {
762
864
  if (inlinePlugins && inlinePlugins.length > 0) {
763
- const segments = applyInlinePlugins(node.content, inlinePlugins);
865
+ const segments = applyInlinePlugins(node.value, inlinePlugins);
764
866
  // If no plugin matched, fall through to the normal path
765
867
  // O(1) guard: applyInlinePlugins returns a single text segment when
766
868
  // nothing matched — skip the plugin path entirely in that case.
@@ -783,9 +885,9 @@ function renderInline(
783
885
  return <Fragment key={index}>{result}</Fragment>;
784
886
  }
785
887
  }
786
- return wrapTextWithFade(node.content, cursor, index);
888
+ return wrapTextWithFade(node.value, cursor, index);
787
889
  }
788
- case 'bold':
890
+ case 'strong':
789
891
  return (
790
892
  <strong key={index} {...stylex.props(styles.bold)}>
791
893
  {node.children.map((c, i) =>
@@ -798,11 +900,12 @@ function renderInline(
798
900
  linkComponent,
799
901
  inlinePlugins,
800
902
  components,
903
+ preparedPlugins,
801
904
  ),
802
905
  )}
803
906
  </strong>
804
907
  );
805
- case 'italic':
908
+ case 'emphasis':
806
909
  return (
807
910
  <em key={index}>
808
911
  {node.children.map((c, i) =>
@@ -815,11 +918,12 @@ function renderInline(
815
918
  linkComponent,
816
919
  inlinePlugins,
817
920
  components,
921
+ preparedPlugins,
818
922
  ),
819
923
  )}
820
924
  </em>
821
925
  );
822
- case 'strikethrough':
926
+ case 'delete':
823
927
  return (
824
928
  <del key={index} {...stylex.props(styles.strikethrough)}>
825
929
  {node.children.map((c, i) =>
@@ -832,23 +936,32 @@ function renderInline(
832
936
  linkComponent,
833
937
  inlinePlugins,
834
938
  components,
939
+ preparedPlugins,
835
940
  ),
836
941
  )}
837
942
  </del>
838
943
  );
839
- case 'code': {
944
+ case 'inlineCode': {
840
945
  // Track code content length for cursor but don't split inside code
841
- cursor.offset += node.content.length;
946
+ cursor.offset += node.value.length;
842
947
  const InlineCodeComp = components?.inlineCode;
843
948
  const codeEl = InlineCodeComp ? (
844
- <InlineCodeComp key={index}>{node.content}</InlineCodeComp>
949
+ <InlineCodeComp key={index}>{node.value}</InlineCodeComp>
845
950
  ) : (
846
- <Code key={index}>{node.content}</Code>
951
+ <Code key={index}>{node.value}</Code>
847
952
  );
848
953
  return codeEl;
849
954
  }
955
+ case 'inlineMath': {
956
+ const MathComp = components?.math;
957
+ if (MathComp == null) {
958
+ return wrapTextWithFade(`$${node.value}$`, cursor, index);
959
+ }
960
+ cursor.offset += node.value.length;
961
+ return <MathComp key={index} value={node.value} display="inline" />;
962
+ }
850
963
  case 'link': {
851
- const safeHref = sanitizeUrl(node.href);
964
+ const safeHref = sanitizeMarkdownUrl(node.url);
852
965
  if (safeHref == null) {
853
966
  // Unsafe URL — render as plain text
854
967
  return (
@@ -863,6 +976,7 @@ function renderInline(
863
976
  linkComponent,
864
977
  inlinePlugins,
865
978
  components,
979
+ preparedPlugins,
866
980
  ),
867
981
  )}
868
982
  </span>
@@ -882,6 +996,7 @@ function renderInline(
882
996
  linkComponent,
883
997
  inlinePlugins,
884
998
  components,
999
+ preparedPlugins,
885
1000
  ),
886
1001
  )}
887
1002
  </LinkComp>
@@ -920,13 +1035,14 @@ function renderInline(
920
1035
  linkComponent,
921
1036
  inlinePlugins,
922
1037
  components,
1038
+ preparedPlugins,
923
1039
  ),
924
1040
  )}
925
1041
  </LinkTag>
926
1042
  );
927
1043
  }
928
1044
  case 'image': {
929
- const safeSrc = sanitizeUrl(node.src);
1045
+ const safeSrc = sanitizeMarkdownUrl(node.url);
930
1046
  if (safeSrc == null) {
931
1047
  return <span key={index}>[{node.alt}]</span>;
932
1048
  }
@@ -943,6 +1059,34 @@ function renderInline(
943
1059
  />
944
1060
  );
945
1061
  }
1062
+ case 'extension': {
1063
+ const renderer = getMarkdownExtensionRenderer(preparedPlugins, node);
1064
+ const fallback = wrapTextWithFade(
1065
+ node.source ?? markdownExtensionText(preparedPlugins, node),
1066
+ cursor,
1067
+ index,
1068
+ );
1069
+ if (renderer == null) {
1070
+ return fallback;
1071
+ }
1072
+ let rendered: React.ReactNode;
1073
+ try {
1074
+ rendered = renderer.render({node});
1075
+ } catch (error) {
1076
+ reportMarkdownPluginFailure(node.plugin, 'render', error);
1077
+ return fallback;
1078
+ }
1079
+ return (
1080
+ <MarkdownPluginBoundary
1081
+ key={index}
1082
+ pluginName={node.plugin}
1083
+ resetKey={node}
1084
+ resetRenderer={renderer.render}
1085
+ fallback={fallback}>
1086
+ <Suspense fallback={fallback}>{rendered}</Suspense>
1087
+ </MarkdownPluginBoundary>
1088
+ );
1089
+ }
946
1090
  case 'break':
947
1091
  cursor.offset += 1;
948
1092
  return <br key={index} />;
@@ -984,13 +1128,13 @@ function renderInline(
984
1128
  // ---------------------------------------------------------------------------
985
1129
 
986
1130
  function getElementSpacing(
987
- node: BlockNode,
1131
+ node: RenderBlockNode,
988
1132
  density: 'default' | 'compact',
989
1133
  ): StyleXStyles {
990
1134
  const compact = density === 'compact';
991
1135
  switch (node.type) {
992
1136
  case 'heading':
993
- return node.level <= 3
1137
+ return node.depth <= 3
994
1138
  ? compact
995
1139
  ? styles.spacingHeadingMajorCompact
996
1140
  : styles.spacingHeadingMajorDefault
@@ -1001,7 +1145,8 @@ function getElementSpacing(
1001
1145
  return compact
1002
1146
  ? styles.spacingParagraphCompact
1003
1147
  : styles.spacingParagraphDefault;
1004
- case 'codeblock':
1148
+ case 'code':
1149
+ case 'math':
1005
1150
  return compact
1006
1151
  ? styles.spacingCodeblockCompact
1007
1152
  : styles.spacingCodeblockDefault;
@@ -1013,7 +1158,11 @@ function getElementSpacing(
1013
1158
  return compact ? styles.spacingListCompact : styles.spacingListDefault;
1014
1159
  case 'table':
1015
1160
  return compact ? styles.spacingTableCompact : styles.spacingTableDefault;
1016
- case 'hr':
1161
+ case 'extension':
1162
+ return compact
1163
+ ? styles.spacingParagraphCompact
1164
+ : styles.spacingParagraphDefault;
1165
+ case 'thematicBreak':
1017
1166
  return compact ? styles.spacingHrCompact : styles.spacingHrDefault;
1018
1167
  case 'image':
1019
1168
  return compact ? styles.spacingImageCompact : styles.spacingImageDefault;
@@ -1028,15 +1177,17 @@ function getElementSpacing(
1028
1177
  * Compute per-column min-widths from table AST content.
1029
1178
  * Buckets: ≤6 chars → 60px, 7–15 → 80px, >15 → 120px.
1030
1179
  */
1031
- function computeTableColumnMinWidths(node: {
1032
- headers: {children: InlineNode[]}[];
1033
- rows: {children: InlineNode[]}[][];
1034
- }): number[] {
1035
- return node.headers.map((h, colIdx) => {
1036
- let maxLen = countInlineTextLength(h.children);
1037
- for (const row of node.rows) {
1038
- if (row[colIdx]) {
1039
- const len = countInlineTextLength(row[colIdx].children);
1180
+ function computeTableColumnMinWidths(node: RenderTable): number[] {
1181
+ const [header, ...rows] = node.children;
1182
+ if (header == null) {
1183
+ return [];
1184
+ }
1185
+ return header.children.map((cell, colIdx) => {
1186
+ let maxLen = countInlineTextLength(cell.children);
1187
+ for (const row of rows) {
1188
+ const rowCell = row.children[colIdx];
1189
+ if (rowCell != null) {
1190
+ const len = countInlineTextLength(rowCell.children);
1040
1191
  if (len > maxLen) {
1041
1192
  maxLen = len;
1042
1193
  }
@@ -1047,7 +1198,7 @@ function computeTableColumnMinWidths(node: {
1047
1198
  }
1048
1199
 
1049
1200
  function renderBlock(
1050
- node: BlockNode,
1201
+ node: RenderBlockNode,
1051
1202
  index: number,
1052
1203
  blockCount: number,
1053
1204
  density: 'default' | 'compact',
@@ -1060,8 +1211,9 @@ function renderBlock(
1060
1211
  linkComponent: LinkComponentType = 'a',
1061
1212
  inlinePlugins: MarkdownInlinePlugin[] | undefined,
1062
1213
  components: Partial<MarkdownComponents> | undefined,
1214
+ preparedPlugins: PreparedMarkdownPlugins | undefined,
1063
1215
  t: TranslatorFn,
1064
- headingIdMap?: ReadonlyMap<BlockNode, string>,
1216
+ headingIdMap?: ReadonlyMap<RenderBlockNode, string>,
1065
1217
  ): SyncReactNode {
1066
1218
  const blockAlignMargin = BLOCK_ALIGN_MARGIN[contentAlign];
1067
1219
  const blockAlignStyle =
@@ -1074,7 +1226,7 @@ function renderBlock(
1074
1226
 
1075
1227
  switch (node.type) {
1076
1228
  case 'heading': {
1077
- const level = Math.min(node.level + headingLevelStart - 1, 6) as
1229
+ const level = Math.min(node.depth + headingLevelStart - 1, 6) as
1078
1230
  1 | 2 | 3 | 4 | 5 | 6;
1079
1231
  const headingChildren = node.children.map((c, i) =>
1080
1232
  renderInline(
@@ -1086,6 +1238,7 @@ function renderBlock(
1086
1238
  linkComponent,
1087
1239
  inlinePlugins,
1088
1240
  components,
1241
+ preparedPlugins,
1089
1242
  ),
1090
1243
  );
1091
1244
  // Only top-level headings get an id: the map is built from the same
@@ -1137,6 +1290,7 @@ function renderBlock(
1137
1290
  linkComponent,
1138
1291
  inlinePlugins,
1139
1292
  components,
1293
+ preparedPlugins,
1140
1294
  ),
1141
1295
  );
1142
1296
  const ParagraphComp = components?.paragraph;
@@ -1174,20 +1328,17 @@ function renderBlock(
1174
1328
  </div>
1175
1329
  );
1176
1330
  }
1177
- case 'codeblock': {
1331
+ case 'code': {
1178
1332
  // Track codeblock content in cursor for accurate character counting
1179
- cursor.offset += node.content.length;
1333
+ cursor.offset += node.value.length;
1334
+ const language = getMarkdownAstLegacyCodeLanguage(node) ?? 'plaintext';
1180
1335
  const CodeBlockComp = components?.code;
1181
1336
  if (CodeBlockComp) {
1182
1337
  return (
1183
- <CodeBlockComp
1184
- key={index}
1185
- code={node.content}
1186
- language={node.language}
1187
- />
1338
+ <CodeBlockComp key={index} code={node.value} language={language} />
1188
1339
  );
1189
1340
  }
1190
- return (
1341
+ const fallback = (
1191
1342
  <div
1192
1343
  key={index}
1193
1344
  {...mergeProps(
@@ -1200,8 +1351,8 @@ function renderBlock(
1200
1351
  ),
1201
1352
  )}>
1202
1353
  <CodeBlock
1203
- code={node.content}
1204
- language={node.language}
1354
+ code={node.value}
1355
+ language={language}
1205
1356
  isCollapsible
1206
1357
  xstyle={[
1207
1358
  contentWidthValue != null
@@ -1212,6 +1363,48 @@ function renderBlock(
1212
1363
  />
1213
1364
  </div>
1214
1365
  );
1366
+ const proposal = getMarkdownFenceProposal(node);
1367
+ if (proposal == null) {
1368
+ return fallback;
1369
+ }
1370
+ const renderer = getMarkdownExtensionRenderer(
1371
+ preparedPlugins,
1372
+ proposal.node,
1373
+ );
1374
+ if (renderer == null) {
1375
+ return fallback;
1376
+ }
1377
+ try {
1378
+ const rendered = renderer.render({node: proposal.node});
1379
+ if (rendered == null || typeof rendered === 'boolean') {
1380
+ return fallback;
1381
+ }
1382
+ return (
1383
+ <MarkdownPluginBoundary
1384
+ key={index}
1385
+ pluginName={proposal.node.plugin}
1386
+ resetKey={proposal.node}
1387
+ resetRenderer={renderer.render}
1388
+ fallback={fallback}>
1389
+ <Suspense fallback={fallback}>{rendered}</Suspense>
1390
+ </MarkdownPluginBoundary>
1391
+ );
1392
+ } catch (error) {
1393
+ reportMarkdownPluginFailure(proposal.node.plugin, 'render', error);
1394
+ return fallback;
1395
+ }
1396
+ }
1397
+ case 'math': {
1398
+ cursor.offset += node.value.length;
1399
+ const MathComp = components?.math;
1400
+ if (MathComp == null) {
1401
+ return (
1402
+ <div key={index} role="paragraph">
1403
+ {`$$${node.value}$$`}
1404
+ </div>
1405
+ );
1406
+ }
1407
+ return <MathComp key={index} value={node.value} display="block" />;
1215
1408
  }
1216
1409
  case 'blockquote': {
1217
1410
  const BlockquoteComp = components?.blockquote;
@@ -1231,6 +1424,7 @@ function renderBlock(
1231
1424
  linkComponent,
1232
1425
  inlinePlugins,
1233
1426
  components,
1427
+ preparedPlugins,
1234
1428
  t,
1235
1429
  ),
1236
1430
  );
@@ -1266,6 +1460,7 @@ function renderBlock(
1266
1460
  linkComponent,
1267
1461
  inlinePlugins,
1268
1462
  components,
1463
+ preparedPlugins,
1269
1464
  t,
1270
1465
  ),
1271
1466
  )}
@@ -1275,11 +1470,12 @@ function renderBlock(
1275
1470
  case 'list': {
1276
1471
  // Detect task lists: all items have a checked state
1277
1472
  const isTaskList =
1278
- node.items.length > 0 && node.items.every(item => item.checked != null);
1473
+ node.children.length > 0 &&
1474
+ node.children.every(item => item.checked != null);
1279
1475
 
1280
1476
  if (isTaskList) {
1281
1477
  // Extract labels from task items — render as rich inline content
1282
- const checkedValues = node.items
1478
+ const checkedValues = node.children
1283
1479
  .map((item, i) => ({item, key: `task-${i}`}))
1284
1480
  .filter(({item}) => item.checked)
1285
1481
  .map(({key}) => key);
@@ -1302,7 +1498,7 @@ function renderBlock(
1302
1498
  xstyle={styles.blockIndent}
1303
1499
  isReadOnly
1304
1500
  density="compact">
1305
- {node.items.map((item, i) => {
1501
+ {node.children.map((item, i) => {
1306
1502
  const firstChild = item.children[0];
1307
1503
  const isInline =
1308
1504
  item.children.length === 1 &&
@@ -1320,6 +1516,7 @@ function renderBlock(
1320
1516
  linkComponent,
1321
1517
  inlinePlugins,
1322
1518
  components,
1519
+ preparedPlugins,
1323
1520
  ),
1324
1521
  )}
1325
1522
  </>
@@ -1340,6 +1537,7 @@ function renderBlock(
1340
1537
  linkComponent,
1341
1538
  inlinePlugins,
1342
1539
  components,
1540
+ preparedPlugins,
1343
1541
  t,
1344
1542
  ),
1345
1543
  )}
@@ -1382,7 +1580,7 @@ function renderBlock(
1382
1580
  density="compact"
1383
1581
  start={node.ordered ? node.start : undefined}
1384
1582
  xstyle={styles.blockIndent}>
1385
- {node.items.map((item, i) => {
1583
+ {node.children.map((item, i) => {
1386
1584
  const firstChild = item.children[0];
1387
1585
  const isInline =
1388
1586
  item.children.length === 1 && firstChild?.type === 'paragraph';
@@ -1402,6 +1600,7 @@ function renderBlock(
1402
1600
  linkComponent,
1403
1601
  inlinePlugins,
1404
1602
  components,
1603
+ preparedPlugins,
1405
1604
  ),
1406
1605
  )}
1407
1606
  </>
@@ -1422,6 +1621,7 @@ function renderBlock(
1422
1621
  linkComponent,
1423
1622
  inlinePlugins,
1424
1623
  components,
1624
+ preparedPlugins,
1425
1625
  t,
1426
1626
  ),
1427
1627
  )}
@@ -1442,6 +1642,7 @@ function renderBlock(
1442
1642
  }
1443
1643
  case 'table': {
1444
1644
  const colMinWidths = computeTableColumnMinWidths(node);
1645
+ const [header, ...rows] = node.children;
1445
1646
 
1446
1647
  return (
1447
1648
  <div
@@ -1471,14 +1672,14 @@ function renderBlock(
1471
1672
  <Table dividers="rows" textOverflow="wrap">
1472
1673
  <TableHeader>
1473
1674
  <TableRow>
1474
- {node.headers.map((h, i) => (
1675
+ {header?.children.map((h, i) => (
1475
1676
  <TableHeaderCell
1476
1677
  // eslint-disable-next-line @eslint-react/no-array-index-key -- markdown table columns are positional by definition
1477
1678
  key={i}
1478
1679
  xstyle={[
1479
1680
  dynamicStyles.cellMinWidth(`${colMinWidths[i]}px`),
1480
- node.alignments[i] === 'center' && cellAlignStyles.center,
1481
- node.alignments[i] === 'right' && cellAlignStyles.end,
1681
+ node.align[i] === 'center' && cellAlignStyles.center,
1682
+ node.align[i] === 'right' && cellAlignStyles.end,
1482
1683
  ]}>
1483
1684
  {h.children.map((c, j) =>
1484
1685
  renderInline(
@@ -1490,6 +1691,7 @@ function renderBlock(
1490
1691
  linkComponent,
1491
1692
  inlinePlugins,
1492
1693
  components,
1694
+ preparedPlugins,
1493
1695
  ),
1494
1696
  )}
1495
1697
  </TableHeaderCell>
@@ -1497,14 +1699,14 @@ function renderBlock(
1497
1699
  </TableRow>
1498
1700
  </TableHeader>
1499
1701
  <TableBody>
1500
- {node.rows.map((row, i) => {
1501
- const cells = row.map((cell, j) => (
1702
+ {rows.map((row, i) => {
1703
+ const cells = row.children.map((cell, j) => (
1502
1704
  <TableCell
1503
1705
  // eslint-disable-next-line @eslint-react/no-array-index-key -- markdown table cells are positional by row and column
1504
1706
  key={j}
1505
1707
  xstyle={[
1506
- node.alignments[j] === 'center' && cellAlignStyles.center,
1507
- node.alignments[j] === 'right' && cellAlignStyles.end,
1708
+ node.align[j] === 'center' && cellAlignStyles.center,
1709
+ node.align[j] === 'right' && cellAlignStyles.end,
1508
1710
  ]}>
1509
1711
  {cell.children.map((c, k) =>
1510
1712
  renderInline(
@@ -1516,6 +1718,7 @@ function renderBlock(
1516
1718
  linkComponent,
1517
1719
  inlinePlugins,
1518
1720
  components,
1721
+ preparedPlugins,
1519
1722
  ),
1520
1723
  )}
1521
1724
  </TableCell>
@@ -1533,7 +1736,47 @@ function renderBlock(
1533
1736
  </div>
1534
1737
  );
1535
1738
  }
1536
- case 'hr': {
1739
+ case 'extension': {
1740
+ const renderer = getMarkdownExtensionRenderer(preparedPlugins, node);
1741
+ const fallbackText =
1742
+ node.source ?? markdownExtensionText(preparedPlugins, node);
1743
+ const fallback = wrapTextWithFade(fallbackText, cursor, index);
1744
+ let content: React.ReactNode = fallback;
1745
+ if (renderer != null) {
1746
+ try {
1747
+ const rendered = renderer.render({node});
1748
+ content = (
1749
+ <MarkdownPluginBoundary
1750
+ pluginName={node.plugin}
1751
+ resetKey={node}
1752
+ resetRenderer={renderer.render}
1753
+ fallback={fallback}>
1754
+ <Suspense fallback={fallback}>{rendered}</Suspense>
1755
+ </MarkdownPluginBoundary>
1756
+ );
1757
+ } catch (error) {
1758
+ reportMarkdownPluginFailure(node.plugin, 'render', error);
1759
+ }
1760
+ }
1761
+ return (
1762
+ <div
1763
+ key={index}
1764
+ {...stylex.props(
1765
+ spacing,
1766
+ contentWidthValue != null
1767
+ ? dynamicStyles.proseWidth(contentWidthValue)
1768
+ : null,
1769
+ contentAlign !== 'start'
1770
+ ? dynamicStyles.proseAlign(ALIGN_MARGIN[contentAlign])
1771
+ : null,
1772
+ isFirst && styles.noMarginBlockStart,
1773
+ isLast && styles.noMarginBlockEnd,
1774
+ )}>
1775
+ {content}
1776
+ </div>
1777
+ );
1778
+ }
1779
+ case 'thematicBreak': {
1537
1780
  const HrComp = components?.hr;
1538
1781
  if (HrComp) {
1539
1782
  return <HrComp key={index} />;
@@ -1554,7 +1797,7 @@ function renderBlock(
1554
1797
  );
1555
1798
  }
1556
1799
  case 'image': {
1557
- const safeSrc = sanitizeUrl(node.src);
1800
+ const safeSrc = sanitizeMarkdownUrl(node.url);
1558
1801
  if (safeSrc == null) {
1559
1802
  return (
1560
1803
  <div
@@ -1608,7 +1851,9 @@ function renderBlock(
1608
1851
  * </Markdown>
1609
1852
  * ```
1610
1853
  */
1611
- export function Markdown({
1854
+ export function Markdown<
1855
+ const Plugins extends ReadonlyArray<MarkdownPluginEntry> = readonly [],
1856
+ >({
1612
1857
  ref,
1613
1858
  children,
1614
1859
  display = 'block',
@@ -1621,6 +1866,7 @@ export function Markdown({
1621
1866
  contentWidth = 680,
1622
1867
  contentAlign = 'start',
1623
1868
  components,
1869
+ plugins,
1624
1870
  inlinePlugins,
1625
1871
  autolink,
1626
1872
  xstyle,
@@ -1628,7 +1874,7 @@ export function Markdown({
1628
1874
  style,
1629
1875
  'data-testid': testId,
1630
1876
  ...props
1631
- }: MarkdownProps): React.ReactElement {
1877
+ }: MarkdownProps<Plugins>): React.ReactElement {
1632
1878
  const t = useTranslator();
1633
1879
  const LinkComponent = useLinkComponent();
1634
1880
  // Derive the set of source IDs for the parser (stable across renders when sources don't change)
@@ -1637,44 +1883,109 @@ export function Markdown({
1637
1883
  [sources],
1638
1884
  );
1639
1885
 
1640
- const parseOptions = useMemo(
1641
- () => ({sourceIds, autolink}),
1642
- [sourceIds, autolink],
1886
+ const preparedPlugins = useMemo(() => {
1887
+ if (plugins == null) {
1888
+ return undefined;
1889
+ }
1890
+ try {
1891
+ return prepareMarkdownPlugins(plugins);
1892
+ } catch (error) {
1893
+ reportMarkdownPluginFailure('configuration', 'transform', error);
1894
+ return undefined;
1895
+ }
1896
+ }, [plugins]);
1897
+ const syntaxPlugins = useStableMarkdownSyntaxEntries(preparedPlugins);
1898
+ const hasMathRenderer = components?.math != null;
1899
+ const legacyParseOptions = useMemo<
1900
+ ParseOptions<ReadonlyArray<MarkdownPluginEntry>>
1901
+ >(
1902
+ () => ({sourceIds, autolink, plugins: syntaxPlugins}),
1903
+ [sourceIds, autolink, syntaxPlugins],
1904
+ );
1905
+ const mathParseOptions = useMemo<
1906
+ MathParseOptions<ReadonlyArray<MarkdownPluginEntry>>
1907
+ >(
1908
+ () => ({sourceIds, autolink, math: true, plugins: syntaxPlugins}),
1909
+ [sourceIds, autolink, syntaxPlugins],
1643
1910
  );
1644
1911
 
1645
1912
  // Smooth bursty streamed chunks into a steady character-by-character reveal.
1646
1913
  // When not streaming, the hook returns children unchanged (no-op).
1647
1914
  const smoothedText = useStreamingText(children, isStreaming);
1648
1915
 
1649
- const incrementalStateRef = useRef<IncrementalState>(
1650
- createIncrementalState(),
1916
+ const incrementalStateRef = useRef<IncrementalState<boolean>>(
1917
+ createIncrementalState<boolean>(),
1651
1918
  );
1652
- // Reset incremental cache when the autolink option toggles — cached
1653
- // settled blocks were parsed with the previous setting.
1919
+ // Reset incremental cache when parser-affecting component options toggle —
1920
+ // cached settled blocks were parsed with the previous setting.
1654
1921
  const prevAutolinkRef = useRef(autolink);
1655
- if (prevAutolinkRef.current !== autolink) {
1656
- incrementalStateRef.current = createIncrementalState();
1922
+ const prevMathRef = useRef(hasMathRenderer);
1923
+ if (
1924
+ prevAutolinkRef.current !== autolink ||
1925
+ prevMathRef.current !== hasMathRenderer
1926
+ ) {
1927
+ incrementalStateRef.current = createIncrementalState<boolean>();
1657
1928
  prevAutolinkRef.current = autolink;
1929
+ prevMathRef.current = hasMathRenderer;
1658
1930
  }
1659
1931
 
1660
- const blocks = useMemo(() => {
1932
+ const parsedBlocks = useMemo(() => {
1661
1933
  if (display === 'inline') {
1662
1934
  return [];
1663
1935
  }
1664
1936
  if (isStreaming) {
1665
1937
  if (smoothedText === '') {
1666
- incrementalStateRef.current = createIncrementalState();
1938
+ incrementalStateRef.current = createIncrementalState<boolean>();
1667
1939
  return [];
1668
1940
  }
1669
- const input = trimStreamingArtifacts(smoothedText);
1670
- return parseMarkdownIncremental(
1671
- input,
1672
- incrementalStateRef.current,
1673
- parseOptions,
1674
- );
1941
+ const options = hasMathRenderer ? mathParseOptions : legacyParseOptions;
1942
+ const input = trimStreamingArtifacts(smoothedText, options);
1943
+ return (
1944
+ hasMathRenderer
1945
+ ? parseMarkdownAstIncremental(
1946
+ input,
1947
+ incrementalStateRef.current,
1948
+ mathParseOptions,
1949
+ )
1950
+ : parseMarkdownAstIncremental(
1951
+ input,
1952
+ incrementalStateRef.current,
1953
+ legacyParseOptions,
1954
+ )
1955
+ ).children;
1675
1956
  }
1676
- return parseMarkdown(children, parseOptions);
1677
- }, [display, smoothedText, children, isStreaming, parseOptions]);
1957
+ return (
1958
+ hasMathRenderer
1959
+ ? parseMarkdownAst(children, mathParseOptions)
1960
+ : parseMarkdownAst(children, legacyParseOptions)
1961
+ ).children;
1962
+ }, [
1963
+ display,
1964
+ smoothedText,
1965
+ children,
1966
+ isStreaming,
1967
+ hasMathRenderer,
1968
+ mathParseOptions,
1969
+ legacyParseOptions,
1970
+ ]);
1971
+
1972
+ const transformSource = isStreaming
1973
+ ? trimStreamingArtifacts(
1974
+ smoothedText,
1975
+ hasMathRenderer ? mathParseOptions : legacyParseOptions,
1976
+ )
1977
+ : children;
1978
+ const blocks = useMemo(
1979
+ () =>
1980
+ applyMarkdownTransforms(
1981
+ {type: 'root', children: parsedBlocks},
1982
+ preparedPlugins,
1983
+ transformSource,
1984
+ !isStreaming,
1985
+ 'block',
1986
+ ).children,
1987
+ [parsedBlocks, preparedPlugins, transformSource, isStreaming],
1988
+ );
1678
1989
 
1679
1990
  // Assign each top-level heading the slug that parseOutlineFromMarkdown
1680
1991
  // would derive for it, so Outline hash links built from the same source
@@ -1686,24 +1997,71 @@ export function Markdown({
1686
1997
  if (display === 'inline' || blocks.length === 0) {
1687
1998
  return undefined;
1688
1999
  }
1689
- const map = new Map<BlockNode, string>();
2000
+ const map = new Map<RenderBlockNode, string>();
1690
2001
  const counts = new Map<string, number>();
1691
2002
  for (const block of blocks) {
1692
2003
  if (block.type === 'heading') {
1693
- const label = inlineText(block.children).trim();
2004
+ const label = markdownAstText(block.children, node =>
2005
+ markdownExtensionText(preparedPlugins, node),
2006
+ ).trim();
1694
2007
  map.set(block, uniqueSlug(slugify(label), counts));
1695
2008
  }
1696
2009
  }
1697
2010
  return map;
1698
- }, [display, blocks]);
2011
+ }, [display, blocks, preparedPlugins]);
1699
2012
 
2013
+ const parsedInlineNodes = useMemo(() => {
2014
+ if (display !== 'inline') {
2015
+ return [];
2016
+ }
2017
+ const options = hasMathRenderer ? mathParseOptions : legacyParseOptions;
2018
+ const input = isStreaming
2019
+ ? trimStreamingArtifacts(smoothedText, options)
2020
+ : children;
2021
+ return hasMathRenderer
2022
+ ? parseInlineAst(input, mathParseOptions)
2023
+ : parseInlineAst(input, legacyParseOptions);
2024
+ }, [
2025
+ display,
2026
+ smoothedText,
2027
+ children,
2028
+ isStreaming,
2029
+ hasMathRenderer,
2030
+ mathParseOptions,
2031
+ legacyParseOptions,
2032
+ ]);
2033
+
2034
+ const inlineTransformSource = isStreaming
2035
+ ? trimStreamingArtifacts(
2036
+ smoothedText,
2037
+ hasMathRenderer ? mathParseOptions : legacyParseOptions,
2038
+ )
2039
+ : children;
1700
2040
  const inlineNodes = useMemo(() => {
1701
2041
  if (display !== 'inline') {
1702
2042
  return [];
1703
2043
  }
1704
- const input = isStreaming ? trimStreamingArtifacts(smoothedText) : children;
1705
- return parseInline(input, parseOptions);
1706
- }, [display, smoothedText, children, isStreaming, parseOptions]);
2044
+ const transformed = applyMarkdownTransforms(
2045
+ {
2046
+ type: 'root',
2047
+ children: [{type: 'paragraph', children: parsedInlineNodes}],
2048
+ },
2049
+ preparedPlugins,
2050
+ inlineTransformSource,
2051
+ !isStreaming,
2052
+ 'inline',
2053
+ );
2054
+ const paragraph = transformed.children[0];
2055
+ return paragraph?.type === 'paragraph'
2056
+ ? [...paragraph.children]
2057
+ : parsedInlineNodes;
2058
+ }, [
2059
+ display,
2060
+ parsedInlineNodes,
2061
+ preparedPlugins,
2062
+ inlineTransformSource,
2063
+ isStreaming,
2064
+ ]);
1707
2065
 
1708
2066
  // Track recent boundaries for stacked fade-in animation.
1709
2067
  // The number of spans needed = ceil(animationDuration / tickInterval).
@@ -1719,8 +2077,8 @@ export function Markdown({
1719
2077
  return Math.min(Math.ceil(duration / tickMs), 12);
1720
2078
  }, [token]);
1721
2079
 
1722
- const prevBlocksRef = useRef<BlockNode[]>([]);
1723
- const prevInlineNodesRef = useRef<InlineNode[]>([]);
2080
+ const prevBlocksRef = useRef<ReadonlyArray<RenderBlockNode>>([]);
2081
+ const prevInlineNodesRef = useRef<ReadonlyArray<RenderInlineNode>>([]);
1724
2082
  const boundariesRef = useRef<number[]>([]);
1725
2083
  const smoothedLen = smoothedText.length;
1726
2084
  const boundaries = useMemo(() => {
@@ -1769,6 +2127,7 @@ export function Markdown({
1769
2127
  LinkComponent,
1770
2128
  inlinePlugins,
1771
2129
  components,
2130
+ preparedPlugins,
1772
2131
  ),
1773
2132
  )}
1774
2133
  </span>
@@ -1814,6 +2173,7 @@ export function Markdown({
1814
2173
  LinkComponent,
1815
2174
  inlinePlugins,
1816
2175
  components,
2176
+ preparedPlugins,
1817
2177
  t,
1818
2178
  headingIdMap,
1819
2179
  ),