@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
@@ -0,0 +1,72 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file Markdown.renderBoundary.test.tsx
5
+ * @input Uses vitest, @testing-library/react, Markdown with a mocked parser
6
+ * @output Tests for the render-side navigation check in Markdown
7
+ * @position Testing; the parser already rejects blocked destinations, so
8
+ * these tests bypass it and hand the renderer a pre-parsed link node.
9
+ * That pins the render boundary on its own: it must apply the shared
10
+ * navigation rule (utils/safeUrl.ts) even when a link node arrives with a
11
+ * destination the parser never saw.
12
+ */
13
+
14
+ import {afterEach, describe, expect, it, vi} from 'vitest';
15
+ import {cleanup, render} from '@testing-library/react';
16
+ import {Markdown} from './Markdown';
17
+ import type * as Parser from './parser';
18
+ import type {MarkdownAstBlockContent} from './ast';
19
+
20
+ const parsed: {blocks: MarkdownAstBlockContent[]} = {blocks: []};
21
+
22
+ vi.mock('./parser', async importOriginal => {
23
+ const actual = await importOriginal<typeof Parser>();
24
+ return {
25
+ ...actual,
26
+ parseMarkdownAst: () => ({type: 'root', children: parsed.blocks}),
27
+ };
28
+ });
29
+
30
+ function linkParagraph(url: string): MarkdownAstBlockContent[] {
31
+ return [
32
+ {
33
+ type: 'paragraph',
34
+ children: [{type: 'link', url, children: [{type: 'text', value: 'go'}]}],
35
+ },
36
+ ];
37
+ }
38
+
39
+ afterEach(() => {
40
+ cleanup();
41
+ });
42
+
43
+ describe('Markdown render boundary — link destinations', () => {
44
+ it('renders an accepted pre-parsed destination as a link', () => {
45
+ parsed.blocks = linkParagraph('data:image/png;base64,iVBORw0KGgo=');
46
+ const {container} = render(<Markdown>{'ignored'}</Markdown>);
47
+ expect(container.querySelector('a')?.getAttribute('href')).toBe(
48
+ 'data:image/png;base64,iVBORw0KGgo=',
49
+ );
50
+ });
51
+
52
+ it.each([
53
+ 'javascript:alert(1)',
54
+ 'vbscript:MsgBox(1)',
55
+ 'data:text/html,<script>alert(1)</script>',
56
+ 'java\nscript:alert(1)',
57
+ ])('renders a blocked pre-parsed destination %s as text', href => {
58
+ parsed.blocks = linkParagraph(href);
59
+ const {container} = render(<Markdown>{'ignored'}</Markdown>);
60
+ expect(container.querySelector('a')).toBeNull();
61
+ expect(container.textContent).toContain('go');
62
+ });
63
+
64
+ it('does not hand a blocked destination to a custom link renderer', () => {
65
+ parsed.blocks = linkParagraph('javascript:alert(1)');
66
+ const link = vi.fn(({children}: {href: string; children?: unknown}) => (
67
+ <a data-custom>{children as never}</a>
68
+ ));
69
+ render(<Markdown components={{link}}>{'ignored'}</Markdown>);
70
+ expect(link).not.toHaveBeenCalled();
71
+ });
72
+ });
@@ -7,19 +7,23 @@ authority: current
7
7
  archive_reason: null
8
8
  superseded_by: null
9
9
  approved_by: cixzhang
10
- approved_at: 2026-09-13
10
+ approved_at: 2026-09-22
11
11
  owners: [cixzhang]
12
12
  review_triggers: [api, theming]
13
13
  verified_by:
14
14
  [
15
15
  packages/core/src/Markdown/Markdown.test.tsx,
16
+ packages/core/src/Markdown/Markdown.renderBoundary.test.tsx,
16
17
  packages/core/src/Markdown/Markdown.public.test.ts,
17
18
  packages/core/src/Markdown/parser.test.ts,
18
19
  packages/core/src/Markdown/incremental.test.ts,
20
+ packages/core/src/Markdown/remark.test.tsx,
21
+ packages/core/src/Markdown/plugins/softBreaks.test.tsx,
22
+ packages/core/src/Outline/parseOutlineFromMarkdown.test.ts,
19
23
  packages/core/src/theme/themingTargets.test.ts,
20
24
  scripts/check-knowledge.mjs,
21
25
  ]
22
- modules: []
26
+ modules: [module:Markdown/remark, module:Markdown/softBreaks]
23
27
  families: [family:navigation-destinations]
24
28
  design_specs: []
25
29
  architecture:
@@ -31,6 +35,10 @@ system_specs:
31
35
  spec:AST-002/DEC-5,
32
36
  spec:AST-005/DEC-1,
33
37
  spec:AST-005/DEC-2,
38
+ spec:AST-036/DEC-1,
39
+ spec:AST-036/DEC-2,
40
+ spec:AST-036/DEC-3,
41
+ spec:AST-036/DEC-4,
34
42
  ]
35
43
  ---
36
44
 
@@ -39,18 +47,20 @@ system_specs:
39
47
  ## Intent
40
48
 
41
49
  Markdown renders parsed content in a Document with stable default block parts and
42
- constrained renderer seams. In addition to the existing element overrides and
43
- prose-only inline plugins, a caller may opt a document into dollar-delimited math
44
- by supplying one typed renderer for both inline and display expressions. The
45
- parser exposes the same syntax only through an explicit option. Existing parsing,
46
- rendering, styling, and streaming behavior remain unchanged when math is absent.
50
+ constrained renderer seams. Callers may opt into the canonical plugin protocol for
51
+ bounded source syntax, immutable document transformation, typed extension
52
+ rendering, and native typed document-start frontmatter. They may separately opt into dollar-delimited math by supplying one typed
53
+ renderer for both inline and display expressions. The parser accepts matching
54
+ explicit options. Existing parsing, rendering, styling, and streaming behavior
55
+ remain unchanged when plugins and math are absent.
47
56
 
48
57
  ## Compatibility and migration
49
58
 
50
59
  - Released default preserved: `yes`
51
60
  - Compatibility class: additive, opt-in public API; existing parser nodes, DOM,
52
- styling, targets, and dollar-delimited text remain unchanged unless the caller
53
- supplies `components.math` or passes `{math: true}` to a parser.
61
+ styling, targets, dollar-delimited text, `components`, and `inlinePlugins` remain
62
+ unchanged unless the caller supplies `plugins`, supplies `components.math`, or
63
+ passes the matching explicit parser option.
54
64
  - Controlled/uncontrolled behavior: not applicable
55
65
  - Migration decision: none
56
66
 
@@ -69,6 +79,16 @@ Consumer migration instructions belong in consumer docs and release notes.
69
79
  delimiter boundaries, escape behavior, parser nodes, and streaming parity.
70
80
  - Passing each recognized expression as inert text to the caller's one math
71
81
  renderer with an `inline` or `block` display value.
82
+ - Applying the canonical `plugins` protocol in the fixed syntax → immutable
83
+ transform → render order while preserving built-in lexical shields, Core-owned
84
+ semantics, and local readable fallback.
85
+ - Validating and freezing replacement document roots before later transforms or
86
+ rendering observe them.
87
+ - Sharing plugin-enabled parse configuration, transformed heading projection, and
88
+ collision-safe heading IDs with Markdown-derived Outline utilities.
89
+ - Decoding an optional document-start frontmatter block into caller-defined typed
90
+ metadata, withholding unfinished frontmatter while streaming, and excluding
91
+ completed frontmatter syntax from rendered content.
72
92
 
73
93
  **Does not own / non-goals**
74
94
 
@@ -78,11 +98,43 @@ Consumer migration instructions belong in consumer docs and release notes.
78
98
  additional default block anatomy.
79
99
  - Nested anatomy or targets owned by CodeBlock, Blockquote, List, CheckboxList,
80
100
  or Table.
81
- - Executing or sanitizing a renderer's math library output, raw HTML parsing,
82
- arbitrary AST plugins, or new list/table/inline-style override slots.
101
+ - Executing or sanitizing a renderer's math or plugin output, raw HTML parsing,
102
+ mutable or unrestricted AST plugins, package discovery, or new
103
+ list/table/inline-style override slots.
83
104
 
84
105
  ## Public concepts
85
106
 
107
+ `plugins` is one optional ordered list of opaque entries created by
108
+ `createMarkdownPlugin()`. A plugin declares only the `syntax`, `transform`, and
109
+ `renderers` capabilities it uses. Syntax-bearing entries supply stable parse
110
+ identity. Parsing, transforms, rendering, and Outline use one stable, strictly typed,
111
+ MDAST-aligned canonical tree; `parseMarkdownAst()` and `parseInlineAst()` expose
112
+ that tree from the server-safe `@astryxdesign/core/Markdown/parser` subpath while
113
+ `@astryxdesign/core/Markdown/plugins` remains server-safe for constructing and
114
+ running plugins in server or RSC code. The rendered `Markdown` component remains a
115
+ client entry: function-bearing plugin entries are not serializable props and cannot
116
+ cross an RSC boundary. `MarkdownAstNodeMap` and `visitMarkdownNodes` provide
117
+ node-kind narrowing. Released parser functions preserve their existing result shape
118
+ through a compatibility projection. Transforms return validated replacement roots
119
+ without entering parse identity. Every extension node introduced
120
+ by syntax or transformation has complete renderer ownership and a deterministic
121
+ text projection. Text matching, semantic fences, source decoration, and native
122
+ frontmatter are helpers that compile to transforms rather than separate protocol
123
+ phases. Frontmatter is document metadata: it has no renderer, uses a bounded
124
+ first-party key/value grammar rather than Remark compatibility, and exposes typed
125
+ metadata through the helper that created it. `spec:AST-036`
126
+ owns the shared protocol and limited Remark compatibility profile,
127
+ `module:Markdown/remark` owns that profile's adapter, and this component owns
128
+ aggregate application and fallback.
129
+
130
+ ### Acceptance and implementation state
131
+
132
+ The plugin clauses below are the accepted target contract for the AST-036 rollout,
133
+ not a claim that the APIs already ship. Until every clause's implementation and
134
+ verification land, the currently released no-plugin, parser, `components`, and
135
+ `inlinePlugins` behavior remains the only available contract. Each implementation PR
136
+ must identify the clauses it completes without weakening the zero-breaking baseline.
137
+
86
138
  `MarkdownComponents.math` is one optional renderer with the signature
87
139
  `({value: string, display: 'inline' | 'block'}) => ReactNode`. Supplying it opts
88
140
  the component into math parsing because the caller owns both whether dollar
@@ -97,19 +149,33 @@ unions. Enabled calls return the explicit `InlineNodeWithMath` and
97
149
 
98
150
  ## Behavioral and layout contract
99
151
 
100
- | ID | Invariant |
101
- | ---- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
102
- | FR1 | Block and inline displays render one Document root carrying the current `markdown` target. Inline display renders no block anatomy. |
103
- | FR2 | On the default block render path, Heading, Paragraph, List, Code block, Blockquote, Table, Divider, and Image carry the eight current local block targets documented below. |
104
- | FR3 | A supplied `heading`, `paragraph`, `code`, `blockquote`, `hr`, or safe-URL `image` renderer replaces the corresponding default part, so Markdown does not impose that part's local target on the replacement. |
105
- | FR4 | The released Code block target remains `markdown-codeblock`; this compatibility anomaly is not renamed or aliased. |
106
- | FR5 | Density and Heading level remain reflected capabilities on their owning targets. Display mode, streaming state, and renderer selection do not become separate anatomy entries. |
107
- | FR6 | Without `components.math`, Markdown does not recognize math syntax. Default, legacy-set, `math: false`, and `ParseOptions`-annotated parser calls retain the released `InlineNode` / `BlockNode` result unions; only `MathParseOptions` returns the explicit math-enabled unions. |
108
- | FR7 | With math enabled, `$…$` produces an inline `math` node and `$$…$$` produces a block `math` node. The renderer receives the delimiter-free source as `value` and its placement as `display`. |
109
- | FR8 | Inline math stays on one line, cannot have whitespace touching either delimiter, and cannot open immediately after a digit or close immediately before one. `$$` is reserved for display math. These boundaries keep paired currency amounts literal. |
110
- | FR9 | A backslash-escaped dollar is literal outside math and does not close math inside it. An unmatched inline or display delimiter remains literal in non-streaming output. |
111
- | FR10 | Code spans and fenced code blocks are opaque to math parsing. Link destinations are opaque; link labels may contain inline math. Inline plugins run only on prose text and never inside math. |
112
- | FR11 | Streaming converges to the same nodes as a full parse at every chunk boundary, including display math nested in ordinary lists, task lists, blockquotes, and their supported combinations, with LF or CRLF and with or without source ranges. Incomplete math is withheld only while its exact owning container remains open; a list/quote exit or quote-depth change restores literal parsing. Math-enabled incremental calls require `IncrementalParseState<true>`, so the cache and returned union share one contract. |
152
+ | ID | Invariant |
153
+ | ---- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
154
+ | FR1 | Block and inline displays render one Document root carrying the current `markdown` target. Inline display renders no block anatomy. |
155
+ | FR2 | On the default block render path, Heading, Paragraph, List, Code block, Blockquote, Table, Divider, and Image carry the eight current local block targets documented below. |
156
+ | FR3 | A supplied `heading`, `paragraph`, `code`, `blockquote`, `hr`, or safe-URL `image` renderer replaces the corresponding default part, so Markdown does not impose that part's local target on the replacement. |
157
+ | FR4 | The released Code block target remains `markdown-codeblock`; this compatibility anomaly is not renamed or aliased. |
158
+ | FR5 | Density and Heading level remain reflected capabilities on their owning targets. Display mode, streaming state, and renderer selection do not become separate anatomy entries. |
159
+ | FR6 | Without `components.math`, Markdown does not recognize math syntax. Default, legacy-set, `math: false`, and `ParseOptions`-annotated parser calls retain the released `InlineNode` / `BlockNode` result unions; only `MathParseOptions` returns the explicit math-enabled unions. |
160
+ | FR7 | With math enabled, `$…$` produces an inline `math` node and `$$…$$` produces a block `math` node. The renderer receives the delimiter-free source as `value` and its placement as `display`. |
161
+ | FR8 | Inline math stays on one line, cannot have whitespace touching either delimiter, and cannot open immediately after a digit or close immediately before one. `$$` is reserved for display math. These boundaries keep paired currency amounts literal. |
162
+ | FR9 | A backslash-escaped dollar is literal outside math and does not close math inside it. An unmatched inline or display delimiter remains literal in non-streaming output. |
163
+ | FR10 | Code spans and fenced code blocks are opaque to math parsing. Link destinations are opaque; link labels may contain inline math. Inline plugins run only on prose text and never inside math. |
164
+ | FR11 | Streaming converges to the same nodes as a full parse at every chunk boundary, including display math nested in ordinary lists, task lists, blockquotes, and their supported combinations, with LF or CRLF and with or without source ranges. Incomplete math is withheld only while its exact owning container remains open; a list/quote exit or quote-depth change restores literal parsing. Math-enabled incremental calls require `IncrementalParseState<true>`, so the cache and returned union share one contract. |
165
+ | FR12 | Omitting `plugins` and passing an empty list are one semantic empty pipeline with identical parser unions, AST, DOM, styling, targets, IDs, and streaming behavior. Core may skip empty preparation and allocation without creating a separate behavior model. `components`, `inlinePlugins`, citations, autolinking, sources, and math retain their released meaning. |
166
+ | FR13 | Plugin-enabled parsing follows built-in lexical shields and ordered syntax claims, then uses one stable, strictly typed, MDAST-aligned canonical tree for transforms, rendering, and Outline. `MarkdownAstNodeMap` and `visitMarkdownNodes` narrow callbacks by node kind. Released parser functions preserve their existing result shape through a compatibility projection. Every returned root is finite, acyclic, representable, validated, and frozen before later plugins or rendering observe it. |
167
+ | FR14 | Plugin failures preserve the last valid document and readable authored source. Core retains heading, navigation, image, list, table, and document semantics and exposes no raw-markup parser channel, registry, package discovery, mutable shared AST, or unrestricted DOM hook. URL-like plugin data remains untrusted; Astryx-owned sinks follow `family:navigation-destinations`. |
168
+ | FR15 | Incremental parse identity contains every parse-affecting Markdown option and only ordered syntax-bearing plugin name, protocol version, and `parseKey`. Transform or renderer changes reuse settled parse output, rerun transformation, and do not remount unaffected extension output. |
169
+ | FR16 | Plugin-enabled Markdown and Markdown-derived Outline use the same parse options, ordered transforms, extension text projection, slugger, and collision allocator so every visible heading, Outline label, heading ID, and target agree. A limited Remark adapter may run only synchronous transform plugins whose input and output round-trip through the documented supported MDAST subset. |
170
+ | FR17 | An extension node declares `content` as `'none'`, `'phrasing'`, `'flow'`, or an explicit `{allow, min?, max?}` allowlist that narrows the category its `display` implies. Markdown parses every container's inner source span under the same grammar and shields, validates children at each transform boundary, renders children through the same built-in renderers and `components` seams, counts nesting toward the built-in depth bound, leaves FR16 heading traversal unchanged, and renders children in place when a container renderer fails. |
171
+ | FR18 | A transform may read and remove another plugin's extension nodes, including a subtree containing them, and may insert or remove headings; it may not create, edit, internally reorder, or duplicate another plugin's nodes, change a source heading's depth, or forge or duplicate heading identity. `dependsOn` is validated at preparation; an unmet or misordered dependency skips only that plugin's transform. Every rejection names the rule and owning plugin. |
172
+ | FR19 | `onPluginDiagnostic` is available on the component and parser options and receives one source-free event — plugin, phase, stable code, severity — per failure, advisory, or silent degradation, in development and production, rate-limited with a suppression code. Admission, duplicate-name, and protocol-version failures behave identically through the component and every parser entrypoint: the call succeeds with the last valid configuration and never throws into the caller. |
173
+ | FR20 | `parseMarkdownAst()` and `parseInlineAst()` return the canonical tree and accept the same options and plugin list as the component; `@astryxdesign/core/Markdown/parser` exposes parsing, canonical AST types, and plugin admission with no client boundary. `createMarkdownPlugin()` infers the extension-node union, so no callsite needs explicit type arguments, and a declaration that yields no usable extension type is a type error rather than a silent `never`. |
174
+ | FR21 | A transform runs again for every streamed update and must be idempotent and convergent; transforms whose effect requires complete input use the final-input signal. Semantically equal plugin lists reuse prepared work whether or not the array reference is stable, and development reports one diagnostic when a recreated list prevents reuse. |
175
+ | FR22 | An extension renderer may opt into the Markdown-owned extension theme target so themes reach plugin output. Opting out leaves output untargeted. The target adds no default styling or anatomy beyond the block spacing and content width Core already applies. |
176
+ | FR23 | Markdown owns an explicit supported dialect rather than claiming full CommonMark or GFM conformance. Adjacent compatible ordered or unordered items remain one list regardless of task-marker presence; each item independently preserves its checked state or ordinary list-item semantics, including at nested levels. The default grammar keeps its released task-list and table support, while `autolink: 'gfm'` adds only the documented autolink behavior and does not toggle any other syntax. |
177
+ | FR24 | `createMarkdownFrontmatter()` recognizes only a leading `---` block of unique `key: value` lines, decodes it through the caller's typed parser, stores finite JSON-like metadata on the canonical document, and removes the syntax from rendered content. An unfinished leading block yields no visible Markdown while streaming; malformed or unfinished final input remains ordinary Markdown. Frontmatter has no renderer and requires no Remark compatibility. |
178
+ | FR25 | The canonical parser and plugin-construction subpaths remain server-safe and can run function-bearing plugins entirely within server or RSC code. The client-owned `Markdown` component supports traditional and streaming SSR, but plugin entries containing functions cannot be serialized from a Server Component into that client boundary; direct RSC rendering requires a future additive server renderer rather than weakening the plugin protocol. |
113
179
 
114
180
  ### Allowed variation
115
181
 
@@ -125,21 +191,28 @@ unions. Enabled calls return the explicit `InlineNodeWithMath` and
125
191
  - **AV5 — Math renderer.** The caller may use any renderer that accepts the raw
126
192
  expression and display value. Its DOM, styles, typesetting engine, error UI,
127
193
  and accessibility representation are outside Markdown's ownership.
194
+ - **AV6 — Installed plugins.** A host may supply any ordered set of compatible
195
+ opaque plugin entries. Syntax, immutable transform behavior, renderer-owned
196
+ output, and helper implementation may vary while validation, readable fallback,
197
+ Core semantics, and heading identity stay fixed.
128
198
 
129
199
  ### Representative states
130
200
 
131
- | State | Required invariant | Allowed variation |
132
- | ---------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------- |
133
- | Default block content | Every parsed block uses its corresponding current Markdown target. | Block count, order, density, content width, and alignment. |
134
- | Custom block renderers | The replaced Heading, Paragraph, Code block, Blockquote, Divider, or Image lacks the corresponding Markdown target. | Replacement structure and styling. |
135
- | Ordered/unordered list | List carries `markdown-list`. | Marker kind, start value, item count, and nested content. |
136
- | Task list | The outer List part carries `markdown-list`. | Checked values and item content. |
137
- | Safe block image | Default Image carries `markdown-image`, or a custom image renderer replaces it. | Source and alternative text. |
138
- | Unsafe block image URL | Markdown renders its fallback Image part with `markdown-image`; no custom image renderer receives the rejected URL. | Alternative text shown by the fallback. |
139
- | Inline display | Document carries `markdown`; no block target renders. | Inline text, links, code, citations, plugins, and opt-in inline math. |
140
- | Math renderer absent | Dollar-delimited source follows the released Markdown grammar and no `math` node or renderer output exists. | Currency, unmatched delimiters, and ordinary prose. |
141
- | Math renderer present | Complete supported delimiters are opaque to Markdown formatting and are passed to the renderer as inert text. | Inline or block display and any renderer-owned output. |
142
- | Streaming math | Incomplete math is withheld; once complete, the streamed nodes equal the full-parse nodes at top level and inside list/blockquote containers. | Delimiters and expression text may arrive in separate chunks; source ranges remain optional. |
201
+ | State | Required invariant | Allowed variation |
202
+ | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------- |
203
+ | Default block content | Every parsed block uses its corresponding current Markdown target. | Block count, order, density, content width, and alignment. |
204
+ | Custom block renderers | The replaced Heading, Paragraph, Code block, Blockquote, Divider, or Image lacks the corresponding Markdown target. | Replacement structure and styling. |
205
+ | Ordered/unordered list | List carries `markdown-list`. | Marker kind, start value, item count, and nested content. |
206
+ | Task list | Each task-marked item carries its own checked state; mixed task/plain items stay in one compatible list and preserve document order and nesting. | Checked values, item content, and adjacent plain items. |
207
+ | Safe block image | Default Image carries `markdown-image`, or a custom image renderer replaces it. | Source and alternative text. |
208
+ | Unsafe block image URL | Markdown renders its fallback Image part with `markdown-image`; no custom image renderer receives the rejected URL. | Alternative text shown by the fallback. |
209
+ | Inline display | Document carries `markdown`; no block target renders. | Inline text, links, code, citations, plugins, and opt-in inline math. |
210
+ | Math renderer absent | Dollar-delimited source follows the released Markdown grammar and no `math` node or renderer output exists. | Currency, unmatched delimiters, and ordinary prose. |
211
+ | Math renderer present | Complete supported delimiters are opaque to Markdown formatting and are passed to the renderer as inert text. | Inline or block display and any renderer-owned output. |
212
+ | Streaming math | Incomplete math is withheld; once complete, the streamed nodes equal the full-parse nodes at top level and inside list/blockquote containers. | Delimiters and expression text may arrive in separate chunks; source ranges remain optional. |
213
+ | Plugins omitted | Released parser unions, AST, DOM, targets, heading IDs, and performance remain unchanged. | Omitted or empty list; both are one empty transform pipeline. |
214
+ | Plugins enabled | Fixed syntax → immutable transform → render order, validated roots, readable fallback, and matching Markdown/Outline heading identity remain invariant. | Syntax, transforms, renderers, helper execution plans, plugin order, and live post-parse state. |
215
+ | Native frontmatter | A complete leading block is absent from rendered content and yields typed metadata; unfinished streaming input is withheld. | Metadata schema and values are caller-defined finite data. |
143
216
 
144
217
  ### Transformation and precedence order
145
218
 
@@ -151,6 +224,16 @@ unions. Enabled calls return the explicit `InlineNodeWithMath` and
151
224
  display value. Markdown never turns it into HTML or executes it.
152
225
  - Existing URL sanitization remains in force for links and images; math adds no
153
226
  navigation or raw-HTML sink.
227
+ - Built-in syntax and protected contexts claim first; extension syntax claims only
228
+ eligible source; ordered transforms then receive deeply readonly document roots;
229
+ Core validates each returned root before rendering.
230
+ - A configured native frontmatter helper claims only the document-start delimiter.
231
+ It withholds an unfinished block during streaming, removes a completed block
232
+ before later transforms render the document, and makes typed metadata available
233
+ to those later transforms and to callers of that helper.
234
+ - Text matching, semantic fences, and source decorations use transform helpers. Core
235
+ may compile those helpers into indexed internal plans without exposing additional
236
+ public phases.
154
237
 
155
238
  ### Performance and resources
156
239
 
@@ -162,6 +245,8 @@ unions. Enabled calls return the explicit `InlineNodeWithMath` and
162
245
  container depth so an indented closer cannot become a new opener and a depth
163
246
  transition cannot swallow literal content. The factory-created state carries
164
247
  the same legacy or math-enabled node contract as the parser call.
248
+ - Stable and semantically equal plugin lists reuse prepared syntax and transform plans. Only syntax enters parse identity; transform and renderer changes reuse parsed output. Zero-work and representative transforms remain within `spec:AST-036` FR21–FR23 budgets, including the plugin-authored and streaming paths.
249
+ - Remark compatibility adapters, the conformance kit, and optional renderers stay outside Core bundles unless explicitly imported.
165
250
 
166
251
  ## Accessibility contract
167
252
 
@@ -170,7 +255,10 @@ scrollable Table wrapper, and image alternative text remain unchanged. Math has
170
255
  no Astryx-owned default output: the caller's renderer owns an accessible
171
256
  representation appropriate to its typesetting engine (for example MathML or a
172
257
  labelled `role="math"` element). Markdown adds no wrapper, ARIA attributes, or
173
- HTML injection around renderer output.
258
+ HTML injection around renderer output. Plugin renderers likewise own their
259
+ complete documented semantic pattern, while Core preserves its own document,
260
+ heading, navigation, image, list, and table semantics. Transforms cannot erase
261
+ required accessible meaning or make meaning color-only.
174
262
 
175
263
  ## Design relationships
176
264
 
@@ -230,22 +318,39 @@ and this change preserves the existing spelling exactly.
230
318
  current with the implementation.
231
319
  - `family:navigation-destinations` owns the shared accept/block result for parsed
232
320
  links and every Astryx-owned navigation sink. `spec:AST-005/DEC-1` requires
233
- Markdown navigation to remain conformant with Core link plumbing.
321
+ Markdown parsing and link rendering to preserve the same normalized navigation
322
+ decision as other Core links. Rejected links remain inert and do not reach a
323
+ custom link renderer; accepted links retain their ordinary behavior.
234
324
  - `spec:AST-005/DEC-2` keeps embedded-resource policy separate. Markdown may
235
325
  reject a broader set of image/resource URLs without narrowing the shared
236
326
  navigation contract.
327
+ - `spec:AST-036` owns the opaque syntax/transform/renderer protocol, immutable AST
328
+ validation, limited Remark compatibility, performance, and resource boundaries.
329
+ This record owns aggregate Markdown behavior in FR12–FR22;
330
+ `module:Markdown/remark` owns the separately imported Remark adapter's
331
+ supported subset, rejections, and diagnostics; and
332
+ `module:Outline/parseOutlineFromMarkdown` owns the corresponding Outline
333
+ projection.
237
334
  - Nested Astryx primitives retain ownership of their own anatomy and targets;
238
335
  Markdown owns the outer block targets listed here.
239
336
 
240
337
  ## Verification map
241
338
 
242
- | Contract | Verification | Representative states | Failure signal |
243
- | ---------------------- | -------------------------------------------------------------------------- | --------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
244
- | FR1–FR5 | `Markdown.test.tsx`, theme-target tests, and `scripts/check-knowledge.mjs` | Default block/inline output and all current targets | Existing DOM, target, spacing, or renderer behavior changes. |
245
- | FR6–FR10 | `parser.test.ts` and `Markdown.test.tsx` | Opt-out, inline/display math, escapes, currency, code, links, plugins | A delimiter is claimed without opt-in, TeX is formatted as Markdown, or opaque contexts leak. |
246
- | FR11 | `incremental.test.ts` and `Markdown.test.tsx` | Every-character top-level/list/task-list/blockquote splits, CRLF, source ranges | Streaming diverges from a full parse, shows partial syntax, mistakes a nested closer for an opener, or crosses a closed container. |
247
- | Public syntax/types | `Markdown.public.test.ts`, core typecheck, and `Markdown.doc.mjs` | Legacy exhaustive switches, component renderer, full and incremental math opt-ins | A legacy union widens, math-enabled results omit math nodes, or docs drift from declarations. |
248
- | Security/accessibility | `parser.test.ts`, `Markdown.test.tsx`, and renderer guidance | Inert expression strings and renderer-owned semantics | Astryx executes math as HTML or silently claims renderer-owned accessibility. |
339
+ | Contract | Verification | Representative states | Failure signal |
340
+ | ---------------------- | ----------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
341
+ | FR1–FR5 | `Markdown.test.tsx`, theme-target tests, and `scripts/check-knowledge.mjs` | Default block/inline output and all current targets | Existing DOM, target, spacing, or renderer behavior changes. |
342
+ | FR6–FR10 | `parser.test.ts` and `Markdown.test.tsx` | Opt-out, inline/display math, escapes, currency, code, links, plugins | A delimiter is claimed without opt-in, TeX is formatted as Markdown, or opaque contexts leak. |
343
+ | FR11 | `incremental.test.ts` and `Markdown.test.tsx` | Every-character top-level/list/task-list/blockquote splits, CRLF, source ranges | Streaming diverges from a full parse, shows partial syntax, mistakes a nested closer for an opener, or crosses a closed container. |
344
+ | FR12–FR16 | plugin, transform, adapter, performance, and Outline parser tests | omitted/empty lists, immutable transforms, invalid outputs, live updates, one compatible Remark plugin, duplicate headings | Empty behavior forks, input mutates, invalid structure escapes, transforms reparse, adapter loses content, budgets fail, or heading targets diverge. |
345
+ | FR17–FR18 | container parse/validation, ownership, and dependency tests | leaf/container declarations, nested containers, foreign read/remove/mint/edit, unmet/misordered dependencies | Plugin-built parsed children, invalid content, lost fallback children, foreign mint/edit, or generic ownership codes. |
346
+ | FR19 | diagnostic-channel tests in development and production | every phase, advisory reports, rate suppression, no handler, malformed list, duplicate Core, version skew | A silent production failure, document content in a diagnostic, or one entrypoint throwing where another recovers. |
347
+ | FR20–FR22 | canonical/server imports, inference, streaming, preparation, theming tests | server imports, no explicit type args, chunk boundaries, recreated lists, themed/unthemed extensions | Client references, explicit-type workarounds, oscillation, per-render re-preparation, or unreachable opted-in output. |
348
+ | FR23 | parser, renderer, nesting, and public option tests | task-only, plain-only, and mixed ordered/unordered lists at top level and nested; autolink omitted/enabled | A mixed list splits or loses order/state, a plain item becomes a checkbox, or `autolink: 'gfm'` changes non-autolink syntax. |
349
+ | FR24 | `plugins/frontmatter.test.tsx`, Storybook, and server rendering | complete, malformed, non-leading, LF/CRLF, unfinished streaming, full plugin stack | Metadata syntax renders after completion, unfinished syntax leaks while streaming, typing is lost, or later plugins stop composing. |
350
+ | FR25 | `parser.public.test.ts`, plugin SSR tests, and package export checks | server/RSC parsing with plugins, synchronous SSR, suspending renderer streaming boundary | A server import gains `use client`, plugin execution needs serialization, SSR loses fallback, or direct RSC rendering is misrepresented as supported. |
351
+ | Public syntax/types | `Markdown.public.test.ts`, core typecheck, and `Markdown.doc.mjs` | Legacy exhaustive switches, math opt-ins, inferred extension-node unions | A released union widens, an enabled union loses nodes, or docs drift from declarations. |
352
+ | Navigation contract | `parser.test.ts`, `Markdown.test.tsx`, and `Markdown.renderBoundary.test.tsx` | Parsed and rendered links, including transformed built-in links; accepted ordinary schemes; rejected destinations; links versus images | A blocked destination reaches navigation or a custom link renderer, or resource policy narrows accepted navigation. |
353
+ | Security/accessibility | `parser.test.ts`, `Markdown.test.tsx`, and renderer guidance | Inert expression strings and renderer-owned semantics | Astryx executes math as HTML or silently claims renderer-owned accessibility. |
249
354
 
250
355
  Focused tests continue to pin all nine current target names and default block
251
356
  placement. Math intentionally adds no target and no default anatomy.
@@ -274,6 +379,59 @@ Rejected: a generic AST/plugin escape hatch, raw HTML rendering, new list/table
274
379
  slots without consumer evidence, or a separate boolean on the component that
275
380
  could enable math without a renderer.
276
381
 
382
+ ### DEC-2 — Immutable transformation is the canonical Markdown extension seam
383
+
384
+ **Reference:** `component:Markdown/DEC-2`
385
+ **Decider:** `cixzhang`, `2026-09-15`
386
+
387
+ Markdown accepts one ordered `plugins` list whose opaque entries are created by
388
+ `createMarkdownPlugin()`. The public protocol exposes only bounded `syntax`,
389
+ immutable `transform`, and typed `renderers`. Core owns deep-readonly input,
390
+ validation and freezing of replacement roots, readable fallback, syntax-only parse
391
+ identity, preparation reuse, shared heading identity, containers, diagnostics, canonical/server parsing, and theming in FR12–FR22. Existing
392
+ `components`, `inlinePlugins`, math, citations, autolinking, and parser calls remain
393
+ compatible.
394
+
395
+ Text matching, semantic fences, and source decoration are transform helpers rather
396
+ than separate protocol phases. A tree-shakeable adapter may run only synchronous
397
+ transform-only Remark plugins over the documented MDAST subset; unsupported behavior
398
+ fails closed rather than being approximated.
399
+
400
+ This projects `spec:AST-036/DEC-1` through `DEC-4` into the component owner. It rejects a registry, package discovery, mutable shared AST, raw markup, a second plugin prop, or an unrestricted Unified runtime.
401
+
402
+ ### DEC-3 — Containers, diagnostics, and canonical APIs are Markdown-owned
403
+
404
+ **Reference:** `component:Markdown/DEC-3`
405
+ **Decider:** `cixzhang`, `2026-09-16`
406
+
407
+ Markdown parses every extension container's inner span itself and validates children against the plugin's declared content shape, so a callout holds real Markdown while heading identity, protected contexts, navigation policy, and Outline scope stay Core-owned. Containers change what a document can express, not which headings have identity: the released top-level traversal shared by heading IDs and Outline is untouched. A failed container renderer shows its children rather than literal source. Ownership rejections name the rule and owner; removal of another plugin's nodes is permitted and only minting, editing, internal reordering, duplication, and identity forgery are not.
408
+
409
+ Markdown also owns the protocol's observability and entry surface: `onPluginDiagnostic` makes every failure visible in production without carrying document content, admission failures degrade instead of throwing at any entrypoint, canonical parse entrypoints and a server-safe parser entry exist beside the released projection, extension types are inferred, and extension output may opt into one theme target without becoming default anatomy.
410
+
411
+ This projects `spec:AST-036/DEC-5` through `DEC-11` into the component owner in FR17–FR22. It rejects leaf-only extensions, plugin-authored parsed children, independent document shells, development-only or free-text diagnostics, parsers reachable only through a client barrel, required hand-written extension aliases, and default anatomy for plugin output.
412
+
413
+ ### DEC-4 — Own an explicit Markdown dialect, not a profile switch
414
+
415
+ **Reference:** `component:Markdown/DEC-4`
416
+ **Decider:** `cixzhang`, `2026-09-19`
417
+
418
+ Markdown's released grammar is an explicit Astryx-owned subset. Task-list markers are item semantics inside the ordinary ordered or unordered list structure, so mixed task and plain items stay in one compatible list and each item retains its own state at every nesting level. Released table and task-list syntax remains enabled by default. The optional `autolink: 'gfm'` value adds only the documented autolink behavior; it neither enables another syntax feature nor changes list structure.
419
+
420
+ This keeps documents stable as Astryx adds or declines individual ecosystem features. It rejects a blanket CommonMark or GFM conformance claim, aggregate all-task/all-plain classification, an implicit whole-grammar mode switch, and silently enabling future GFM features under the existing autolink option.
421
+
422
+ ### DEC-5 — Frontmatter is typed document metadata
423
+
424
+ **Reference:** `component:Markdown/DEC-5`
425
+ **Decider:** `cixzhang`, `2026-09-19`
426
+
427
+ Native frontmatter is a first-party helper in the ordered plugin pipeline. It
428
+ recognizes only a leading delimited block, parses a deliberately small key/value
429
+ grammar through a caller-provided typed decoder, and removes the syntax from the
430
+ rendered document. It does not create a visual extension node or require Remark's
431
+ frontmatter format. During streaming, incomplete frontmatter is withheld so raw
432
+ metadata never flashes as content; once closed, later plugins can consume the
433
+ metadata and the remaining document normally.
434
+
277
435
  ## Open questions
278
436
 
279
437
  - **OQ1 — Which focused tests should pin target absence for the five remaining
@@ -80,6 +80,17 @@ describe('Markdown', () => {
80
80
  }
81
81
  });
82
82
 
83
+ it('keeps citation markers out of released heading ids', () => {
84
+ render(
85
+ <Markdown sources={{cite: {title: 'Citation'}}}>
86
+ {'# Before [cite] after'}
87
+ </Markdown>,
88
+ );
89
+ expect(
90
+ screen.getByRole('heading', {name: /Before.*after/}),
91
+ ).toHaveAttribute('id', 'before-after');
92
+ });
93
+
83
94
  it('passes the generated id to a custom heading component', () => {
84
95
  const received: (string | undefined)[] = [];
85
96
  render(
@@ -625,6 +636,58 @@ describe('Markdown', () => {
625
636
  expect(links[1].getAttribute('href')).toBe('/page');
626
637
  });
627
638
 
639
+ describe('link destinations follow the shared navigation rule', () => {
640
+ // The same matrix Core's link plumbing and imperative navigation apply
641
+ // (utils/safeUrl.ts): only executable document schemes are blocked.
642
+ const blocked = [
643
+ 'javascript:alert(1)',
644
+ 'JaVaScRiPt:alert(1)',
645
+ 'vbscript:MsgBox(1)',
646
+ 'data:text/html,<script>alert(1)</script>',
647
+ 'java\nscript:alert(1)',
648
+ ];
649
+ const accepted = [
650
+ 'https://example.com',
651
+ '/page',
652
+ '#section',
653
+ '//example.com/x',
654
+ 'mailto:a@example.com',
655
+ 'tel:+15555550100',
656
+ 'data:image/png;base64,iVBORw0KGgo=',
657
+ ];
658
+
659
+ it.each(blocked)('renders %s as text, not a link', destination => {
660
+ const {container} = render(
661
+ <Markdown>{`[click](${destination})`}</Markdown>,
662
+ );
663
+ expect(container.querySelector('a')).toBeNull();
664
+ expect(container.textContent).toContain('click');
665
+ });
666
+
667
+ it.each(accepted)('renders %s as a link', destination => {
668
+ const {container} = render(
669
+ <Markdown>{`[click](${destination})`}</Markdown>,
670
+ );
671
+ const link = container.querySelector('a');
672
+ expect(link).not.toBeNull();
673
+ expect(link?.getAttribute('href')).toBe(destination);
674
+ });
675
+
676
+ it('a data:image link is navigation and is accepted, while a data:image image stays rejected by the resource policy', () => {
677
+ const {container} = render(
678
+ <Markdown>
679
+ {
680
+ '[view](data:image/png;base64,iVBORw0KGgo=)\n\n![pic](data:image/png;base64,iVBORw0KGgo=)'
681
+ }
682
+ </Markdown>,
683
+ );
684
+ expect(container.querySelector('a')?.getAttribute('href')).toBe(
685
+ 'data:image/png;base64,iVBORw0KGgo=',
686
+ );
687
+ expect(container.querySelector('img')).toBeNull();
688
+ });
689
+ });
690
+
628
691
  it('preserves dollar-delimited text when no math renderer is supplied', () => {
629
692
  const {container} = render(
630
693
  <Markdown>{'Total $5 and formula $x_1 + *y*$.'}</Markdown>,