@astryxdesign/core 0.6.2 → 0.6.3-canary.01fa439
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.
- package/CHANGELOG.md +81 -0
- package/dist/BottomSheet/BottomSheet.d.ts +1 -1
- package/dist/BottomSheet/BottomSheet.d.ts.map +1 -1
- package/dist/BottomSheet/BottomSheet.js +3 -1
- package/dist/BottomSheet/BottomSheetPanel.d.ts +7 -5
- package/dist/BottomSheet/BottomSheetPanel.d.ts.map +1 -1
- package/dist/BottomSheet/BottomSheetPanel.js +56 -19
- package/dist/Chat/ChatComposer.d.ts.map +1 -1
- package/dist/Chat/ChatComposer.js +10 -5
- package/dist/Chat/ChatComposerDrawer.d.ts +19 -5
- package/dist/Chat/ChatComposerDrawer.d.ts.map +1 -1
- package/dist/Chat/ChatComposerDrawer.js +48 -23
- package/dist/Chat/ChatComposerInput.d.ts +11 -3
- package/dist/Chat/ChatComposerInput.d.ts.map +1 -1
- package/dist/Chat/ChatComposerInput.js +63 -16
- package/dist/Chat/ChatDictationButton.d.ts +3 -2
- package/dist/Chat/ChatDictationButton.d.ts.map +1 -1
- package/dist/Chat/ChatDictationButton.js +6 -4
- package/dist/Chat/ChatSendButton.js +1 -1
- package/dist/Chat/index.d.ts +1 -1
- package/dist/Chat/index.d.ts.map +1 -1
- package/dist/CheckboxInput/CheckboxInput.d.ts.map +1 -1
- package/dist/CheckboxInput/CheckboxInput.js +12 -2
- package/dist/Collapsible/Collapsible.d.ts.map +1 -1
- package/dist/Collapsible/Collapsible.js +6 -1
- package/dist/CommandPalette/CommandPaletteInput.d.ts.map +1 -1
- package/dist/CommandPalette/CommandPaletteInput.js +1 -1
- package/dist/ComplexSelector/ComplexSelector.d.ts.map +1 -1
- package/dist/ComplexSelector/ComplexSelector.js +1 -1
- package/dist/DateInput/DateInput.d.ts.map +1 -1
- package/dist/DateInput/DateInput.js +4 -4
- package/dist/DateInput/NativeDateField.d.ts.map +1 -1
- package/dist/DateInput/NativeDateField.js +16 -16
- package/dist/DateInput/TouchDateField.d.ts.map +1 -1
- package/dist/DateInput/TouchDateField.js +2 -2
- package/dist/DateRangeInput/DateRangeInput.d.ts.map +1 -1
- package/dist/DateRangeInput/DateRangeInput.js +4 -4
- package/dist/DateTimeInput/DateTimeInput.d.ts.map +1 -1
- package/dist/DateTimeInput/DateTimeInput.js +8 -8
- package/dist/DateTimeInput/TouchDateTimeField.d.ts.map +1 -1
- package/dist/DateTimeInput/TouchDateTimeField.js +4 -4
- package/dist/DateTimeInput/nativePickerSegmentStyles.d.ts +9 -3
- package/dist/DateTimeInput/nativePickerSegmentStyles.d.ts.map +1 -1
- package/dist/DateTimeInput/nativePickerSegmentStyles.js +3 -3
- package/dist/Dialog/DialogHeader.d.ts +8 -1
- package/dist/Dialog/DialogHeader.d.ts.map +1 -1
- package/dist/Dialog/DialogHeader.js +10 -1
- package/dist/Field/InputClearButton.d.ts +4 -2
- package/dist/Field/InputClearButton.d.ts.map +1 -1
- package/dist/Field/InputClearButton.js +4 -2
- package/dist/Field/PanelSearchInput.d.ts.map +1 -1
- package/dist/Field/PanelSearchInput.js +1 -1
- package/dist/FileInput/FileInput.d.ts.map +1 -1
- package/dist/FileInput/FileInput.js +7 -7
- package/dist/Kbd/Kbd.d.ts +5 -3
- package/dist/Kbd/Kbd.d.ts.map +1 -1
- package/dist/Kbd/Kbd.js +36 -42
- package/dist/Link/Link.d.ts.map +1 -1
- package/dist/Link/Link.js +6 -2
- package/dist/Markdown/Markdown.d.ts +8 -2
- package/dist/Markdown/Markdown.d.ts.map +1 -1
- package/dist/Markdown/Markdown.fr23.bench.d.ts +2 -0
- package/dist/Markdown/Markdown.fr23.bench.d.ts.map +1 -0
- package/dist/Markdown/Markdown.fr23.bench.js +150 -0
- package/dist/Markdown/Markdown.fr23.sampling.d.ts +2 -0
- package/dist/Markdown/Markdown.fr23.sampling.d.ts.map +1 -0
- package/dist/Markdown/Markdown.fr23.sampling.js +43 -0
- package/dist/Markdown/Markdown.js +262 -109
- package/dist/Markdown/ast.d.ts +162 -0
- package/dist/Markdown/ast.d.ts.map +1 -0
- package/dist/Markdown/ast.js +74 -0
- package/dist/Markdown/index.d.ts +5 -3
- package/dist/Markdown/index.d.ts.map +1 -1
- package/dist/Markdown/index.js +3 -2
- package/dist/Markdown/parser/index.d.ts +10 -0
- package/dist/Markdown/parser/index.d.ts.map +1 -0
- package/dist/Markdown/parser/index.js +10 -0
- package/dist/Markdown/parser.d.ts +126 -58
- package/dist/Markdown/parser.d.ts.map +1 -1
- package/dist/Markdown/parser.js +716 -167
- package/dist/Markdown/plugins/frontmatter.d.ts +32 -0
- package/dist/Markdown/plugins/frontmatter.d.ts.map +1 -0
- package/dist/Markdown/plugins/frontmatter.js +132 -0
- package/dist/Markdown/plugins/index.d.ts +16 -0
- package/dist/Markdown/plugins/index.d.ts.map +1 -0
- package/dist/Markdown/plugins/index.js +13 -0
- package/dist/Markdown/plugins/protocol.d.ts +191 -0
- package/dist/Markdown/plugins/protocol.d.ts.map +1 -0
- package/dist/Markdown/plugins/protocol.js +957 -0
- package/dist/Markdown/plugins/semanticFence.d.ts +36 -0
- package/dist/Markdown/plugins/semanticFence.d.ts.map +1 -0
- package/dist/Markdown/plugins/semanticFence.js +213 -0
- package/dist/Markdown/plugins/sourceDecoration.d.ts +76 -0
- package/dist/Markdown/plugins/sourceDecoration.d.ts.map +1 -0
- package/dist/Markdown/plugins/sourceDecoration.js +393 -0
- package/dist/Markdown/plugins/textTransform.d.ts +30 -0
- package/dist/Markdown/plugins/textTransform.d.ts.map +1 -0
- package/dist/Markdown/plugins/textTransform.js +374 -0
- package/dist/Markdown/remark.d.ts +196 -0
- package/dist/Markdown/remark.d.ts.map +1 -0
- package/dist/Markdown/remark.js +1062 -0
- package/dist/Markdown/url.d.ts +4 -0
- package/dist/Markdown/url.d.ts.map +1 -0
- package/dist/Markdown/url.js +25 -0
- package/dist/Markdown/utils.d.ts +4 -2
- package/dist/Markdown/utils.d.ts.map +1 -1
- package/dist/Markdown/utils.js +2 -1
- package/dist/NumberInput/NumberInput.d.ts.map +1 -1
- package/dist/NumberInput/NumberInput.js +4 -4
- package/dist/Outline/index.d.ts +1 -0
- package/dist/Outline/index.d.ts.map +1 -1
- package/dist/Outline/parseOutlineFromMarkdown.d.ts +7 -1
- package/dist/Outline/parseOutlineFromMarkdown.d.ts.map +1 -1
- package/dist/Outline/parseOutlineFromMarkdown.js +14 -8
- package/dist/Outline/useOutlineFromMarkdown.d.ts +3 -1
- package/dist/Outline/useOutlineFromMarkdown.d.ts.map +1 -1
- package/dist/Outline/useOutlineFromMarkdown.js +8 -3
- package/dist/PowerSearch/PowerSearchEditPopover.d.ts.map +1 -1
- package/dist/PowerSearch/PowerSearchEditPopover.js +46 -30
- package/dist/ProgressBar/ProgressBar.d.ts.map +1 -1
- package/dist/ProgressBar/ProgressBar.js +36 -31
- package/dist/RadioList/RadioListItem.d.ts.map +1 -1
- package/dist/RadioList/RadioListItem.js +13 -1
- package/dist/SegmentedControl/SegmentedControlItem.d.ts.map +1 -1
- package/dist/SegmentedControl/SegmentedControlItem.js +5 -5
- package/dist/Selector/Selector.d.ts +9 -7
- package/dist/Selector/Selector.d.ts.map +1 -1
- package/dist/Selector/Selector.js +7 -5
- package/dist/SideNav/SideNav.d.ts +2 -1
- package/dist/SideNav/SideNav.d.ts.map +1 -1
- package/dist/SideNav/SideNav.js +9 -3
- package/dist/Slider/Slider.d.ts.map +1 -1
- package/dist/Slider/Slider.js +28 -12
- package/dist/Spinner/Spinner.d.ts.map +1 -1
- package/dist/Spinner/Spinner.js +23 -7
- package/dist/Switch/Switch.d.ts.map +1 -1
- package/dist/Switch/Switch.js +11 -0
- package/dist/TabList/Tab.d.ts +1 -1
- package/dist/TabList/Tab.d.ts.map +1 -1
- package/dist/TabList/Tab.js +20 -7
- package/dist/Table/Table.d.ts +1 -0
- package/dist/Table/Table.d.ts.map +1 -1
- package/dist/Table/Table.js +29 -13
- package/dist/TextArea/TextArea.d.ts.map +1 -1
- package/dist/TextArea/TextArea.js +1 -1
- package/dist/TextInput/TextInput.d.ts.map +1 -1
- package/dist/TextInput/TextInput.js +2 -2
- package/dist/TimeInput/TimeInput.d.ts.map +1 -1
- package/dist/TimeInput/TimeInput.js +4 -4
- package/dist/ToggleButton/ToggleButton.d.ts +9 -6
- package/dist/ToggleButton/ToggleButton.d.ts.map +1 -1
- package/dist/ToggleButton/ToggleButton.js +15 -27
- package/dist/TreeList/TreeList.d.ts.map +1 -1
- package/dist/TreeList/TreeList.js +3 -0
- package/dist/TreeList/TreeListItem.d.ts +6 -1
- package/dist/TreeList/TreeListItem.d.ts.map +1 -1
- package/dist/TreeList/TreeListItem.js +12 -2
- package/dist/TreeList/TreeListTypes.d.ts +8 -1
- package/dist/TreeList/TreeListTypes.d.ts.map +1 -1
- package/dist/Typeahead/BaseTypeahead.d.ts.map +1 -1
- package/dist/Typeahead/BaseTypeahead.js +16 -7
- package/dist/astryx.css +18 -7
- package/dist/hooks/scrollKeyboardDelegation.d.ts +3 -0
- package/dist/hooks/scrollKeyboardDelegation.d.ts.map +1 -0
- package/dist/hooks/scrollKeyboardDelegation.js +146 -0
- package/dist/hooks/useScrollableArea.d.ts +6 -2
- package/dist/hooks/useScrollableArea.d.ts.map +1 -1
- package/dist/hooks/useScrollableArea.js +17 -5
- package/dist/theme/generateThemeRules.d.ts.map +1 -1
- package/dist/theme/generateThemeRules.js +10 -0
- package/dist/utils/interactionOverlay.stylex.d.ts +8 -0
- package/dist/utils/interactionOverlay.stylex.d.ts.map +1 -1
- package/dist/utils/interactionOverlay.stylex.js +9 -0
- package/locales/af-ZA.json +530 -1
- package/locales/ar-SA.json +490 -1
- package/locales/ca-ES.json +514 -1
- package/locales/cs-CZ.json +498 -1
- package/locales/da-DK.json +522 -1
- package/locales/de-DE.json +522 -1
- package/locales/el-GR.json +498 -1
- package/locales/es-ES.json +510 -1
- package/locales/fi-FI.json +498 -1
- package/locales/fr-FR.json +534 -1
- package/locales/he-IL.json +498 -1
- package/locales/hu-HU.json +498 -1
- package/locales/it-IT.json +510 -1
- package/locales/ja-JP.json +490 -1
- package/locales/ko-KR.json +498 -1
- package/locales/nl-NL.json +534 -1
- package/locales/no-NO.json +510 -1
- package/locales/pl-PL.json +506 -1
- package/locales/pt-BR.json +506 -1
- package/locales/pt-PT.json +506 -1
- package/locales/ro-RO.json +506 -1
- package/locales/ru-RU.json +498 -1
- package/locales/sr-SP.json +498 -1
- package/locales/sv-SE.json +502 -1
- package/locales/tr-TR.json +498 -1
- package/locales/uk-UA.json +498 -1
- package/locales/vi-VN.json +502 -1
- package/locales/zh-CN.json +490 -1
- package/locales/zh-TW.json +490 -1
- package/package.json +19 -4
- package/src/BottomSheet/BottomSheet.doc.mjs +8 -1
- package/src/BottomSheet/BottomSheet.spec.md +46 -20
- package/src/BottomSheet/BottomSheet.test.tsx +6 -3
- package/src/BottomSheet/BottomSheet.tsx +3 -1
- package/src/BottomSheet/BottomSheetKeyboard.test.tsx +195 -0
- package/src/BottomSheet/BottomSheetPanel.test.tsx +11 -1
- package/src/BottomSheet/BottomSheetPanel.tsx +49 -16
- package/src/BottomSheet/__tests__/BottomSheetKeyboard.a11y.browser.spec.ts +344 -0
- package/src/Button/__tests__/Button.a11y.chromium.spec.ts +17 -1
- package/src/Button/__tests__/Button.a11y.known-failures.ts +0 -29
- package/src/Button/__tests__/Button.a11y.renders.tsx +9 -2
- package/src/Button/__tests__/Button.a11y.states.ts +9 -0
- package/src/Chat/ChatComposer.doc.mjs +14 -0
- package/src/Chat/ChatComposer.spec.md +230 -0
- package/src/Chat/ChatComposer.test.tsx +57 -0
- package/src/Chat/ChatComposer.tsx +21 -9
- package/src/Chat/ChatComposerDrawer.doc.mjs +63 -5
- package/src/Chat/ChatComposerDrawer.spec.md +267 -0
- package/src/Chat/ChatComposerDrawer.test.tsx +81 -12
- package/src/Chat/ChatComposerDrawer.tsx +38 -9
- package/src/Chat/ChatComposerInput.doc.mjs +114 -26
- package/src/Chat/ChatComposerInput.spec.md +185 -0
- package/src/Chat/ChatComposerInput.test.tsx +113 -3
- package/src/Chat/ChatComposerInput.tsx +78 -13
- package/src/Chat/ChatComposerTokenElement.spec.md +152 -0
- package/src/Chat/ChatComposerTokenElement.test.tsx +85 -0
- package/src/Chat/ChatDictationButton.doc.mjs +3 -3
- package/src/Chat/ChatDictationButton.spec.md +160 -0
- package/src/Chat/ChatDictationButton.test.tsx +92 -27
- package/src/Chat/ChatDictationButton.tsx +8 -6
- package/src/Chat/ChatSendButton.test.tsx +1 -4
- package/src/Chat/ChatSendButton.tsx +1 -1
- package/src/Chat/index.ts +1 -0
- package/src/CheckboxInput/CheckboxInput.doc.mjs +11 -0
- package/src/CheckboxInput/CheckboxInput.test.tsx +34 -0
- package/src/CheckboxInput/CheckboxInput.tsx +21 -1
- package/src/ClickableCard/ClickableCard.test.tsx +102 -5
- package/src/Collapsible/Collapsible.doc.mjs +11 -0
- package/src/Collapsible/Collapsible.test.tsx +21 -0
- package/src/Collapsible/Collapsible.tsx +5 -0
- package/src/CommandPalette/CommandPaletteInput.tsx +6 -1
- package/src/ComplexSelector/ComplexSelector.tsx +6 -1
- package/src/DateInput/DateInput.tsx +6 -1
- package/src/DateInput/NativeDateField.tsx +24 -8
- package/src/DateInput/TouchDateField.tsx +6 -2
- package/src/DateRangeInput/DateRangeInput.tsx +6 -1
- package/src/DateTimeInput/DateTimeInput.tsx +6 -1
- package/src/DateTimeInput/TouchDateTimeField.tsx +6 -1
- package/src/DateTimeInput/nativePickerSegmentStyles.ts +24 -8
- package/src/Dialog/DialogHeader.doc.mjs +12 -0
- package/src/Dialog/DialogHeader.test.tsx +97 -0
- package/src/Dialog/DialogHeader.tsx +25 -3
- package/src/Dialog/modules/DialogHeader.spec.md +75 -38
- package/src/Field/InputClearButton.test.tsx +17 -3
- package/src/Field/InputClearButton.tsx +5 -2
- package/src/Field/PanelSearchInput.tsx +5 -2
- package/src/FieldStatus/__tests__/StatusMessage.a11y.states.ts +1 -1
- package/src/FileInput/FileInput.tsx +11 -2
- package/src/Kbd/Kbd.doc.mjs +3 -3
- package/src/Kbd/Kbd.test.tsx +57 -1
- package/src/Kbd/Kbd.tsx +57 -37
- package/src/Layout/LayoutFooter.doc.mjs +17 -0
- package/src/Link/Link.doc.mjs +11 -0
- package/src/Link/Link.test.tsx +24 -0
- package/src/Link/Link.tsx +5 -0
- package/src/List/List.spec.md +200 -0
- package/src/Markdown/Markdown.doc.mjs +272 -0
- package/src/Markdown/Markdown.fr23.bench.ts +174 -0
- package/src/Markdown/Markdown.fr23.perf.test.ts +249 -0
- package/src/Markdown/Markdown.fr23.sampling.ts +49 -0
- package/src/Markdown/Markdown.helpers.perf.test.ts +489 -0
- package/src/Markdown/Markdown.public.test.ts +126 -3
- package/src/Markdown/Markdown.spec.md +197 -44
- package/src/Markdown/Markdown.test.tsx +11 -0
- package/src/Markdown/Markdown.tsx +414 -135
- package/src/Markdown/ast.test.ts +307 -0
- package/src/Markdown/ast.ts +337 -0
- package/src/Markdown/incremental.test.ts +12 -0
- package/src/Markdown/index.ts +37 -1
- package/src/Markdown/modules/remark.spec.md +269 -0
- package/src/Markdown/parser/index.ts +64 -0
- package/src/Markdown/parser.public.test.ts +103 -0
- package/src/Markdown/parser.ts +1180 -235
- package/src/Markdown/plugins/frontmatter.test.tsx +125 -0
- package/src/Markdown/plugins/frontmatter.ts +198 -0
- package/src/Markdown/plugins/index.ts +53 -0
- package/src/Markdown/plugins/protocol.test.tsx +1022 -0
- package/src/Markdown/plugins/protocol.ts +1618 -0
- package/src/Markdown/plugins/semanticFence.test.tsx +546 -0
- package/src/Markdown/plugins/semanticFence.ts +424 -0
- package/src/Markdown/plugins/sourceDecoration.test.tsx +799 -0
- package/src/Markdown/plugins/sourceDecoration.ts +539 -0
- package/src/Markdown/plugins/textTransform.test.ts +467 -0
- package/src/Markdown/plugins/textTransform.ts +554 -0
- package/src/Markdown/remark.test.tsx +1877 -0
- package/src/Markdown/remark.ts +1574 -0
- package/src/Markdown/url.ts +30 -0
- package/src/Markdown/utils.ts +13 -1
- package/src/MultiSelector/MultiSelector.test.tsx +1 -35
- package/src/MultiSelector/__tests__/MultiSelector.listbox.a11y.test.tsx +128 -0
- package/src/NumberInput/NumberInput.tsx +6 -1
- package/src/Outline/Outline.doc.mjs +7 -5
- package/src/Outline/Outline.spec.md +1 -1
- package/src/Outline/index.ts +1 -0
- package/src/Outline/modules/parseOutlineFromMarkdown.spec.md +142 -0
- package/src/Outline/parseOutlineFromMarkdown.ts +40 -10
- package/src/Outline/useOutlineFromMarkdown.ts +15 -3
- package/src/PowerSearch/PowerSearch.doc.mjs +2 -2
- package/src/PowerSearch/PowerSearchEditPopover.test.tsx +150 -1
- package/src/PowerSearch/PowerSearchEditPopover.tsx +51 -28
- package/src/ProgressBar/ProgressBar.test.tsx +29 -11
- package/src/ProgressBar/ProgressBar.tsx +59 -72
- package/src/RadioList/RadioList.doc.mjs +11 -0
- package/src/RadioList/RadioList.test.tsx +32 -0
- package/src/RadioList/RadioListItem.tsx +25 -1
- package/src/ScrollableArea/modules/useScrollableArea.spec.md +50 -22
- package/src/SegmentedControl/SegmentedControl.doc.mjs +2 -2
- package/src/SegmentedControl/SegmentedControl.test.tsx +31 -0
- package/src/SegmentedControl/SegmentedControlItem.tsx +6 -9
- package/src/Selector/Selector.doc.mjs +2 -2
- package/src/Selector/Selector.spec.md +51 -49
- package/src/Selector/Selector.test.tsx +61 -55
- package/src/Selector/Selector.tsx +23 -14
- package/src/Selector/__tests__/Listbox.a11y.chromium.spec.ts +130 -0
- package/src/Selector/__tests__/Listbox.a11y.dom.ts +38 -0
- package/src/Selector/__tests__/Listbox.a11y.inventory.test.ts +63 -0
- package/src/Selector/__tests__/Listbox.a11y.known-failures.ts +37 -0
- package/src/Selector/__tests__/Listbox.a11y.states.ts +232 -0
- package/src/Selector/__tests__/Selector.listbox.a11y.test.tsx +121 -0
- package/src/SideNav/SideNav.doc.mjs +1 -1
- package/src/SideNav/SideNav.test.tsx +10 -0
- package/src/SideNav/SideNav.tsx +14 -2
- package/src/Slider/Slider.doc.mjs +17 -3
- package/src/Slider/Slider.test.tsx +221 -0
- package/src/Slider/Slider.tsx +40 -9
- package/src/Spinner/Spinner.test.tsx +4 -0
- package/src/Spinner/Spinner.tsx +31 -18
- package/src/Switch/Switch.doc.mjs +11 -0
- package/src/Switch/Switch.test.tsx +16 -0
- package/src/Switch/Switch.tsx +28 -0
- package/src/TabList/Tab.tsx +35 -7
- package/src/TabList/TabList.doc.mjs +11 -0
- package/src/TabList/TabList.spec.md +204 -0
- package/src/TabList/TabList.test.tsx +66 -0
- package/src/TabList/__tests__/Tabs.a11y.known-failures.ts +1 -34
- package/src/Table/Table.doc.mjs +1 -1
- package/src/Table/Table.spec.md +54 -40
- package/src/Table/Table.test.tsx +83 -10
- package/src/Table/Table.tsx +30 -22
- package/src/Table/__tests__/TableScroll.a11y.chromium.spec.ts +137 -0
- package/src/TextArea/TextArea.tsx +6 -1
- package/src/TextInput/TextInput.test.tsx +20 -1
- package/src/TextInput/TextInput.tsx +6 -1
- package/src/TimeInput/TimeInput.tsx +6 -1
- package/src/ToggleButton/ToggleButton.doc.mjs +3 -3
- package/src/ToggleButton/ToggleButton.test.tsx +299 -4
- package/src/ToggleButton/ToggleButton.tsx +26 -33
- package/src/ToggleButton/ToggleButtonGroup.doc.mjs +3 -3
- package/src/ToggleButton/__tests__/ToggleButton.a11y.chromium.spec.ts +209 -0
- package/src/Tokenizer/Tokenizer.spec.md +142 -75
- package/src/TreeList/TreeList.doc.mjs +3 -3
- package/src/TreeList/TreeList.spec.md +123 -75
- package/src/TreeList/TreeList.test.tsx +29 -0
- package/src/TreeList/TreeList.tsx +3 -0
- package/src/TreeList/TreeListItem.tsx +15 -1
- package/src/TreeList/TreeListTypes.ts +11 -1
- package/src/Typeahead/BaseTypeahead.spec.md +4 -3
- package/src/Typeahead/BaseTypeahead.tsx +21 -7
- package/src/Typeahead/Typeahead.test.tsx +53 -0
- package/src/__tests__/PressedState.a11y.chromium.spec.ts +813 -0
- package/src/__tests__/pressState.ts +93 -0
- package/src/hooks/scrollKeyboardDelegation.test.ts +155 -0
- package/src/hooks/scrollKeyboardDelegation.ts +233 -0
- package/src/hooks/useScrollableArea.doc.mjs +15 -3
- package/src/hooks/useScrollableArea.test.tsx +59 -1
- package/src/hooks/useScrollableArea.ts +34 -10
- package/src/inputFontFloor.test.ts +106 -0
- package/src/tailwind-theme.css +0 -6
- package/src/theme/generateThemeRules.test.ts +30 -0
- package/src/theme/generateThemeRules.ts +10 -0
- package/src/utils/interactionOverlay.stylex.ts +20 -0
|
@@ -0,0 +1,1618 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file protocol.ts
|
|
5
|
+
* @input Markdown plugin definitions, syntax tokenizers, immutable transforms, and renderers
|
|
6
|
+
* @output Public plugin protocol plus validated internal preparation and execution
|
|
7
|
+
* @position Shared Markdown extension boundary consumed by the parser and renderer
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type React from 'react';
|
|
11
|
+
import {devError, warnOnce} from '../../utils/devWarning';
|
|
12
|
+
import {isSafeMarkdownParserUrl} from '../url';
|
|
13
|
+
import type {
|
|
14
|
+
MarkdownAstDataValue,
|
|
15
|
+
MarkdownAstExtensionNode,
|
|
16
|
+
MarkdownAstNodeBase,
|
|
17
|
+
MarkdownAstPosition,
|
|
18
|
+
MarkdownAstRoot,
|
|
19
|
+
} from '../ast';
|
|
20
|
+
|
|
21
|
+
export type MarkdownPluginData = MarkdownAstDataValue;
|
|
22
|
+
|
|
23
|
+
export type MarkdownExtensionNode<
|
|
24
|
+
PluginName extends string = string,
|
|
25
|
+
NodeName extends string = string,
|
|
26
|
+
Data extends MarkdownPluginData = MarkdownPluginData,
|
|
27
|
+
Display extends 'inline' | 'block' = 'inline' | 'block',
|
|
28
|
+
> = MarkdownAstExtensionNode<PluginName, NodeName, Data, Display>;
|
|
29
|
+
|
|
30
|
+
export function isMarkdownExtensionNode<Node extends MarkdownExtensionNode>(
|
|
31
|
+
node: MarkdownAstNodeBase & {readonly type: string},
|
|
32
|
+
plugin: Node['plugin'],
|
|
33
|
+
name: Node['name'],
|
|
34
|
+
): node is Node {
|
|
35
|
+
return (
|
|
36
|
+
node.type === 'extension' &&
|
|
37
|
+
(node as MarkdownExtensionNode).plugin === plugin &&
|
|
38
|
+
(node as MarkdownExtensionNode).name === name
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface MarkdownTokenizerInput {
|
|
43
|
+
readonly source: string;
|
|
44
|
+
/** UTF-16 offset into `source`. */
|
|
45
|
+
readonly offset: number;
|
|
46
|
+
/** Exclusive UTF-16 bound visible to this tokenizer. */
|
|
47
|
+
readonly end: number;
|
|
48
|
+
readonly isFinal: boolean;
|
|
49
|
+
readonly context: 'inline' | 'block';
|
|
50
|
+
readonly lineStart: number;
|
|
51
|
+
readonly column: number;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
type ExtensionNodeWithoutProvenance<Node extends MarkdownExtensionNode> = Omit<
|
|
55
|
+
Node,
|
|
56
|
+
'source' | 'position'
|
|
57
|
+
>;
|
|
58
|
+
|
|
59
|
+
export type MarkdownTokenizeResult<Node extends MarkdownExtensionNode> =
|
|
60
|
+
| {readonly status: 'no-match'}
|
|
61
|
+
| {readonly status: 'defer'}
|
|
62
|
+
| {
|
|
63
|
+
readonly status: 'match';
|
|
64
|
+
readonly end: number;
|
|
65
|
+
readonly node: ExtensionNodeWithoutProvenance<Node>;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
export interface MarkdownSyntaxContribution<
|
|
69
|
+
Node extends MarkdownExtensionNode = MarkdownExtensionNode,
|
|
70
|
+
> {
|
|
71
|
+
readonly startsWith: readonly [string, ...string[]];
|
|
72
|
+
/** Maximum UTF-16 span Core exposes from a candidate offset. */
|
|
73
|
+
readonly maxSpan: number;
|
|
74
|
+
tokenize(input: MarkdownTokenizerInput): MarkdownTokenizeResult<Node>;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
type ExtensionForDisplay<
|
|
78
|
+
Node extends MarkdownExtensionNode,
|
|
79
|
+
Display extends 'inline' | 'block',
|
|
80
|
+
> = Display extends Node['display']
|
|
81
|
+
? Node & {readonly display: Display}
|
|
82
|
+
: never;
|
|
83
|
+
|
|
84
|
+
export interface MarkdownSyntaxCapability<
|
|
85
|
+
Node extends MarkdownExtensionNode = MarkdownExtensionNode,
|
|
86
|
+
> {
|
|
87
|
+
readonly inline?: readonly [
|
|
88
|
+
MarkdownSyntaxContribution<ExtensionForDisplay<Node, 'inline'>>,
|
|
89
|
+
...MarkdownSyntaxContribution<ExtensionForDisplay<Node, 'inline'>>[],
|
|
90
|
+
];
|
|
91
|
+
readonly block?: readonly [
|
|
92
|
+
MarkdownSyntaxContribution<ExtensionForDisplay<Node, 'block'>>,
|
|
93
|
+
...MarkdownSyntaxContribution<ExtensionForDisplay<Node, 'block'>>[],
|
|
94
|
+
];
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export interface MarkdownTransformContext {
|
|
98
|
+
readonly source: string;
|
|
99
|
+
readonly isFinal: boolean;
|
|
100
|
+
readonly display: 'inline' | 'block';
|
|
101
|
+
report(message: string): void;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const markdownTransformPluginName = Symbol('MarkdownTransformPluginName');
|
|
105
|
+
const markdownTransformHasRenderer = Symbol('MarkdownTransformHasRenderer');
|
|
106
|
+
const markdownTransformSourceUnchanged = Symbol(
|
|
107
|
+
'MarkdownTransformSourceUnchanged',
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
type InternalMarkdownTransformContext = MarkdownTransformContext & {
|
|
111
|
+
readonly [markdownTransformPluginName]: string;
|
|
112
|
+
readonly [markdownTransformHasRenderer]: (nodeName: string) => boolean;
|
|
113
|
+
readonly [markdownTransformSourceUnchanged]: boolean;
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
/** @internal Reads the owning plugin name without expanding the public context. */
|
|
117
|
+
export function getMarkdownTransformPluginName(
|
|
118
|
+
context: MarkdownTransformContext,
|
|
119
|
+
): string | undefined {
|
|
120
|
+
return (context as Partial<InternalMarkdownTransformContext>)[
|
|
121
|
+
markdownTransformPluginName
|
|
122
|
+
];
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* @internal The ownership facts a Core helper needs to validate the nodes a
|
|
127
|
+
* caller's callback handed it, without seeing the rest of the pipeline.
|
|
128
|
+
*/
|
|
129
|
+
export interface MarkdownHelperOwnership {
|
|
130
|
+
readonly pluginName: string;
|
|
131
|
+
readonly hasRenderer: (nodeName: string) => boolean;
|
|
132
|
+
/** Whether source text still describes the current transform input. */
|
|
133
|
+
readonly sourceUnchanged: boolean;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** @internal Reads this transform's ownership, when Core is running it. */
|
|
137
|
+
export function getMarkdownHelperOwnership(
|
|
138
|
+
context: MarkdownTransformContext,
|
|
139
|
+
): MarkdownHelperOwnership | undefined {
|
|
140
|
+
const internal = context as Partial<InternalMarkdownTransformContext>;
|
|
141
|
+
const pluginName = internal[markdownTransformPluginName];
|
|
142
|
+
const hasRenderer = internal[markdownTransformHasRenderer];
|
|
143
|
+
const sourceUnchanged = internal[markdownTransformSourceUnchanged];
|
|
144
|
+
return pluginName === undefined ||
|
|
145
|
+
hasRenderer === undefined ||
|
|
146
|
+
sourceUnchanged === undefined
|
|
147
|
+
? undefined
|
|
148
|
+
: {pluginName, hasRenderer, sourceUnchanged};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
declare const markdownTransformNode: unique symbol;
|
|
152
|
+
|
|
153
|
+
export interface MarkdownTransform<Node extends MarkdownExtensionNode = never> {
|
|
154
|
+
(
|
|
155
|
+
document: MarkdownAstRoot<MarkdownExtensionNode>,
|
|
156
|
+
context: MarkdownTransformContext,
|
|
157
|
+
): MarkdownAstRoot<MarkdownExtensionNode>;
|
|
158
|
+
readonly [markdownTransformNode]?: Node;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const markdownTransformClaim = Symbol('MarkdownTransformClaim');
|
|
162
|
+
const markdownTransformTrusted = Symbol('MarkdownTransformTrusted');
|
|
163
|
+
|
|
164
|
+
interface ClaimAwareMarkdownTransform<
|
|
165
|
+
Node extends MarkdownExtensionNode = MarkdownExtensionNode,
|
|
166
|
+
> extends MarkdownTransform<Node> {
|
|
167
|
+
readonly [markdownTransformClaim]?: (source: string) => boolean;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
interface TrustAwareMarkdownTransform extends MarkdownTransform<never> {
|
|
171
|
+
readonly [markdownTransformTrusted]?: true;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* @internal Marks a Core-authored transform whose OUTPUT STRUCTURE is entirely
|
|
176
|
+
* Core-computed — it never inserts a caller-supplied node, never edits an
|
|
177
|
+
* existing node's meaning, provenance, or ownership, and never mutates its
|
|
178
|
+
* input. Core then skips the validation and immutability guards it applies to
|
|
179
|
+
* plugin-authored output, whose purpose is to contain untrusted structure.
|
|
180
|
+
*
|
|
181
|
+
* Only helpers in this package may be marked, and only when every node they
|
|
182
|
+
* emit is built here from validated inputs. A helper that inserts anything the
|
|
183
|
+
* caller supplied — the text helper's replacement nodes, for example — is NOT
|
|
184
|
+
* trusted: Core must still validate the tree it returns.
|
|
185
|
+
*/
|
|
186
|
+
export function markMarkdownTransformTrusted(
|
|
187
|
+
transform: MarkdownTransform<never>,
|
|
188
|
+
): MarkdownTransform<never> {
|
|
189
|
+
Object.defineProperty(transform, markdownTransformTrusted, {
|
|
190
|
+
configurable: false,
|
|
191
|
+
enumerable: false,
|
|
192
|
+
value: true,
|
|
193
|
+
writable: false,
|
|
194
|
+
});
|
|
195
|
+
return transform;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** @internal Registers a cheap source-level claim check for a helper transform. */
|
|
199
|
+
export function markMarkdownTransformClaim<
|
|
200
|
+
Node extends MarkdownExtensionNode = never,
|
|
201
|
+
>(
|
|
202
|
+
transform: MarkdownTransform<Node>,
|
|
203
|
+
claims: (source: string) => boolean,
|
|
204
|
+
): MarkdownTransform<Node> {
|
|
205
|
+
Object.defineProperty(transform, markdownTransformClaim, {
|
|
206
|
+
configurable: false,
|
|
207
|
+
enumerable: false,
|
|
208
|
+
value: claims,
|
|
209
|
+
writable: false,
|
|
210
|
+
});
|
|
211
|
+
return transform;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export interface MarkdownExtensionRenderer<Node extends MarkdownExtensionNode> {
|
|
215
|
+
/** Pure render callback. Hooks belong in components returned by this callback. */
|
|
216
|
+
readonly render: (props: {node: Node}) => React.ReactNode;
|
|
217
|
+
readonly toText: (node: Node) => string;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
export type MarkdownExtensionRenderers<Node extends MarkdownExtensionNode> =
|
|
221
|
+
Readonly<{
|
|
222
|
+
[NodeName in Node['name']]: MarkdownExtensionRenderer<
|
|
223
|
+
Extract<Node, {name: NodeName}>
|
|
224
|
+
>;
|
|
225
|
+
}>;
|
|
226
|
+
|
|
227
|
+
interface MarkdownPluginDefinitionBase<Name extends string> {
|
|
228
|
+
readonly name: Name;
|
|
229
|
+
readonly apiVersion: 1;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
export interface MarkdownSyntaxPluginDefinition<
|
|
233
|
+
Name extends string,
|
|
234
|
+
Node extends MarkdownExtensionNode<Name>,
|
|
235
|
+
> extends MarkdownPluginDefinitionBase<Name> {
|
|
236
|
+
readonly parseKey: string;
|
|
237
|
+
readonly syntax: MarkdownSyntaxCapability<Node>;
|
|
238
|
+
readonly transform?: MarkdownTransform<Node>;
|
|
239
|
+
readonly renderers: MarkdownExtensionRenderers<Node>;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
export type MarkdownTransformPluginDefinition<
|
|
243
|
+
Name extends string,
|
|
244
|
+
Node extends MarkdownExtensionNode<Name> = never,
|
|
245
|
+
> = MarkdownPluginDefinitionBase<Name> & {
|
|
246
|
+
readonly transform: MarkdownTransform<Node>;
|
|
247
|
+
readonly parseKey?: never;
|
|
248
|
+
readonly syntax?: never;
|
|
249
|
+
} & ([Node] extends [never]
|
|
250
|
+
? {readonly renderers?: never}
|
|
251
|
+
: {readonly renderers: MarkdownExtensionRenderers<Node>});
|
|
252
|
+
|
|
253
|
+
export type MarkdownPluginDefinition<
|
|
254
|
+
Name extends string,
|
|
255
|
+
Node extends MarkdownExtensionNode<Name> = never,
|
|
256
|
+
> =
|
|
257
|
+
| MarkdownSyntaxPluginDefinition<Name, Node>
|
|
258
|
+
| MarkdownTransformPluginDefinition<Name, Node>;
|
|
259
|
+
|
|
260
|
+
declare const markdownPluginNode: unique symbol;
|
|
261
|
+
|
|
262
|
+
/** Opaque, covariant entry safe to store in heterogeneous plugin lists. */
|
|
263
|
+
export interface MarkdownPluginEntry<
|
|
264
|
+
Node extends MarkdownExtensionNode = MarkdownExtensionNode,
|
|
265
|
+
> {
|
|
266
|
+
readonly name: string;
|
|
267
|
+
readonly apiVersion: 1;
|
|
268
|
+
readonly [markdownPluginNode]: Node;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
export type MarkdownNodeOf<Entry> =
|
|
272
|
+
Entry extends MarkdownPluginEntry<infer Node> ? Node : never;
|
|
273
|
+
|
|
274
|
+
export type MarkdownExtensionsOf<
|
|
275
|
+
Plugins extends ReadonlyArray<MarkdownPluginEntry>,
|
|
276
|
+
> = MarkdownNodeOf<Plugins[number]>;
|
|
277
|
+
|
|
278
|
+
const MARKDOWN_PLUGIN_BRAND = '@astryxdesign/core/MarkdownPluginEntry';
|
|
279
|
+
const markdownPluginDefinition = Symbol.for(MARKDOWN_PLUGIN_BRAND);
|
|
280
|
+
|
|
281
|
+
interface MarkdownPluginBrand<
|
|
282
|
+
Node extends MarkdownExtensionNode = MarkdownExtensionNode,
|
|
283
|
+
> {
|
|
284
|
+
readonly kind: typeof MARKDOWN_PLUGIN_BRAND;
|
|
285
|
+
readonly apiVersion: 1;
|
|
286
|
+
readonly definition: MarkdownPluginDefinition<string, Node>;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
interface InternalMarkdownPluginEntry<
|
|
290
|
+
Node extends MarkdownExtensionNode = MarkdownExtensionNode,
|
|
291
|
+
> extends MarkdownPluginEntry<Node> {
|
|
292
|
+
readonly [markdownPluginDefinition]: MarkdownPluginBrand<Node>;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function getMarkdownPluginDefinition(
|
|
296
|
+
publicEntry: MarkdownPluginEntry,
|
|
297
|
+
): MarkdownPluginDefinition<string, MarkdownExtensionNode> {
|
|
298
|
+
if (
|
|
299
|
+
publicEntry == null ||
|
|
300
|
+
typeof publicEntry !== 'object' ||
|
|
301
|
+
Object.getPrototypeOf(publicEntry) !== Object.prototype
|
|
302
|
+
) {
|
|
303
|
+
fail('entries must come from a compatible createMarkdownPlugin()');
|
|
304
|
+
}
|
|
305
|
+
const entry = publicEntry as Partial<InternalMarkdownPluginEntry>;
|
|
306
|
+
const brand = entry[markdownPluginDefinition];
|
|
307
|
+
const definition = brand?.definition;
|
|
308
|
+
if (
|
|
309
|
+
Reflect.ownKeys(entry).length !== 3 ||
|
|
310
|
+
typeof entry.name !== 'string' ||
|
|
311
|
+
entry.apiVersion !== 1 ||
|
|
312
|
+
!Object.isFrozen(entry) ||
|
|
313
|
+
brand == null ||
|
|
314
|
+
typeof brand !== 'object' ||
|
|
315
|
+
Object.getPrototypeOf(brand) !== Object.prototype ||
|
|
316
|
+
Reflect.ownKeys(brand).length !== 3 ||
|
|
317
|
+
!Object.isFrozen(brand) ||
|
|
318
|
+
brand.kind !== MARKDOWN_PLUGIN_BRAND ||
|
|
319
|
+
brand.apiVersion !== 1 ||
|
|
320
|
+
definition == null ||
|
|
321
|
+
typeof definition !== 'object' ||
|
|
322
|
+
!Object.isFrozen(definition) ||
|
|
323
|
+
definition.name !== entry.name ||
|
|
324
|
+
definition.apiVersion !== entry.apiVersion
|
|
325
|
+
) {
|
|
326
|
+
fail('entries must come from a compatible createMarkdownPlugin()');
|
|
327
|
+
}
|
|
328
|
+
return definition;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function deepFreezeConfig<T>(value: T, seen: Set<object> = new Set()): T {
|
|
332
|
+
if (value != null && typeof value === 'object' && !seen.has(value)) {
|
|
333
|
+
seen.add(value);
|
|
334
|
+
for (const nested of Object.values(value)) {
|
|
335
|
+
deepFreezeConfig(nested, seen);
|
|
336
|
+
}
|
|
337
|
+
Object.freeze(value);
|
|
338
|
+
}
|
|
339
|
+
return value;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function fail(message: string): never {
|
|
343
|
+
throw new Error(`Markdown plugin: ${message}`);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function validateSyntax(
|
|
347
|
+
pluginName: string,
|
|
348
|
+
syntax: MarkdownSyntaxCapability,
|
|
349
|
+
): void {
|
|
350
|
+
const contributions = [...(syntax.inline ?? []), ...(syntax.block ?? [])];
|
|
351
|
+
if (contributions.length === 0) {
|
|
352
|
+
fail(`"${pluginName}" syntax must declare an inline or block contribution`);
|
|
353
|
+
}
|
|
354
|
+
for (const contribution of contributions) {
|
|
355
|
+
if (!Number.isFinite(contribution.maxSpan) || contribution.maxSpan <= 0) {
|
|
356
|
+
fail(`"${pluginName}" syntax maxSpan must be positive and finite`);
|
|
357
|
+
}
|
|
358
|
+
for (const prefix of contribution.startsWith) {
|
|
359
|
+
if (prefix === '' || prefix.length > contribution.maxSpan) {
|
|
360
|
+
fail(`"${pluginName}" syntax prefixes must be non-empty and bounded`);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
export function createMarkdownPlugin<
|
|
367
|
+
const Name extends string,
|
|
368
|
+
const Node extends MarkdownExtensionNode<Name>,
|
|
369
|
+
>(
|
|
370
|
+
definition: MarkdownSyntaxPluginDefinition<Name, Node>,
|
|
371
|
+
): MarkdownPluginEntry<Node>;
|
|
372
|
+
export function createMarkdownPlugin<const Name extends string>(
|
|
373
|
+
definition: MarkdownTransformPluginDefinition<Name, never>,
|
|
374
|
+
): MarkdownPluginEntry<never>;
|
|
375
|
+
export function createMarkdownPlugin<
|
|
376
|
+
const Name extends string,
|
|
377
|
+
const Node extends MarkdownExtensionNode<Name>,
|
|
378
|
+
>(
|
|
379
|
+
definition: MarkdownTransformPluginDefinition<Name, Node>,
|
|
380
|
+
): MarkdownPluginEntry<Node>;
|
|
381
|
+
export function createMarkdownPlugin(
|
|
382
|
+
definition:
|
|
383
|
+
| MarkdownPluginDefinition<string, MarkdownExtensionNode>
|
|
384
|
+
| MarkdownTransformPluginDefinition<string, never>,
|
|
385
|
+
): MarkdownPluginEntry {
|
|
386
|
+
if (definition.name.trim() === '') {
|
|
387
|
+
fail('name must be a non-empty string');
|
|
388
|
+
}
|
|
389
|
+
if (definition.apiVersion !== 1) {
|
|
390
|
+
fail(`"${definition.name}" uses unsupported apiVersion`);
|
|
391
|
+
}
|
|
392
|
+
if (
|
|
393
|
+
typeof definition.transform !== 'undefined' &&
|
|
394
|
+
typeof definition.transform !== 'function'
|
|
395
|
+
) {
|
|
396
|
+
fail(`"${definition.name}" transform must be a function`);
|
|
397
|
+
}
|
|
398
|
+
if ('syntax' in definition && definition.syntax != null) {
|
|
399
|
+
if (definition.parseKey.trim() === '') {
|
|
400
|
+
fail(`"${definition.name}" parseKey must be non-empty`);
|
|
401
|
+
}
|
|
402
|
+
validateSyntax(definition.name, definition.syntax);
|
|
403
|
+
if (Object.keys(definition.renderers).length === 0) {
|
|
404
|
+
fail(`"${definition.name}" syntax must provide renderers`);
|
|
405
|
+
}
|
|
406
|
+
} else if (definition.transform == null) {
|
|
407
|
+
fail(`"${definition.name}" must declare syntax or transform`);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
const frozenDefinition = deepFreezeConfig(definition);
|
|
411
|
+
const brand = Object.freeze({
|
|
412
|
+
kind: MARKDOWN_PLUGIN_BRAND,
|
|
413
|
+
apiVersion: 1 as const,
|
|
414
|
+
definition: frozenDefinition,
|
|
415
|
+
});
|
|
416
|
+
return Object.freeze({
|
|
417
|
+
name: frozenDefinition.name,
|
|
418
|
+
apiVersion: 1 as const,
|
|
419
|
+
[markdownPluginDefinition]: brand,
|
|
420
|
+
}) as unknown as MarkdownPluginEntry;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
export interface PreparedSyntaxContribution {
|
|
424
|
+
readonly pluginName: string;
|
|
425
|
+
readonly display: 'inline' | 'block';
|
|
426
|
+
readonly contribution: MarkdownSyntaxContribution;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
interface PreparedTransform {
|
|
430
|
+
readonly pluginName: string;
|
|
431
|
+
readonly transform: MarkdownTransform<MarkdownExtensionNode>;
|
|
432
|
+
readonly claims?: (source: string) => boolean;
|
|
433
|
+
/** Core-authored helper whose output needs no plugin-output validation. */
|
|
434
|
+
readonly trusted: boolean;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
interface PreparedRenderer {
|
|
438
|
+
readonly pluginName: string;
|
|
439
|
+
readonly renderer: MarkdownExtensionRenderer<MarkdownExtensionNode>;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
export interface PreparedMarkdownPlugins {
|
|
443
|
+
readonly entries: ReadonlyArray<MarkdownPluginEntry>;
|
|
444
|
+
readonly syntaxEntries: ReadonlyArray<MarkdownPluginEntry>;
|
|
445
|
+
readonly inlineByFirstCharacter: ReadonlyMap<
|
|
446
|
+
string,
|
|
447
|
+
ReadonlyArray<PreparedSyntaxContribution>
|
|
448
|
+
>;
|
|
449
|
+
readonly blockByFirstCharacter: ReadonlyMap<
|
|
450
|
+
string,
|
|
451
|
+
ReadonlyArray<PreparedSyntaxContribution>
|
|
452
|
+
>;
|
|
453
|
+
readonly transforms: ReadonlyArray<PreparedTransform>;
|
|
454
|
+
/** Whether any transform is plugin-authored and needs Core's guards. */
|
|
455
|
+
readonly hasUntrustedTransform: boolean;
|
|
456
|
+
readonly syntaxIdentity: string;
|
|
457
|
+
readonly renderers: ReadonlyMap<string, PreparedRenderer>;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
const preparedPluginLists = new WeakMap<
|
|
461
|
+
ReadonlyArray<MarkdownPluginEntry>,
|
|
462
|
+
PreparedMarkdownPlugins
|
|
463
|
+
>();
|
|
464
|
+
|
|
465
|
+
function appendMapValue<T>(map: Map<string, T[]>, key: string, value: T): void {
|
|
466
|
+
const current = map.get(key);
|
|
467
|
+
if (current == null) {
|
|
468
|
+
map.set(key, [value]);
|
|
469
|
+
} else {
|
|
470
|
+
current.push(value);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
function syntaxOnlyEntry(
|
|
475
|
+
publicEntry: MarkdownPluginEntry,
|
|
476
|
+
): MarkdownPluginEntry {
|
|
477
|
+
const entry = publicEntry as InternalMarkdownPluginEntry;
|
|
478
|
+
const definition = getMarkdownPluginDefinition(publicEntry);
|
|
479
|
+
if (definition.transform == null) {
|
|
480
|
+
return publicEntry;
|
|
481
|
+
}
|
|
482
|
+
const syntaxDefinition = Object.freeze({...definition, transform: undefined});
|
|
483
|
+
return Object.freeze({
|
|
484
|
+
name: entry.name,
|
|
485
|
+
apiVersion: entry.apiVersion,
|
|
486
|
+
[markdownPluginDefinition]: Object.freeze({
|
|
487
|
+
kind: MARKDOWN_PLUGIN_BRAND,
|
|
488
|
+
apiVersion: 1 as const,
|
|
489
|
+
definition: syntaxDefinition,
|
|
490
|
+
}),
|
|
491
|
+
}) as unknown as MarkdownPluginEntry;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
export function prepareMarkdownPlugins(
|
|
495
|
+
plugins: ReadonlyArray<MarkdownPluginEntry>,
|
|
496
|
+
): PreparedMarkdownPlugins | undefined {
|
|
497
|
+
if (plugins.length === 0) {
|
|
498
|
+
return undefined;
|
|
499
|
+
}
|
|
500
|
+
const cached = preparedPluginLists.get(plugins);
|
|
501
|
+
if (cached != null) {
|
|
502
|
+
return cached;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
const names = new Set<string>();
|
|
506
|
+
const inlineByFirstCharacter = new Map<
|
|
507
|
+
string,
|
|
508
|
+
PreparedSyntaxContribution[]
|
|
509
|
+
>();
|
|
510
|
+
const blockByFirstCharacter = new Map<string, PreparedSyntaxContribution[]>();
|
|
511
|
+
const transforms: PreparedTransform[] = [];
|
|
512
|
+
const renderers = new Map<string, PreparedRenderer>();
|
|
513
|
+
const syntaxIdentity: string[] = [];
|
|
514
|
+
const entries: InternalMarkdownPluginEntry[] = [];
|
|
515
|
+
const syntaxEntries: MarkdownPluginEntry[] = [];
|
|
516
|
+
|
|
517
|
+
for (const publicEntry of plugins) {
|
|
518
|
+
const definition = getMarkdownPluginDefinition(publicEntry);
|
|
519
|
+
const entry = publicEntry as InternalMarkdownPluginEntry;
|
|
520
|
+
if (names.has(definition.name)) {
|
|
521
|
+
fail(`duplicate name "${definition.name}"`);
|
|
522
|
+
}
|
|
523
|
+
names.add(definition.name);
|
|
524
|
+
entries.push(entry);
|
|
525
|
+
|
|
526
|
+
if (definition.transform != null) {
|
|
527
|
+
transforms.push({
|
|
528
|
+
pluginName: definition.name,
|
|
529
|
+
transform: definition.transform,
|
|
530
|
+
claims: (definition.transform as ClaimAwareMarkdownTransform)[
|
|
531
|
+
markdownTransformClaim
|
|
532
|
+
],
|
|
533
|
+
trusted:
|
|
534
|
+
(definition.transform as TrustAwareMarkdownTransform)[
|
|
535
|
+
markdownTransformTrusted
|
|
536
|
+
] === true,
|
|
537
|
+
});
|
|
538
|
+
}
|
|
539
|
+
if (definition.renderers != null) {
|
|
540
|
+
for (const [nodeName, renderer] of Object.entries(definition.renderers)) {
|
|
541
|
+
renderers.set(`${definition.name}\0${nodeName}`, {
|
|
542
|
+
pluginName: definition.name,
|
|
543
|
+
renderer: renderer,
|
|
544
|
+
});
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
if ('syntax' in definition && definition.syntax != null) {
|
|
548
|
+
syntaxEntries.push(syntaxOnlyEntry(publicEntry));
|
|
549
|
+
syntaxIdentity.push(
|
|
550
|
+
`${definition.name}\0${definition.apiVersion}\0${definition.parseKey}`,
|
|
551
|
+
);
|
|
552
|
+
for (const contribution of definition.syntax.inline ?? []) {
|
|
553
|
+
const prepared: PreparedSyntaxContribution = {
|
|
554
|
+
pluginName: definition.name,
|
|
555
|
+
display: 'inline',
|
|
556
|
+
contribution,
|
|
557
|
+
};
|
|
558
|
+
for (const firstCharacter of new Set(
|
|
559
|
+
contribution.startsWith.map(prefix => prefix[0]),
|
|
560
|
+
)) {
|
|
561
|
+
appendMapValue(inlineByFirstCharacter, firstCharacter, prepared);
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
for (const contribution of definition.syntax.block ?? []) {
|
|
565
|
+
const prepared: PreparedSyntaxContribution = {
|
|
566
|
+
pluginName: definition.name,
|
|
567
|
+
display: 'block',
|
|
568
|
+
contribution,
|
|
569
|
+
};
|
|
570
|
+
for (const firstCharacter of new Set(
|
|
571
|
+
contribution.startsWith.map(prefix => prefix[0]),
|
|
572
|
+
)) {
|
|
573
|
+
appendMapValue(blockByFirstCharacter, firstCharacter, prepared);
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
const prepared: PreparedMarkdownPlugins = {
|
|
580
|
+
entries,
|
|
581
|
+
syntaxEntries,
|
|
582
|
+
inlineByFirstCharacter,
|
|
583
|
+
blockByFirstCharacter,
|
|
584
|
+
transforms,
|
|
585
|
+
hasUntrustedTransform: transforms.some(entry => !entry.trusted),
|
|
586
|
+
syntaxIdentity: syntaxIdentity.join('\u0001'),
|
|
587
|
+
renderers,
|
|
588
|
+
};
|
|
589
|
+
preparedPluginLists.set(plugins, prepared);
|
|
590
|
+
return prepared;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
export function isMarkdownPluginData(
|
|
594
|
+
value: unknown,
|
|
595
|
+
seen: Set<object> = new Set(),
|
|
596
|
+
): value is MarkdownPluginData {
|
|
597
|
+
if (
|
|
598
|
+
value == null ||
|
|
599
|
+
typeof value === 'boolean' ||
|
|
600
|
+
typeof value === 'string'
|
|
601
|
+
) {
|
|
602
|
+
return true;
|
|
603
|
+
}
|
|
604
|
+
if (typeof value === 'number') {
|
|
605
|
+
return Number.isFinite(value);
|
|
606
|
+
}
|
|
607
|
+
if (typeof value !== 'object' || seen.has(value)) {
|
|
608
|
+
return false;
|
|
609
|
+
}
|
|
610
|
+
seen.add(value);
|
|
611
|
+
let valid: boolean;
|
|
612
|
+
if (Array.isArray(value)) {
|
|
613
|
+
valid = value.every(item => isMarkdownPluginData(item, seen));
|
|
614
|
+
} else if (Object.getPrototypeOf(value) !== Object.prototype) {
|
|
615
|
+
valid = false;
|
|
616
|
+
} else {
|
|
617
|
+
valid = Object.values(value).every(item =>
|
|
618
|
+
isMarkdownPluginData(item, seen),
|
|
619
|
+
);
|
|
620
|
+
}
|
|
621
|
+
seen.delete(value);
|
|
622
|
+
return valid;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
export function freezeMarkdownPluginData<T extends MarkdownPluginData>(
|
|
626
|
+
value: T,
|
|
627
|
+
): T {
|
|
628
|
+
if (value != null && typeof value === 'object' && !Object.isFrozen(value)) {
|
|
629
|
+
for (const nested of Object.values(value)) {
|
|
630
|
+
freezeMarkdownPluginData(nested);
|
|
631
|
+
}
|
|
632
|
+
Object.freeze(value);
|
|
633
|
+
}
|
|
634
|
+
return value;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
function positionKey(position: MarkdownAstPosition | undefined): string | null {
|
|
638
|
+
const start = position?.start.offset;
|
|
639
|
+
const end = position?.end.offset;
|
|
640
|
+
return start == null || end == null ? null : `${start}:${end}`;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
interface CodeInternalProperties {
|
|
644
|
+
readonly descriptors: ReadonlyArray<readonly [symbol, PropertyDescriptor]>;
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
function codeSourceSignature(node: Record<string, unknown>): string {
|
|
648
|
+
return JSON.stringify([
|
|
649
|
+
node.lang,
|
|
650
|
+
node.meta,
|
|
651
|
+
node.value,
|
|
652
|
+
node.position ?? null,
|
|
653
|
+
]);
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
function collectCodeInternalProperties(
|
|
657
|
+
root: MarkdownAstRoot<MarkdownExtensionNode>,
|
|
658
|
+
): Map<string, CodeInternalProperties[]> {
|
|
659
|
+
const properties = new Map<string, CodeInternalProperties[]>();
|
|
660
|
+
const visit = (node: MarkdownAstNodeBase & {readonly type: string}): void => {
|
|
661
|
+
if (node.type === 'code') {
|
|
662
|
+
const descriptors = Object.getOwnPropertySymbols(node)
|
|
663
|
+
.map(
|
|
664
|
+
symbol =>
|
|
665
|
+
[symbol, Object.getOwnPropertyDescriptor(node, symbol)] as const,
|
|
666
|
+
)
|
|
667
|
+
.filter(
|
|
668
|
+
(entry): entry is readonly [symbol, PropertyDescriptor] =>
|
|
669
|
+
entry[1]?.enumerable === true,
|
|
670
|
+
);
|
|
671
|
+
if (descriptors.length > 0) {
|
|
672
|
+
const key = codeSourceSignature(
|
|
673
|
+
node as unknown as Record<string, unknown>,
|
|
674
|
+
);
|
|
675
|
+
properties.set(key, [...(properties.get(key) ?? []), {descriptors}]);
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
if ('children' in node && Array.isArray(node.children)) {
|
|
679
|
+
for (const child of node.children) {
|
|
680
|
+
if (child != null && typeof child === 'object') {
|
|
681
|
+
visit(child as MarkdownAstNodeBase & {readonly type: string});
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
};
|
|
686
|
+
visit(root);
|
|
687
|
+
return properties;
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
function restoreCodeInternalProperties(
|
|
691
|
+
previous: MarkdownAstRoot<MarkdownExtensionNode>,
|
|
692
|
+
next: MarkdownAstRoot<MarkdownExtensionNode>,
|
|
693
|
+
): MarkdownAstRoot<MarkdownExtensionNode> {
|
|
694
|
+
const properties = collectCodeInternalProperties(previous);
|
|
695
|
+
if (properties.size === 0) {
|
|
696
|
+
return next;
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
const visit = (
|
|
700
|
+
node: MarkdownAstNodeBase & {readonly type: string},
|
|
701
|
+
): MarkdownAstNodeBase & {readonly type: string} => {
|
|
702
|
+
let current = node;
|
|
703
|
+
if (node.type === 'code') {
|
|
704
|
+
const key = codeSourceSignature(
|
|
705
|
+
node as unknown as Record<string, unknown>,
|
|
706
|
+
);
|
|
707
|
+
const queue = properties.get(key);
|
|
708
|
+
const internal = queue?.shift();
|
|
709
|
+
const shouldRestore = internal?.descriptors.some(
|
|
710
|
+
([symbol, descriptor]) => {
|
|
711
|
+
const current = Object.getOwnPropertyDescriptor(node, symbol);
|
|
712
|
+
return (
|
|
713
|
+
current == null ||
|
|
714
|
+
current.value !== descriptor.value ||
|
|
715
|
+
current.get !== descriptor.get ||
|
|
716
|
+
current.set !== descriptor.set ||
|
|
717
|
+
current.enumerable !== descriptor.enumerable ||
|
|
718
|
+
current.configurable !== descriptor.configurable ||
|
|
719
|
+
current.writable !== descriptor.writable
|
|
720
|
+
);
|
|
721
|
+
},
|
|
722
|
+
);
|
|
723
|
+
if (internal != null && shouldRestore === true) {
|
|
724
|
+
const clone = {...node};
|
|
725
|
+
for (const [symbol, descriptor] of internal.descriptors) {
|
|
726
|
+
Object.defineProperty(clone, symbol, descriptor);
|
|
727
|
+
}
|
|
728
|
+
current = clone;
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
if ('children' in current && Array.isArray(current.children)) {
|
|
732
|
+
const currentChildren = current.children as ReadonlyArray<
|
|
733
|
+
MarkdownAstNodeBase & {readonly type: string}
|
|
734
|
+
>;
|
|
735
|
+
const children = currentChildren.map(child => visit(child));
|
|
736
|
+
if (children.some((child, index) => child !== currentChildren[index])) {
|
|
737
|
+
const parent: MarkdownAstNodeBase & {
|
|
738
|
+
readonly type: string;
|
|
739
|
+
readonly children: ReadonlyArray<
|
|
740
|
+
MarkdownAstNodeBase & {readonly type: string}
|
|
741
|
+
>;
|
|
742
|
+
} = {...current, children};
|
|
743
|
+
current = parent;
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
return current;
|
|
747
|
+
};
|
|
748
|
+
|
|
749
|
+
return visit(next) as MarkdownAstRoot<MarkdownExtensionNode>;
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
interface SourceInvariant {
|
|
753
|
+
readonly type: string;
|
|
754
|
+
readonly depth?: unknown;
|
|
755
|
+
readonly ordered?: unknown;
|
|
756
|
+
readonly start?: unknown;
|
|
757
|
+
readonly delimiter?: unknown;
|
|
758
|
+
readonly plugin?: unknown;
|
|
759
|
+
readonly name?: unknown;
|
|
760
|
+
readonly display?: unknown;
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
function collectSourceInvariants(
|
|
764
|
+
root: MarkdownAstRoot<MarkdownExtensionNode>,
|
|
765
|
+
): Map<string, SourceInvariant[]> {
|
|
766
|
+
const positions = new Map<string, SourceInvariant[]>();
|
|
767
|
+
const visit = (node: MarkdownAstNodeBase & {readonly type: string}): void => {
|
|
768
|
+
const key = positionKey(node.position);
|
|
769
|
+
if (key != null) {
|
|
770
|
+
const record = node as unknown as Record<string, unknown>;
|
|
771
|
+
const invariant = {
|
|
772
|
+
type: node.type,
|
|
773
|
+
depth: record.depth,
|
|
774
|
+
ordered: record.ordered,
|
|
775
|
+
start: record.start,
|
|
776
|
+
delimiter: record.delimiter,
|
|
777
|
+
plugin: record.plugin,
|
|
778
|
+
name: record.name,
|
|
779
|
+
display: record.display,
|
|
780
|
+
};
|
|
781
|
+
positions.set(key, [...(positions.get(key) ?? []), invariant]);
|
|
782
|
+
}
|
|
783
|
+
if ('children' in node && Array.isArray(node.children)) {
|
|
784
|
+
for (const child of node.children) {
|
|
785
|
+
if (child != null && typeof child === 'object') {
|
|
786
|
+
visit(child as MarkdownAstNodeBase & {readonly type: string});
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
};
|
|
791
|
+
visit(root);
|
|
792
|
+
return positions;
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
function extensionSignature(node: Record<string, unknown>): string {
|
|
796
|
+
return JSON.stringify([
|
|
797
|
+
node.plugin,
|
|
798
|
+
node.name,
|
|
799
|
+
node.display,
|
|
800
|
+
node.data,
|
|
801
|
+
node.source,
|
|
802
|
+
positionKey(node.position as MarkdownAstPosition | undefined),
|
|
803
|
+
]);
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
interface ExistingExtension {
|
|
807
|
+
readonly plugin: string;
|
|
808
|
+
count: number;
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
function collectExtensionSignatures(
|
|
812
|
+
root: MarkdownAstRoot<MarkdownExtensionNode>,
|
|
813
|
+
): Map<string, ExistingExtension> {
|
|
814
|
+
const signatures = new Map<string, ExistingExtension>();
|
|
815
|
+
const visit = (node: MarkdownAstNodeBase & {readonly type: string}): void => {
|
|
816
|
+
if (node.type === 'extension') {
|
|
817
|
+
const signature = extensionSignature(
|
|
818
|
+
node as unknown as Record<string, unknown>,
|
|
819
|
+
);
|
|
820
|
+
const existing = signatures.get(signature);
|
|
821
|
+
signatures.set(signature, {
|
|
822
|
+
plugin: String((node as unknown as Record<string, unknown>).plugin),
|
|
823
|
+
count: (existing?.count ?? 0) + 1,
|
|
824
|
+
});
|
|
825
|
+
}
|
|
826
|
+
if ('children' in node && Array.isArray(node.children)) {
|
|
827
|
+
for (const child of node.children) {
|
|
828
|
+
if (child != null && typeof child === 'object') {
|
|
829
|
+
visit(child as MarkdownAstNodeBase & {readonly type: string});
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
};
|
|
834
|
+
visit(root);
|
|
835
|
+
return signatures;
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
const sourceHeadingMarker = Symbol('MarkdownSourceHeading');
|
|
839
|
+
|
|
840
|
+
interface SourceHeadingMarker {
|
|
841
|
+
readonly depth: number;
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
function markSourceHeadings(
|
|
845
|
+
root: MarkdownAstRoot<MarkdownExtensionNode>,
|
|
846
|
+
): Set<SourceHeadingMarker> {
|
|
847
|
+
const markers = new Set<SourceHeadingMarker>();
|
|
848
|
+
const visit = (node: MarkdownAstNodeBase & {readonly type: string}): void => {
|
|
849
|
+
if (node.type === 'heading' && !(sourceHeadingMarker in node)) {
|
|
850
|
+
Object.defineProperty(node, sourceHeadingMarker, {
|
|
851
|
+
configurable: false,
|
|
852
|
+
enumerable: true,
|
|
853
|
+
value: Object.freeze({
|
|
854
|
+
depth: (node as unknown as {readonly depth: number}).depth,
|
|
855
|
+
} satisfies SourceHeadingMarker),
|
|
856
|
+
writable: false,
|
|
857
|
+
});
|
|
858
|
+
}
|
|
859
|
+
if (node.type === 'heading') {
|
|
860
|
+
markers.add(
|
|
861
|
+
(
|
|
862
|
+
node as unknown as {
|
|
863
|
+
readonly [sourceHeadingMarker]: SourceHeadingMarker;
|
|
864
|
+
}
|
|
865
|
+
)[sourceHeadingMarker],
|
|
866
|
+
);
|
|
867
|
+
}
|
|
868
|
+
if ('children' in node && Array.isArray(node.children)) {
|
|
869
|
+
for (const child of node.children) {
|
|
870
|
+
if (child != null && typeof child === 'object') {
|
|
871
|
+
visit(child as MarkdownAstNodeBase & {readonly type: string});
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
};
|
|
876
|
+
visit(root);
|
|
877
|
+
return markers;
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
const PHRASING_TYPES = new Set([
|
|
881
|
+
'text',
|
|
882
|
+
'strong',
|
|
883
|
+
'emphasis',
|
|
884
|
+
'delete',
|
|
885
|
+
'inlineCode',
|
|
886
|
+
'inlineMath',
|
|
887
|
+
'break',
|
|
888
|
+
'link',
|
|
889
|
+
'image',
|
|
890
|
+
'citation',
|
|
891
|
+
]);
|
|
892
|
+
const BLOCK_TYPES = new Set([
|
|
893
|
+
'heading',
|
|
894
|
+
'paragraph',
|
|
895
|
+
'code',
|
|
896
|
+
'math',
|
|
897
|
+
'blockquote',
|
|
898
|
+
'list',
|
|
899
|
+
'table',
|
|
900
|
+
'thematicBreak',
|
|
901
|
+
'image',
|
|
902
|
+
]);
|
|
903
|
+
const PHRASING_PARENTS = new Set([
|
|
904
|
+
'heading',
|
|
905
|
+
'paragraph',
|
|
906
|
+
'strong',
|
|
907
|
+
'emphasis',
|
|
908
|
+
'delete',
|
|
909
|
+
'link',
|
|
910
|
+
'tableCell',
|
|
911
|
+
]);
|
|
912
|
+
const BLOCK_PARENTS = new Set(['root', 'blockquote', 'listItem']);
|
|
913
|
+
const CHILD_PARENT_TYPES = new Set([
|
|
914
|
+
...PHRASING_PARENTS,
|
|
915
|
+
...BLOCK_PARENTS,
|
|
916
|
+
'list',
|
|
917
|
+
'table',
|
|
918
|
+
'tableRow',
|
|
919
|
+
]);
|
|
920
|
+
|
|
921
|
+
function validateAst(
|
|
922
|
+
root: unknown,
|
|
923
|
+
pluginNames: ReadonlySet<string>,
|
|
924
|
+
rendererKeys: ReadonlySet<string>,
|
|
925
|
+
sourcePositions: Map<string, SourceInvariant[]>,
|
|
926
|
+
sourceHeadingMarkers: ReadonlySet<SourceHeadingMarker>,
|
|
927
|
+
activePluginName: string,
|
|
928
|
+
existingExtensions: Map<string, ExistingExtension>,
|
|
929
|
+
display: 'inline' | 'block',
|
|
930
|
+
): root is MarkdownAstRoot<MarkdownExtensionNode> {
|
|
931
|
+
if (root == null || typeof root !== 'object') {
|
|
932
|
+
return false;
|
|
933
|
+
}
|
|
934
|
+
const seen = new Set<object>();
|
|
935
|
+
const seenHeadingMarkers = new Set<SourceHeadingMarker>();
|
|
936
|
+
let count = 0;
|
|
937
|
+
const visit = (
|
|
938
|
+
value: unknown,
|
|
939
|
+
parent: string | null,
|
|
940
|
+
insideLink: boolean,
|
|
941
|
+
): boolean => {
|
|
942
|
+
if (value == null || typeof value !== 'object' || seen.has(value)) {
|
|
943
|
+
return false;
|
|
944
|
+
}
|
|
945
|
+
if (++count > 100_000) {
|
|
946
|
+
return false;
|
|
947
|
+
}
|
|
948
|
+
seen.add(value);
|
|
949
|
+
const node = value as Record<string, unknown>;
|
|
950
|
+
const type = node.type;
|
|
951
|
+
if (typeof type !== 'string') {
|
|
952
|
+
return false;
|
|
953
|
+
}
|
|
954
|
+
if (node.data !== undefined && !isMarkdownPluginData(node.data)) {
|
|
955
|
+
return false;
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
if (parent != null) {
|
|
959
|
+
const extensionDisplay = type === 'extension' ? node.display : undefined;
|
|
960
|
+
const phrasing =
|
|
961
|
+
PHRASING_TYPES.has(type) || extensionDisplay === 'inline';
|
|
962
|
+
const block = BLOCK_TYPES.has(type) || extensionDisplay === 'block';
|
|
963
|
+
if (
|
|
964
|
+
(PHRASING_PARENTS.has(parent) && !phrasing) ||
|
|
965
|
+
(BLOCK_PARENTS.has(parent) && !block) ||
|
|
966
|
+
(insideLink && type === 'link') ||
|
|
967
|
+
(parent === 'list' && type !== 'listItem') ||
|
|
968
|
+
(parent === 'table' && type !== 'tableRow') ||
|
|
969
|
+
(parent === 'tableRow' && type !== 'tableCell')
|
|
970
|
+
) {
|
|
971
|
+
return false;
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
const position = node.position as MarkdownAstPosition | undefined;
|
|
976
|
+
const key = positionKey(position);
|
|
977
|
+
if (position != null) {
|
|
978
|
+
const start = position.start?.offset;
|
|
979
|
+
const end = position.end?.offset;
|
|
980
|
+
if (
|
|
981
|
+
!Number.isInteger(start) ||
|
|
982
|
+
!Number.isInteger(end) ||
|
|
983
|
+
(start as number) < 0 ||
|
|
984
|
+
(end as number) < (start as number)
|
|
985
|
+
) {
|
|
986
|
+
return false;
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
if (key != null) {
|
|
990
|
+
const invariants = sourcePositions.get(key) ?? [];
|
|
991
|
+
const invariantIndex = invariants.findIndex(
|
|
992
|
+
invariant =>
|
|
993
|
+
invariant.type === type &&
|
|
994
|
+
invariant.depth === node.depth &&
|
|
995
|
+
invariant.ordered === node.ordered &&
|
|
996
|
+
invariant.start === node.start &&
|
|
997
|
+
invariant.delimiter === node.delimiter &&
|
|
998
|
+
invariant.plugin === node.plugin &&
|
|
999
|
+
invariant.name === node.name &&
|
|
1000
|
+
invariant.display === node.display,
|
|
1001
|
+
);
|
|
1002
|
+
if (invariantIndex < 0) {
|
|
1003
|
+
return false;
|
|
1004
|
+
}
|
|
1005
|
+
invariants.splice(invariantIndex, 1);
|
|
1006
|
+
}
|
|
1007
|
+
if (type !== 'extension' && node.source !== undefined) {
|
|
1008
|
+
return false;
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
switch (type) {
|
|
1012
|
+
case 'root':
|
|
1013
|
+
if (parent != null) {
|
|
1014
|
+
return false;
|
|
1015
|
+
}
|
|
1016
|
+
break;
|
|
1017
|
+
case 'heading': {
|
|
1018
|
+
if (![1, 2, 3, 4, 5, 6].includes(node.depth as number)) {
|
|
1019
|
+
return false;
|
|
1020
|
+
}
|
|
1021
|
+
const marker = (
|
|
1022
|
+
node as unknown as {
|
|
1023
|
+
readonly [sourceHeadingMarker]?: SourceHeadingMarker;
|
|
1024
|
+
}
|
|
1025
|
+
)[sourceHeadingMarker];
|
|
1026
|
+
if (
|
|
1027
|
+
marker != null &&
|
|
1028
|
+
(!sourceHeadingMarkers.has(marker) ||
|
|
1029
|
+
marker.depth !== node.depth ||
|
|
1030
|
+
seenHeadingMarkers.has(marker))
|
|
1031
|
+
) {
|
|
1032
|
+
return false;
|
|
1033
|
+
}
|
|
1034
|
+
if (marker != null) {
|
|
1035
|
+
seenHeadingMarkers.add(marker);
|
|
1036
|
+
}
|
|
1037
|
+
break;
|
|
1038
|
+
}
|
|
1039
|
+
case 'text':
|
|
1040
|
+
case 'inlineCode':
|
|
1041
|
+
case 'inlineMath':
|
|
1042
|
+
case 'math':
|
|
1043
|
+
if (typeof node.value !== 'string') {
|
|
1044
|
+
return false;
|
|
1045
|
+
}
|
|
1046
|
+
break;
|
|
1047
|
+
case 'code':
|
|
1048
|
+
if (
|
|
1049
|
+
typeof node.value !== 'string' ||
|
|
1050
|
+
(node.lang !== null && typeof node.lang !== 'string') ||
|
|
1051
|
+
(node.meta !== undefined && typeof node.meta !== 'string')
|
|
1052
|
+
) {
|
|
1053
|
+
return false;
|
|
1054
|
+
}
|
|
1055
|
+
break;
|
|
1056
|
+
case 'link':
|
|
1057
|
+
if (
|
|
1058
|
+
typeof node.url !== 'string' ||
|
|
1059
|
+
!isSafeMarkdownParserUrl(node.url)
|
|
1060
|
+
) {
|
|
1061
|
+
return false;
|
|
1062
|
+
}
|
|
1063
|
+
break;
|
|
1064
|
+
case 'image':
|
|
1065
|
+
if (
|
|
1066
|
+
typeof node.url !== 'string' ||
|
|
1067
|
+
!isSafeMarkdownParserUrl(node.url) ||
|
|
1068
|
+
typeof node.alt !== 'string'
|
|
1069
|
+
) {
|
|
1070
|
+
return false;
|
|
1071
|
+
}
|
|
1072
|
+
break;
|
|
1073
|
+
case 'citation':
|
|
1074
|
+
if (typeof node.sourceId !== 'string') {
|
|
1075
|
+
return false;
|
|
1076
|
+
}
|
|
1077
|
+
break;
|
|
1078
|
+
case 'list':
|
|
1079
|
+
if (
|
|
1080
|
+
typeof node.ordered !== 'boolean' ||
|
|
1081
|
+
(node.start != null && !Number.isFinite(node.start)) ||
|
|
1082
|
+
(node.delimiter != null &&
|
|
1083
|
+
node.delimiter !== '.' &&
|
|
1084
|
+
node.delimiter !== ')')
|
|
1085
|
+
) {
|
|
1086
|
+
return false;
|
|
1087
|
+
}
|
|
1088
|
+
break;
|
|
1089
|
+
case 'table':
|
|
1090
|
+
if (
|
|
1091
|
+
!Array.isArray(node.align) ||
|
|
1092
|
+
!node.align.every(
|
|
1093
|
+
value =>
|
|
1094
|
+
value === null ||
|
|
1095
|
+
value === 'left' ||
|
|
1096
|
+
value === 'center' ||
|
|
1097
|
+
value === 'right',
|
|
1098
|
+
)
|
|
1099
|
+
) {
|
|
1100
|
+
return false;
|
|
1101
|
+
}
|
|
1102
|
+
break;
|
|
1103
|
+
case 'extension': {
|
|
1104
|
+
if (
|
|
1105
|
+
typeof node.plugin !== 'string' ||
|
|
1106
|
+
!pluginNames.has(node.plugin) ||
|
|
1107
|
+
typeof node.name !== 'string' ||
|
|
1108
|
+
(node.display !== 'inline' && node.display !== 'block') ||
|
|
1109
|
+
!isMarkdownPluginData(node.data) ||
|
|
1110
|
+
!rendererKeys.has(`${node.plugin}\0${node.name}`)
|
|
1111
|
+
) {
|
|
1112
|
+
return false;
|
|
1113
|
+
}
|
|
1114
|
+
const signature = extensionSignature(node);
|
|
1115
|
+
const existing = existingExtensions.get(signature);
|
|
1116
|
+
if (existing != null && existing.count > 0) {
|
|
1117
|
+
existing.count--;
|
|
1118
|
+
} else if (
|
|
1119
|
+
node.plugin !== activePluginName ||
|
|
1120
|
+
node.source !== undefined ||
|
|
1121
|
+
node.position !== undefined
|
|
1122
|
+
) {
|
|
1123
|
+
return false;
|
|
1124
|
+
}
|
|
1125
|
+
break;
|
|
1126
|
+
}
|
|
1127
|
+
case 'strong':
|
|
1128
|
+
case 'emphasis':
|
|
1129
|
+
case 'delete':
|
|
1130
|
+
case 'break':
|
|
1131
|
+
case 'paragraph':
|
|
1132
|
+
case 'blockquote':
|
|
1133
|
+
case 'listItem':
|
|
1134
|
+
case 'tableRow':
|
|
1135
|
+
case 'tableCell':
|
|
1136
|
+
case 'thematicBreak':
|
|
1137
|
+
break;
|
|
1138
|
+
default:
|
|
1139
|
+
return false;
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
if (CHILD_PARENT_TYPES.has(type)) {
|
|
1143
|
+
if (!Array.isArray(node.children)) {
|
|
1144
|
+
return false;
|
|
1145
|
+
}
|
|
1146
|
+
for (const child of node.children) {
|
|
1147
|
+
if (!visit(child, type, insideLink || type === 'link')) {
|
|
1148
|
+
return false;
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
} else if ('children' in node) {
|
|
1152
|
+
return false;
|
|
1153
|
+
}
|
|
1154
|
+
return true;
|
|
1155
|
+
};
|
|
1156
|
+
const candidate = root as MarkdownAstRoot<MarkdownExtensionNode>;
|
|
1157
|
+
return (
|
|
1158
|
+
visit(candidate, null, false) &&
|
|
1159
|
+
candidate.type === 'root' &&
|
|
1160
|
+
Array.from(existingExtensions.values()).every(
|
|
1161
|
+
extension =>
|
|
1162
|
+
extension.plugin === activePluginName || extension.count === 0,
|
|
1163
|
+
) &&
|
|
1164
|
+
(display === 'block' ||
|
|
1165
|
+
(candidate.children.length === 1 &&
|
|
1166
|
+
candidate.children[0]?.type === 'paragraph'))
|
|
1167
|
+
);
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
/**
|
|
1171
|
+
* Deeply freezes a Markdown tree.
|
|
1172
|
+
*
|
|
1173
|
+
* No memo set and no cycle set. Both were measured against this parser's own
|
|
1174
|
+
* fixture and together cost four times the freezing they were guarding: a
|
|
1175
|
+
* full freeze of a 12k-node tree runs at 19 percent of the parse that built
|
|
1176
|
+
* it, a WeakSet memo takes that to 47 percent, and a cycle set to 75. Their
|
|
1177
|
+
* absence is safe, not merely cheap: every tree reaching here is acyclic
|
|
1178
|
+
* (the parser builds no cycles, and `validateAst` rejects any plugin tree
|
|
1179
|
+
* that contains one), and `Object.freeze` on an already-frozen node is a
|
|
1180
|
+
* no-op, so re-walking a shared subtree is correct — just not free.
|
|
1181
|
+
*
|
|
1182
|
+
* Deliberately NOT skipping nodes that look frozen: a node a plugin froze
|
|
1183
|
+
* itself may still hold mutable children, and an undecorated sibling a
|
|
1184
|
+
* trusted helper carried over by identity must be frozen like any other.
|
|
1185
|
+
*/
|
|
1186
|
+
function freezeAstNode(value: unknown): void {
|
|
1187
|
+
if (value == null || typeof value !== 'object') {
|
|
1188
|
+
return;
|
|
1189
|
+
}
|
|
1190
|
+
if (Array.isArray(value)) {
|
|
1191
|
+
for (let index = 0; index < value.length; index++) {
|
|
1192
|
+
freezeAstNode(value[index]);
|
|
1193
|
+
}
|
|
1194
|
+
} else {
|
|
1195
|
+
// `for…in` over a plain AST node rather than Object.values: the entries
|
|
1196
|
+
// array is the dominant remaining cost of the walk.
|
|
1197
|
+
for (const key in value) {
|
|
1198
|
+
freezeAstNode((value as Record<string, unknown>)[key]);
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
Object.freeze(value);
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
function freezeAst<T>(value: T): T {
|
|
1205
|
+
freezeAstNode(value);
|
|
1206
|
+
return value;
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
const reportedProductionPluginFailures = new Set<string>();
|
|
1210
|
+
|
|
1211
|
+
/**
|
|
1212
|
+
* Freezes only what `next` does not share with `previous`.
|
|
1213
|
+
*
|
|
1214
|
+
* Valid ONLY when no plugin-authored transform runs in this pipeline. The
|
|
1215
|
+
* nodes it skips are the ones a Core helper carried over untouched, and with
|
|
1216
|
+
* every transform Core-authored there is no actor that could mutate them:
|
|
1217
|
+
* they are left in exactly the state an omitted or empty plugin list leaves
|
|
1218
|
+
* them in, which is the state rendering already observes for every
|
|
1219
|
+
* zero-plugin document today. The moment one untrusted transform exists,
|
|
1220
|
+
* `applyMarkdownTransforms` uses the full `freezeAst` instead, before that
|
|
1221
|
+
* transform observes anything — a shared, undecorated sibling included.
|
|
1222
|
+
*/
|
|
1223
|
+
function freezeBuiltAst<T>(previous: unknown, next: T): T {
|
|
1224
|
+
if (next == null || typeof next !== 'object' || next === previous) {
|
|
1225
|
+
return next;
|
|
1226
|
+
}
|
|
1227
|
+
const before =
|
|
1228
|
+
previous != null && typeof previous === 'object'
|
|
1229
|
+
? (previous as Record<string, unknown>)
|
|
1230
|
+
: undefined;
|
|
1231
|
+
if (Array.isArray(next)) {
|
|
1232
|
+
const beforeArray = Array.isArray(previous) ? previous : undefined;
|
|
1233
|
+
for (let index = 0; index < next.length; index++) {
|
|
1234
|
+
freezeBuiltAst(beforeArray?.[index], next[index]);
|
|
1235
|
+
}
|
|
1236
|
+
} else {
|
|
1237
|
+
for (const key in next) {
|
|
1238
|
+
freezeBuiltAst(before?.[key], (next as Record<string, unknown>)[key]);
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1241
|
+
Object.freeze(next);
|
|
1242
|
+
return next;
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
const HELPER_PHRASING_TYPES = new Set([
|
|
1246
|
+
'text',
|
|
1247
|
+
'strong',
|
|
1248
|
+
'emphasis',
|
|
1249
|
+
'delete',
|
|
1250
|
+
'inlineCode',
|
|
1251
|
+
'inlineMath',
|
|
1252
|
+
'break',
|
|
1253
|
+
'link',
|
|
1254
|
+
'image',
|
|
1255
|
+
'citation',
|
|
1256
|
+
'extension',
|
|
1257
|
+
]);
|
|
1258
|
+
|
|
1259
|
+
/**
|
|
1260
|
+
* @internal Validates and freezes phrasing nodes a Core helper's caller just
|
|
1261
|
+
* authored, so the helper may put them straight into its output tree.
|
|
1262
|
+
* applying exactly the rules `validateAst` applies to newly-introduced nodes:
|
|
1263
|
+
* representable typed data only, no authored provenance, no foreign extension
|
|
1264
|
+
* ownership, no unrenderable extension, revalidated destinations, no nested
|
|
1265
|
+
* link, no block content, no cycles, and a bounded node count.
|
|
1266
|
+
*
|
|
1267
|
+
* A helper that validates and freezes its callback's output this way has
|
|
1268
|
+
* nothing left for Core to check, because every other node it returns is one
|
|
1269
|
+
* Core already validated. That is what lets such a helper run on the trusted
|
|
1270
|
+
* path while keeping FR10 and FR11 intact.
|
|
1271
|
+
*
|
|
1272
|
+
* Returns the frozen nodes, or throws — the caller reports the failure
|
|
1273
|
+
* through the ordinary transform failure path.
|
|
1274
|
+
*/
|
|
1275
|
+
export function adoptMarkdownHelperNode(
|
|
1276
|
+
node: unknown,
|
|
1277
|
+
ownership: MarkdownHelperOwnership,
|
|
1278
|
+
insideLink = false,
|
|
1279
|
+
): void {
|
|
1280
|
+
adoptMarkdownHelperNodes(singleNodeScratch(node), ownership, insideLink);
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
/**
|
|
1284
|
+
* A one-element view for the single-node case, reused between calls so the
|
|
1285
|
+
* common replacement costs no array. Refilled on entry and never held past
|
|
1286
|
+
* the synchronous validation below, and a callback that re-enters the
|
|
1287
|
+
* helper gets a fresh array rather than disturbing an outer walk.
|
|
1288
|
+
*/
|
|
1289
|
+
let scratchInUse = false;
|
|
1290
|
+
const reusableScratch: unknown[] = [undefined];
|
|
1291
|
+
function singleNodeScratch(node: unknown): unknown[] {
|
|
1292
|
+
if (scratchInUse) {
|
|
1293
|
+
return [node];
|
|
1294
|
+
}
|
|
1295
|
+
reusableScratch[0] = node;
|
|
1296
|
+
return reusableScratch;
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
export function adoptMarkdownHelperNodes(
|
|
1300
|
+
nodes: ReadonlyArray<unknown>,
|
|
1301
|
+
ownership: MarkdownHelperOwnership,
|
|
1302
|
+
insideLink = false,
|
|
1303
|
+
): void {
|
|
1304
|
+
// The overwhelmingly common replacement is one flat text node. Checking
|
|
1305
|
+
// that shape directly avoids the general walk's setup entirely, and this
|
|
1306
|
+
// runs once per match.
|
|
1307
|
+
if (nodes.length === 1) {
|
|
1308
|
+
const only = nodes[0];
|
|
1309
|
+
if (
|
|
1310
|
+
only != null &&
|
|
1311
|
+
typeof only === 'object' &&
|
|
1312
|
+
(only as {type?: unknown}).type === 'text' &&
|
|
1313
|
+
typeof (only as {value?: unknown}).value === 'string'
|
|
1314
|
+
) {
|
|
1315
|
+
let extra = false;
|
|
1316
|
+
for (const key in only) {
|
|
1317
|
+
if (key !== 'type' && key !== 'value') {
|
|
1318
|
+
extra = true;
|
|
1319
|
+
break;
|
|
1320
|
+
}
|
|
1321
|
+
}
|
|
1322
|
+
if (!extra) {
|
|
1323
|
+
return;
|
|
1324
|
+
}
|
|
1325
|
+
}
|
|
1326
|
+
}
|
|
1327
|
+
// Cycle tracking is allocated only once a node with children appears: the
|
|
1328
|
+
// common replacement is a flat node, and this runs once per match.
|
|
1329
|
+
let seen: Set<object> | undefined;
|
|
1330
|
+
let count = 0;
|
|
1331
|
+
const visit = (value: unknown, withinLink: boolean): boolean => {
|
|
1332
|
+
if (value == null || typeof value !== 'object' || seen?.has(value)) {
|
|
1333
|
+
return false;
|
|
1334
|
+
}
|
|
1335
|
+
if (++count > 10_000) {
|
|
1336
|
+
return false;
|
|
1337
|
+
}
|
|
1338
|
+
if (Array.isArray((value as {children?: unknown}).children)) {
|
|
1339
|
+
seen ??= new Set<object>();
|
|
1340
|
+
seen.add(value);
|
|
1341
|
+
}
|
|
1342
|
+
const node = value as Record<string, unknown>;
|
|
1343
|
+
const type = node.type;
|
|
1344
|
+
if (typeof type !== 'string' || !HELPER_PHRASING_TYPES.has(type)) {
|
|
1345
|
+
return false;
|
|
1346
|
+
}
|
|
1347
|
+
// A helper's callback authors synthetic nodes only: provenance stays
|
|
1348
|
+
// Core's to assign, so neither field may be present.
|
|
1349
|
+
if (node.position !== undefined) {
|
|
1350
|
+
return false;
|
|
1351
|
+
}
|
|
1352
|
+
if (type !== 'extension' && node.source !== undefined) {
|
|
1353
|
+
return false;
|
|
1354
|
+
}
|
|
1355
|
+
if (node.data !== undefined && !isMarkdownPluginData(node.data)) {
|
|
1356
|
+
return false;
|
|
1357
|
+
}
|
|
1358
|
+
switch (type) {
|
|
1359
|
+
case 'text':
|
|
1360
|
+
case 'inlineCode':
|
|
1361
|
+
case 'inlineMath':
|
|
1362
|
+
if (typeof node.value !== 'string') {
|
|
1363
|
+
return false;
|
|
1364
|
+
}
|
|
1365
|
+
break;
|
|
1366
|
+
case 'link':
|
|
1367
|
+
if (
|
|
1368
|
+
withinLink ||
|
|
1369
|
+
typeof node.url !== 'string' ||
|
|
1370
|
+
!isSafeMarkdownParserUrl(node.url)
|
|
1371
|
+
) {
|
|
1372
|
+
return false;
|
|
1373
|
+
}
|
|
1374
|
+
break;
|
|
1375
|
+
case 'image':
|
|
1376
|
+
if (
|
|
1377
|
+
typeof node.url !== 'string' ||
|
|
1378
|
+
!isSafeMarkdownParserUrl(node.url) ||
|
|
1379
|
+
typeof node.alt !== 'string'
|
|
1380
|
+
) {
|
|
1381
|
+
return false;
|
|
1382
|
+
}
|
|
1383
|
+
break;
|
|
1384
|
+
case 'citation':
|
|
1385
|
+
if (typeof node.sourceId !== 'string') {
|
|
1386
|
+
return false;
|
|
1387
|
+
}
|
|
1388
|
+
break;
|
|
1389
|
+
case 'extension':
|
|
1390
|
+
if (
|
|
1391
|
+
node.plugin !== ownership.pluginName ||
|
|
1392
|
+
typeof node.name !== 'string' ||
|
|
1393
|
+
!ownership.hasRenderer(node.name) ||
|
|
1394
|
+
node.display !== 'inline' ||
|
|
1395
|
+
node.source !== undefined ||
|
|
1396
|
+
!isMarkdownPluginData(node.data)
|
|
1397
|
+
) {
|
|
1398
|
+
return false;
|
|
1399
|
+
}
|
|
1400
|
+
break;
|
|
1401
|
+
case 'strong':
|
|
1402
|
+
case 'emphasis':
|
|
1403
|
+
case 'delete':
|
|
1404
|
+
case 'break':
|
|
1405
|
+
break;
|
|
1406
|
+
}
|
|
1407
|
+
if (type === 'strong' || type === 'emphasis' || type === 'delete') {
|
|
1408
|
+
if (!Array.isArray(node.children)) {
|
|
1409
|
+
return false;
|
|
1410
|
+
}
|
|
1411
|
+
for (const child of node.children) {
|
|
1412
|
+
if (!visit(child, withinLink)) {
|
|
1413
|
+
return false;
|
|
1414
|
+
}
|
|
1415
|
+
}
|
|
1416
|
+
} else if (type === 'link') {
|
|
1417
|
+
if (!Array.isArray(node.children)) {
|
|
1418
|
+
return false;
|
|
1419
|
+
}
|
|
1420
|
+
for (const child of node.children) {
|
|
1421
|
+
if (!visit(child, true)) {
|
|
1422
|
+
return false;
|
|
1423
|
+
}
|
|
1424
|
+
}
|
|
1425
|
+
} else if ('children' in node) {
|
|
1426
|
+
return false;
|
|
1427
|
+
}
|
|
1428
|
+
return true;
|
|
1429
|
+
};
|
|
1430
|
+
for (const node of nodes) {
|
|
1431
|
+
if (!visit(node, insideLink)) {
|
|
1432
|
+
throw new TypeError(
|
|
1433
|
+
'Markdown helper callback returned an unrepresentable node',
|
|
1434
|
+
);
|
|
1435
|
+
}
|
|
1436
|
+
}
|
|
1437
|
+
// Frozen only once every node passed, so a rejected batch leaves the
|
|
1438
|
+
// caller's objects exactly as they were. Freezing is what makes adopting
|
|
1439
|
+
// a caller's object safe instead of copying it: the object is now part of
|
|
1440
|
+
// an immutable tree, and a later write by whoever still holds a reference
|
|
1441
|
+
// cannot reach into the document.
|
|
1442
|
+
const reentrant = nodes !== reusableScratch;
|
|
1443
|
+
if (!reentrant) {
|
|
1444
|
+
scratchInUse = true;
|
|
1445
|
+
}
|
|
1446
|
+
try {
|
|
1447
|
+
for (const node of nodes) {
|
|
1448
|
+
freezeAstNode(node);
|
|
1449
|
+
}
|
|
1450
|
+
} finally {
|
|
1451
|
+
if (!reentrant) {
|
|
1452
|
+
scratchInUse = false;
|
|
1453
|
+
reusableScratch[0] = undefined;
|
|
1454
|
+
}
|
|
1455
|
+
}
|
|
1456
|
+
}
|
|
1457
|
+
|
|
1458
|
+
export function reportMarkdownPluginFailure(
|
|
1459
|
+
pluginName: string,
|
|
1460
|
+
phase: 'syntax' | 'transform' | 'render',
|
|
1461
|
+
error: unknown,
|
|
1462
|
+
): void {
|
|
1463
|
+
const key = `markdown-plugin:${pluginName}:${phase}`;
|
|
1464
|
+
const message = `plugin "${pluginName}" failed in ${phase}; rendered readable fallback.`;
|
|
1465
|
+
if (process.env.NODE_ENV === 'production') {
|
|
1466
|
+
if (!reportedProductionPluginFailures.has(key)) {
|
|
1467
|
+
reportedProductionPluginFailures.add(key);
|
|
1468
|
+
devError('Markdown', message);
|
|
1469
|
+
}
|
|
1470
|
+
return;
|
|
1471
|
+
}
|
|
1472
|
+
warnOnce(key, 'Markdown', message, error);
|
|
1473
|
+
}
|
|
1474
|
+
|
|
1475
|
+
export function applyMarkdownTransforms<Node extends MarkdownExtensionNode>(
|
|
1476
|
+
root: MarkdownAstRoot<Node>,
|
|
1477
|
+
plugins: PreparedMarkdownPlugins | undefined,
|
|
1478
|
+
source: string,
|
|
1479
|
+
isFinal: boolean,
|
|
1480
|
+
display: 'inline' | 'block',
|
|
1481
|
+
): MarkdownAstRoot<Node> {
|
|
1482
|
+
if (plugins == null || plugins.transforms.length === 0) {
|
|
1483
|
+
return root;
|
|
1484
|
+
}
|
|
1485
|
+
let pluginNames: Set<string> | undefined;
|
|
1486
|
+
let rendererKeys: Set<string> | undefined;
|
|
1487
|
+
let document: MarkdownAstRoot<Node> = root;
|
|
1488
|
+
// Heading identity is stamped on the mutable parser tree, before anything
|
|
1489
|
+
// freezes it, and only when a plugin-authored transform will actually be
|
|
1490
|
+
// validated against it. An all-trusted pipeline never pays for it.
|
|
1491
|
+
const sourceHeadingMarkers = plugins.hasUntrustedTransform
|
|
1492
|
+
? markSourceHeadings(root)
|
|
1493
|
+
: undefined;
|
|
1494
|
+
let guarded = false;
|
|
1495
|
+
let priorTransformChangedTree = false;
|
|
1496
|
+
/** A trusted helper has produced a tree nothing has frozen yet. */
|
|
1497
|
+
let deferredFreeze = false;
|
|
1498
|
+
for (const prepared of plugins.transforms) {
|
|
1499
|
+
if (!priorTransformChangedTree && prepared.claims?.(source) === false) {
|
|
1500
|
+
continue;
|
|
1501
|
+
}
|
|
1502
|
+
// A plugin-authored transform observes deeply frozen input and has its
|
|
1503
|
+
// output validated. Both guards exist to contain untrusted structure, so
|
|
1504
|
+
// a Core-authored trusted helper needs neither — and a pipeline of only
|
|
1505
|
+
// trusted helpers never pays for them at all. Every untrusted transform
|
|
1506
|
+
// still receives a frozen tree, including one that follows a trusted
|
|
1507
|
+
// helper, whose output is not frozen on the way out.
|
|
1508
|
+
if (!prepared.trusted) {
|
|
1509
|
+
if (!guarded) {
|
|
1510
|
+
pluginNames = new Set(plugins.entries.map(entry => entry.name));
|
|
1511
|
+
rendererKeys = new Set(plugins.renderers.keys());
|
|
1512
|
+
guarded = true;
|
|
1513
|
+
}
|
|
1514
|
+
// Deeply freezes the whole tree, including anything a trusted helper
|
|
1515
|
+
// left shared and mutable, before plugin-authored code observes it.
|
|
1516
|
+
// Subtrees already frozen by an earlier pass are skipped.
|
|
1517
|
+
document = freezeAst(document);
|
|
1518
|
+
deferredFreeze = false;
|
|
1519
|
+
}
|
|
1520
|
+
try {
|
|
1521
|
+
const context: InternalMarkdownTransformContext = {
|
|
1522
|
+
source,
|
|
1523
|
+
isFinal,
|
|
1524
|
+
display,
|
|
1525
|
+
report(message) {
|
|
1526
|
+
reportMarkdownPluginFailure(
|
|
1527
|
+
prepared.pluginName,
|
|
1528
|
+
'transform',
|
|
1529
|
+
message,
|
|
1530
|
+
);
|
|
1531
|
+
},
|
|
1532
|
+
[markdownTransformPluginName]: prepared.pluginName,
|
|
1533
|
+
[markdownTransformHasRenderer]: (nodeName: string) =>
|
|
1534
|
+
plugins.renderers.has(`${prepared.pluginName}\u0000${nodeName}`),
|
|
1535
|
+
[markdownTransformSourceUnchanged]: !priorTransformChangedTree,
|
|
1536
|
+
};
|
|
1537
|
+
const next = prepared.transform(document, context) as unknown;
|
|
1538
|
+
if (
|
|
1539
|
+
next != null &&
|
|
1540
|
+
typeof next === 'object' &&
|
|
1541
|
+
typeof (next as {then?: unknown}).then === 'function'
|
|
1542
|
+
) {
|
|
1543
|
+
void Promise.resolve(next).catch(() => {});
|
|
1544
|
+
throw new TypeError('Async Markdown transforms are not supported');
|
|
1545
|
+
}
|
|
1546
|
+
if (next === document) {
|
|
1547
|
+
continue;
|
|
1548
|
+
}
|
|
1549
|
+
if (prepared.trusted) {
|
|
1550
|
+
priorTransformChangedTree = true;
|
|
1551
|
+
// No freeze here. Nothing untrusted can observe this tree yet —
|
|
1552
|
+
// only another Core helper, which never mutates its input — so the
|
|
1553
|
+
// pipeline freezes once, below, before anything else sees it. That
|
|
1554
|
+
// freeze is a FULL walk, never one that stops at nodes merely
|
|
1555
|
+
// shared with an earlier unfrozen tree: an undecorated sibling a
|
|
1556
|
+
// helper carried over by identity must be frozen too.
|
|
1557
|
+
deferredFreeze = true;
|
|
1558
|
+
document = next as MarkdownAstRoot<Node>;
|
|
1559
|
+
continue;
|
|
1560
|
+
}
|
|
1561
|
+
const positions = collectSourceInvariants(document);
|
|
1562
|
+
const existingExtensions = collectExtensionSignatures(document);
|
|
1563
|
+
if (
|
|
1564
|
+
!validateAst(
|
|
1565
|
+
next,
|
|
1566
|
+
pluginNames as ReadonlySet<string>,
|
|
1567
|
+
rendererKeys as ReadonlySet<string>,
|
|
1568
|
+
positions,
|
|
1569
|
+
sourceHeadingMarkers as ReadonlySet<SourceHeadingMarker>,
|
|
1570
|
+
prepared.pluginName,
|
|
1571
|
+
existingExtensions,
|
|
1572
|
+
display,
|
|
1573
|
+
)
|
|
1574
|
+
) {
|
|
1575
|
+
throw new TypeError('Transform returned an invalid Markdown document');
|
|
1576
|
+
}
|
|
1577
|
+
priorTransformChangedTree = true;
|
|
1578
|
+
document = freezeAst(
|
|
1579
|
+
restoreCodeInternalProperties(document, next),
|
|
1580
|
+
) as MarkdownAstRoot<Node>;
|
|
1581
|
+
} catch (error) {
|
|
1582
|
+
reportMarkdownPluginFailure(prepared.pluginName, 'transform', error);
|
|
1583
|
+
}
|
|
1584
|
+
}
|
|
1585
|
+
if (!deferredFreeze) {
|
|
1586
|
+
return document;
|
|
1587
|
+
}
|
|
1588
|
+
// Freeze what the helpers built. With a plugin-authored transform in the
|
|
1589
|
+
// list the whole tree is frozen, because that plugin may observe any part
|
|
1590
|
+
// of it; with only Core helpers, the untouched remainder is left exactly
|
|
1591
|
+
// as a zero-plugin parse leaves it.
|
|
1592
|
+
return plugins.hasUntrustedTransform
|
|
1593
|
+
? freezeAst(document)
|
|
1594
|
+
: freezeBuiltAst(root, document);
|
|
1595
|
+
}
|
|
1596
|
+
|
|
1597
|
+
export function getMarkdownExtensionRenderer(
|
|
1598
|
+
plugins: PreparedMarkdownPlugins | undefined,
|
|
1599
|
+
node: MarkdownExtensionNode,
|
|
1600
|
+
): MarkdownExtensionRenderer<MarkdownExtensionNode> | undefined {
|
|
1601
|
+
return plugins?.renderers.get(`${node.plugin}\0${node.name}`)?.renderer;
|
|
1602
|
+
}
|
|
1603
|
+
|
|
1604
|
+
export function markdownExtensionText(
|
|
1605
|
+
plugins: PreparedMarkdownPlugins | undefined,
|
|
1606
|
+
node: MarkdownExtensionNode,
|
|
1607
|
+
): string {
|
|
1608
|
+
const renderer = getMarkdownExtensionRenderer(plugins, node);
|
|
1609
|
+
if (renderer == null) {
|
|
1610
|
+
return node.source ?? '';
|
|
1611
|
+
}
|
|
1612
|
+
try {
|
|
1613
|
+
return renderer.toText(node);
|
|
1614
|
+
} catch (error) {
|
|
1615
|
+
reportMarkdownPluginFailure(node.plugin, 'render', error);
|
|
1616
|
+
return node.source ?? '';
|
|
1617
|
+
}
|
|
1618
|
+
}
|