@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,799 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file sourceDecoration.test.tsx
|
|
5
|
+
* @input Validated source ranges, overlapping claims, and decorated documents
|
|
6
|
+
* @output Behavioral, determinism, invariance, streaming, and ownership coverage
|
|
7
|
+
* @position Acceptance tests for the optional Markdown source-decoration helper
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import {renderToString} from 'react-dom/server';
|
|
11
|
+
import {describe, expect, expectTypeOf, it, vi} from 'vitest';
|
|
12
|
+
import {Markdown} from '../Markdown';
|
|
13
|
+
import {
|
|
14
|
+
createIncrementalState,
|
|
15
|
+
parseMarkdown,
|
|
16
|
+
parseMarkdownAst,
|
|
17
|
+
parseMarkdownAstIncremental,
|
|
18
|
+
parseMarkdownIncremental,
|
|
19
|
+
trimStreamingArtifacts,
|
|
20
|
+
} from '../parser';
|
|
21
|
+
import {createMarkdownPlugin} from './protocol';
|
|
22
|
+
import type {
|
|
23
|
+
MarkdownExtensionNode,
|
|
24
|
+
MarkdownSyntaxPluginDefinition,
|
|
25
|
+
MarkdownTransform,
|
|
26
|
+
} from './protocol';
|
|
27
|
+
import {parseOutlineFromMarkdown} from '../../Outline/parseOutlineFromMarkdown';
|
|
28
|
+
import {
|
|
29
|
+
createMarkdownSourceDecoration,
|
|
30
|
+
getMarkdownSourceDecorations,
|
|
31
|
+
} from './sourceDecoration';
|
|
32
|
+
import type {MarkdownSourceDecoration} from './sourceDecoration';
|
|
33
|
+
|
|
34
|
+
const SOURCE = [
|
|
35
|
+
'# Release notes',
|
|
36
|
+
'',
|
|
37
|
+
'Ada shipped the parser.',
|
|
38
|
+
'',
|
|
39
|
+
'> Quoted detail',
|
|
40
|
+
].join('\n');
|
|
41
|
+
|
|
42
|
+
type Seen = Map<string, ReadonlyArray<MarkdownSourceDecoration>>;
|
|
43
|
+
|
|
44
|
+
function spanOf(text: string, source: string = SOURCE) {
|
|
45
|
+
const start = source.indexOf(text);
|
|
46
|
+
expect(start).toBeGreaterThanOrEqual(0);
|
|
47
|
+
return {start, end: start + text.length};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Reads decorations back through the public protocol: a transform that runs
|
|
52
|
+
* after the decorating one observes exactly what later plugins and rendering
|
|
53
|
+
* observe.
|
|
54
|
+
*/
|
|
55
|
+
function observerPlugin(name: string, seen: Seen) {
|
|
56
|
+
return createMarkdownPlugin({
|
|
57
|
+
name,
|
|
58
|
+
apiVersion: 1,
|
|
59
|
+
transform(root) {
|
|
60
|
+
seen.clear();
|
|
61
|
+
root.children.forEach((block, index) => {
|
|
62
|
+
seen.set(`${index}:${block.type}`, getMarkdownSourceDecorations(block));
|
|
63
|
+
});
|
|
64
|
+
return root;
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function decorationPlugin(
|
|
70
|
+
name: string,
|
|
71
|
+
transform: MarkdownTransform<never>,
|
|
72
|
+
): ReturnType<typeof createMarkdownPlugin> {
|
|
73
|
+
return createMarkdownPlugin({name, apiVersion: 1, transform});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function decorationsFor(
|
|
77
|
+
source: string,
|
|
78
|
+
ranges: ReadonlyArray<{start: number; end: number}>,
|
|
79
|
+
name = 'hit',
|
|
80
|
+
): Seen {
|
|
81
|
+
const seen: Seen = new Map();
|
|
82
|
+
parseMarkdown(source, {
|
|
83
|
+
plugins: [
|
|
84
|
+
decorationPlugin(
|
|
85
|
+
`${name}-decoration`,
|
|
86
|
+
createMarkdownSourceDecoration({name, ranges}),
|
|
87
|
+
),
|
|
88
|
+
observerPlugin(`${name}-observer`, seen),
|
|
89
|
+
],
|
|
90
|
+
});
|
|
91
|
+
return seen;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
describe('createMarkdownSourceDecoration', () => {
|
|
95
|
+
it('decorates positioned blocks without the caller opting into sourceRanges', () => {
|
|
96
|
+
const paragraph = spanOf('Ada shipped the parser.');
|
|
97
|
+
const seen: Seen = new Map();
|
|
98
|
+
parseMarkdown(SOURCE, {
|
|
99
|
+
plugins: [
|
|
100
|
+
decorationPlugin(
|
|
101
|
+
'covers-paragraph',
|
|
102
|
+
createMarkdownSourceDecoration({
|
|
103
|
+
name: 'review-hit',
|
|
104
|
+
ranges: [{...paragraph, data: {reviewer: 'ada'}}],
|
|
105
|
+
}),
|
|
106
|
+
),
|
|
107
|
+
observerPlugin('covers-paragraph-observer', seen),
|
|
108
|
+
],
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
expect(seen.get('1:paragraph')).toEqual([
|
|
112
|
+
{name: 'review-hit', data: {reviewer: 'ada'}},
|
|
113
|
+
]);
|
|
114
|
+
expect(seen.get('0:heading')).toEqual([]);
|
|
115
|
+
expect(seen.get('2:blockquote')).toEqual([]);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('decorates every block a range touches, including partial overlap', () => {
|
|
119
|
+
const heading = spanOf('# Release notes');
|
|
120
|
+
const paragraph = spanOf('Ada shipped the parser.');
|
|
121
|
+
|
|
122
|
+
// One range covering two whole blocks.
|
|
123
|
+
const spanning = decorationsFor(SOURCE, [
|
|
124
|
+
{start: heading.start, end: paragraph.end},
|
|
125
|
+
]);
|
|
126
|
+
expect(spanning.get('0:heading')).toEqual([{name: 'hit'}]);
|
|
127
|
+
expect(spanning.get('1:paragraph')).toEqual([{name: 'hit'}]);
|
|
128
|
+
expect(spanning.get('2:blockquote')).toEqual([]);
|
|
129
|
+
|
|
130
|
+
// A range strictly inside one block still decorates that block: a search
|
|
131
|
+
// hit is a sub-span of the prose it was found in.
|
|
132
|
+
const inside = decorationsFor(SOURCE, [spanOf('shipped')]);
|
|
133
|
+
expect(inside.get('1:paragraph')).toEqual([{name: 'hit'}]);
|
|
134
|
+
expect(inside.get('0:heading')).toEqual([]);
|
|
135
|
+
|
|
136
|
+
// A range straddling a boundary touches both blocks.
|
|
137
|
+
const straddling = decorationsFor(SOURCE, [
|
|
138
|
+
{start: paragraph.end - 4, end: spanOf('> Quoted detail').start + 3},
|
|
139
|
+
]);
|
|
140
|
+
expect(straddling.get('1:paragraph')).toEqual([{name: 'hit'}]);
|
|
141
|
+
expect(straddling.get('2:blockquote')).toEqual([{name: 'hit'}]);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('is observable through the rendered component without changing its output', () => {
|
|
145
|
+
const paragraph = spanOf('Ada shipped the parser.');
|
|
146
|
+
const seen: Seen = new Map();
|
|
147
|
+
const plugins = [
|
|
148
|
+
decorationPlugin(
|
|
149
|
+
'component-decoration',
|
|
150
|
+
createMarkdownSourceDecoration({
|
|
151
|
+
name: 'search-hit',
|
|
152
|
+
ranges: [paragraph],
|
|
153
|
+
}),
|
|
154
|
+
),
|
|
155
|
+
observerPlugin('component-observer', seen),
|
|
156
|
+
];
|
|
157
|
+
|
|
158
|
+
const decorated = renderToString(
|
|
159
|
+
<Markdown plugins={plugins}>{SOURCE}</Markdown>,
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
expect(seen.get('1:paragraph')).toEqual([{name: 'search-hit'}]);
|
|
163
|
+
expect(decorated).toBe(renderToString(<Markdown>{SOURCE}</Markdown>));
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('is observable through Outline without changing heading identity', () => {
|
|
167
|
+
const seen: Seen = new Map();
|
|
168
|
+
const plugins = [
|
|
169
|
+
decorationPlugin(
|
|
170
|
+
'outline-decoration',
|
|
171
|
+
createMarkdownSourceDecoration({
|
|
172
|
+
name: 'search-hit',
|
|
173
|
+
ranges: [spanOf('# Release notes')],
|
|
174
|
+
}),
|
|
175
|
+
),
|
|
176
|
+
observerPlugin('outline-observer', seen),
|
|
177
|
+
];
|
|
178
|
+
|
|
179
|
+
expect(parseOutlineFromMarkdown(SOURCE, {plugins})).toEqual(
|
|
180
|
+
parseOutlineFromMarkdown(SOURCE),
|
|
181
|
+
);
|
|
182
|
+
expect(seen.get('0:heading')).toEqual([{name: 'search-hit'}]);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it('exposes decorations only on the settled snapshot, never present then absent', () => {
|
|
186
|
+
// An ordinary table: while it streams, the covering block is re-parsed
|
|
187
|
+
// from paragraph to table and its extent changes under the range.
|
|
188
|
+
const source = [
|
|
189
|
+
'# Title',
|
|
190
|
+
'',
|
|
191
|
+
'Intro prose here.',
|
|
192
|
+
'',
|
|
193
|
+
'| Item | Detail |',
|
|
194
|
+
'| --- | --- |',
|
|
195
|
+
'| 1 | two |',
|
|
196
|
+
'',
|
|
197
|
+
'Tail prose.',
|
|
198
|
+
].join('\n');
|
|
199
|
+
|
|
200
|
+
for (const target of [
|
|
201
|
+
'Intro prose here.',
|
|
202
|
+
'| Item | Detail |',
|
|
203
|
+
'| 1 | two |',
|
|
204
|
+
'Tail prose.',
|
|
205
|
+
]) {
|
|
206
|
+
const start = source.indexOf(target);
|
|
207
|
+
const range = {start, end: start + target.length};
|
|
208
|
+
const seen: Seen = new Map();
|
|
209
|
+
const plugins = [
|
|
210
|
+
decorationPlugin(
|
|
211
|
+
'streaming-decoration',
|
|
212
|
+
createMarkdownSourceDecoration({name: 'hit', ranges: [range]}),
|
|
213
|
+
),
|
|
214
|
+
observerPlugin('streaming-observer', seen),
|
|
215
|
+
];
|
|
216
|
+
|
|
217
|
+
const state = createIncrementalState();
|
|
218
|
+
const presence: boolean[] = [];
|
|
219
|
+
for (let length = 1; length <= source.length; length++) {
|
|
220
|
+
const chunk = source.slice(0, length);
|
|
221
|
+
const isFinal = length === source.length;
|
|
222
|
+
parseMarkdownIncremental(
|
|
223
|
+
isFinal ? chunk : trimStreamingArtifacts(chunk, {}),
|
|
224
|
+
state,
|
|
225
|
+
{plugins, isFinal},
|
|
226
|
+
);
|
|
227
|
+
presence.push([...seen.values()].some(entries => entries.length > 0));
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Monotone by construction: absent while the source can still change,
|
|
231
|
+
// present once it cannot. No present -> absent transition exists.
|
|
232
|
+
expect(presence.slice(0, -1).every(shown => !shown)).toBe(true);
|
|
233
|
+
expect(presence[presence.length - 1]).toBe(true);
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it('decorates blocks an earlier transform reordered', () => {
|
|
238
|
+
const heading = spanOf('# Release notes');
|
|
239
|
+
const quote = spanOf('> Quoted detail');
|
|
240
|
+
// A transform that reverses the document puts spans out of source order,
|
|
241
|
+
// so a sweep that assumed document order would miss or mis-assign them.
|
|
242
|
+
const reverse = createMarkdownPlugin({
|
|
243
|
+
name: 'reverse-blocks',
|
|
244
|
+
apiVersion: 1,
|
|
245
|
+
transform: root => ({...root, children: [...root.children].reverse()}),
|
|
246
|
+
});
|
|
247
|
+
const seen: Seen = new Map();
|
|
248
|
+
parseMarkdown(SOURCE, {
|
|
249
|
+
plugins: [
|
|
250
|
+
reverse,
|
|
251
|
+
decorationPlugin(
|
|
252
|
+
'reordered',
|
|
253
|
+
createMarkdownSourceDecoration({
|
|
254
|
+
name: 'hit',
|
|
255
|
+
ranges: [heading, {...quote, data: {tail: true}}],
|
|
256
|
+
}),
|
|
257
|
+
),
|
|
258
|
+
observerPlugin('reordered-observer', seen),
|
|
259
|
+
],
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
// Reversed order: blockquote, paragraph, heading.
|
|
263
|
+
expect(seen.get('0:blockquote')).toEqual([
|
|
264
|
+
{name: 'hit', data: {tail: true}},
|
|
265
|
+
]);
|
|
266
|
+
expect(seen.get('1:paragraph')).toEqual([]);
|
|
267
|
+
expect(seen.get('2:heading')).toEqual([{name: 'hit'}]);
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
it('resolves overlapping, duplicate, and unordered ranges deterministically', () => {
|
|
271
|
+
const paragraph = spanOf('Ada shipped the parser.');
|
|
272
|
+
const wide = {start: 0, end: paragraph.end};
|
|
273
|
+
const run = () => {
|
|
274
|
+
const seen: Seen = new Map();
|
|
275
|
+
parseMarkdown(SOURCE, {
|
|
276
|
+
plugins: [
|
|
277
|
+
decorationPlugin(
|
|
278
|
+
'overlaps',
|
|
279
|
+
createMarkdownSourceDecoration({
|
|
280
|
+
name: 'hit',
|
|
281
|
+
ranges: [
|
|
282
|
+
{...paragraph, data: {rank: 2}},
|
|
283
|
+
wide,
|
|
284
|
+
{...paragraph, data: {rank: 1}},
|
|
285
|
+
// Exact duplicate of the first range.
|
|
286
|
+
{...paragraph, data: {rank: 2}},
|
|
287
|
+
paragraph,
|
|
288
|
+
],
|
|
289
|
+
}),
|
|
290
|
+
),
|
|
291
|
+
observerPlugin('overlaps-observer', seen),
|
|
292
|
+
],
|
|
293
|
+
});
|
|
294
|
+
return seen.get('1:paragraph');
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
// Sorted by start, then end, then payload; the repeated rank-2 range
|
|
298
|
+
// collapses into one entry.
|
|
299
|
+
expect(run()).toEqual([
|
|
300
|
+
// The wide range starts first.
|
|
301
|
+
{name: 'hit'},
|
|
302
|
+
// Then the three paragraph-aligned ranges, payload-free first.
|
|
303
|
+
{name: 'hit'},
|
|
304
|
+
{name: 'hit', data: {rank: 1}},
|
|
305
|
+
{name: 'hit', data: {rank: 2}},
|
|
306
|
+
]);
|
|
307
|
+
expect(run()).toEqual(run());
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
it('records each range once per block however many blocks it sweeps', () => {
|
|
311
|
+
const sections = 200;
|
|
312
|
+
const source = Array.from(
|
|
313
|
+
{length: sections},
|
|
314
|
+
(_, index) => `Section ${index} prose.`,
|
|
315
|
+
).join('\n\n');
|
|
316
|
+
// One range per block plus one range spanning the whole document.
|
|
317
|
+
const ranges = [
|
|
318
|
+
{start: 0, end: source.length},
|
|
319
|
+
...Array.from({length: sections}, (_, index) =>
|
|
320
|
+
spanOf(`Section ${index} prose.`, source),
|
|
321
|
+
),
|
|
322
|
+
];
|
|
323
|
+
const seen = decorationsFor(source, ranges);
|
|
324
|
+
|
|
325
|
+
expect(seen.size).toBe(sections);
|
|
326
|
+
for (let index = 0; index < sections; index++) {
|
|
327
|
+
// The document-wide range and exactly this block's own range.
|
|
328
|
+
expect(seen.get(`${index}:paragraph`)).toEqual([
|
|
329
|
+
{name: 'hit'},
|
|
330
|
+
{name: 'hit'},
|
|
331
|
+
]);
|
|
332
|
+
}
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
it('appends later plugins after earlier ones', () => {
|
|
336
|
+
const paragraph = spanOf('Ada shipped the parser.');
|
|
337
|
+
const seen: Seen = new Map();
|
|
338
|
+
parseMarkdown(SOURCE, {
|
|
339
|
+
plugins: [
|
|
340
|
+
decorationPlugin(
|
|
341
|
+
'first-pass',
|
|
342
|
+
createMarkdownSourceDecoration({name: 'first', ranges: [paragraph]}),
|
|
343
|
+
),
|
|
344
|
+
decorationPlugin(
|
|
345
|
+
'second-pass',
|
|
346
|
+
createMarkdownSourceDecoration({name: 'second', ranges: [paragraph]}),
|
|
347
|
+
),
|
|
348
|
+
observerPlugin('order-observer', seen),
|
|
349
|
+
],
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
expect(seen.get('1:paragraph')).toEqual([
|
|
353
|
+
{name: 'first'},
|
|
354
|
+
{name: 'second'},
|
|
355
|
+
]);
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
it('leaves parsed results, rendered output, and heading identity unchanged', () => {
|
|
359
|
+
const decorated = decorationPlugin(
|
|
360
|
+
'invariance',
|
|
361
|
+
createMarkdownSourceDecoration({
|
|
362
|
+
name: 'hit',
|
|
363
|
+
ranges: [spanOf('# Release notes'), spanOf('Ada shipped the parser.')],
|
|
364
|
+
}),
|
|
365
|
+
);
|
|
366
|
+
|
|
367
|
+
expect(parseMarkdown(SOURCE, {plugins: [decorated]})).toEqual(
|
|
368
|
+
parseMarkdown(SOURCE),
|
|
369
|
+
);
|
|
370
|
+
expect(
|
|
371
|
+
parseMarkdown(SOURCE, {sourceRanges: true, plugins: [decorated]}),
|
|
372
|
+
).toEqual(parseMarkdown(SOURCE, {sourceRanges: true}));
|
|
373
|
+
|
|
374
|
+
const plain = renderToString(<Markdown>{SOURCE}</Markdown>);
|
|
375
|
+
const withDecoration = renderToString(
|
|
376
|
+
<Markdown plugins={[decorated]}>{SOURCE}</Markdown>,
|
|
377
|
+
);
|
|
378
|
+
expect(withDecoration).toBe(plain);
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
it('never mutates the document a transform received', () => {
|
|
382
|
+
const paragraph = spanOf('Ada shipped the parser.');
|
|
383
|
+
const before: ReadonlyArray<MarkdownSourceDecoration>[] = [];
|
|
384
|
+
const after: ReadonlyArray<MarkdownSourceDecoration>[] = [];
|
|
385
|
+
const capture = (
|
|
386
|
+
name: string,
|
|
387
|
+
sink: ReadonlyArray<MarkdownSourceDecoration>[],
|
|
388
|
+
) =>
|
|
389
|
+
createMarkdownPlugin({
|
|
390
|
+
name,
|
|
391
|
+
apiVersion: 1,
|
|
392
|
+
transform(root) {
|
|
393
|
+
const block = root.children[1];
|
|
394
|
+
sink.push(getMarkdownSourceDecorations(block));
|
|
395
|
+
expect(Object.isFrozen(block)).toBe(true);
|
|
396
|
+
return root;
|
|
397
|
+
},
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
parseMarkdown(SOURCE, {
|
|
401
|
+
plugins: [
|
|
402
|
+
capture('mutation-before', before),
|
|
403
|
+
decorationPlugin(
|
|
404
|
+
'mutation-decorate',
|
|
405
|
+
createMarkdownSourceDecoration({name: 'hit', ranges: [paragraph]}),
|
|
406
|
+
),
|
|
407
|
+
capture('mutation-after', after),
|
|
408
|
+
],
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
expect(before).toEqual([[]]);
|
|
412
|
+
expect(after).toEqual([[{name: 'hit'}]]);
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
it('freezes every sibling before an untrusted plugin observes the tree', () => {
|
|
416
|
+
// A trusted helper carries undecorated siblings over by identity. Those
|
|
417
|
+
// siblings must still be deeply frozen before plugin-authored code runs,
|
|
418
|
+
// or a later plugin can mutate a node the helper never touched.
|
|
419
|
+
const source = '# Title\n\nAlpha prose.\n\nBeta prose.\n\nGamma prose.';
|
|
420
|
+
const target = source.indexOf('Beta prose.');
|
|
421
|
+
const observed: {type: string; frozen: boolean; textFrozen: boolean}[] = [];
|
|
422
|
+
let mutationRejected = false;
|
|
423
|
+
|
|
424
|
+
const attacker = createMarkdownPlugin({
|
|
425
|
+
name: 'sibling-attacker',
|
|
426
|
+
apiVersion: 1,
|
|
427
|
+
transform(root) {
|
|
428
|
+
root.children.forEach(block => {
|
|
429
|
+
const child =
|
|
430
|
+
'children' in block
|
|
431
|
+
? (block.children[0] as object | undefined)
|
|
432
|
+
: undefined;
|
|
433
|
+
observed.push({
|
|
434
|
+
type: block.type,
|
|
435
|
+
frozen: Object.isFrozen(block),
|
|
436
|
+
textFrozen: child == null || Object.isFrozen(child),
|
|
437
|
+
});
|
|
438
|
+
});
|
|
439
|
+
// Attempt an in-place edit of an UNDECORATED sibling's text node.
|
|
440
|
+
const victim = root.children[1];
|
|
441
|
+
const text =
|
|
442
|
+
victim.type === 'paragraph'
|
|
443
|
+
? (victim.children[0] as {value: string})
|
|
444
|
+
: undefined;
|
|
445
|
+
try {
|
|
446
|
+
if (text != null) {
|
|
447
|
+
text.value = 'HACKED';
|
|
448
|
+
}
|
|
449
|
+
mutationRejected = text?.value !== 'HACKED';
|
|
450
|
+
} catch {
|
|
451
|
+
mutationRejected = true;
|
|
452
|
+
}
|
|
453
|
+
return root;
|
|
454
|
+
},
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
const blocks = parseMarkdown(source, {
|
|
458
|
+
plugins: [
|
|
459
|
+
decorationPlugin(
|
|
460
|
+
'sibling-decoration',
|
|
461
|
+
createMarkdownSourceDecoration({
|
|
462
|
+
name: 'hit',
|
|
463
|
+
ranges: [{start: target, end: target + 'Beta prose.'.length}],
|
|
464
|
+
}),
|
|
465
|
+
),
|
|
466
|
+
attacker,
|
|
467
|
+
],
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
expect(observed).toHaveLength(4);
|
|
471
|
+
expect(observed.every(entry => entry.frozen && entry.textFrozen)).toBe(
|
|
472
|
+
true,
|
|
473
|
+
);
|
|
474
|
+
expect(mutationRejected).toBe(true);
|
|
475
|
+
// The undecorated sibling reached rendering unchanged.
|
|
476
|
+
expect(blocks[1]).toEqual({
|
|
477
|
+
type: 'paragraph',
|
|
478
|
+
children: [{type: 'text', content: 'Alpha prose.'}],
|
|
479
|
+
});
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
it('leaves a prior streamed snapshot untouched when a later chunk decorates', () => {
|
|
483
|
+
const source = 'Alpha prose.\n\nBeta prose.';
|
|
484
|
+
const target = source.indexOf('Beta prose.');
|
|
485
|
+
const plugins = [
|
|
486
|
+
decorationPlugin(
|
|
487
|
+
'snapshot-decoration',
|
|
488
|
+
createMarkdownSourceDecoration({
|
|
489
|
+
name: 'hit',
|
|
490
|
+
ranges: [{start: target, end: target + 'Beta prose.'.length}],
|
|
491
|
+
}),
|
|
492
|
+
),
|
|
493
|
+
];
|
|
494
|
+
|
|
495
|
+
const state = createIncrementalState();
|
|
496
|
+
const partial = parseMarkdownAstIncremental(
|
|
497
|
+
source.slice(0, target),
|
|
498
|
+
state,
|
|
499
|
+
{plugins, isFinal: false},
|
|
500
|
+
).children;
|
|
501
|
+
const partialSnapshot = JSON.parse(JSON.stringify(partial));
|
|
502
|
+
const partialBlocks = [...partial];
|
|
503
|
+
|
|
504
|
+
parseMarkdownAstIncremental(source, state, {plugins, isFinal: true});
|
|
505
|
+
|
|
506
|
+
// The earlier snapshot is a settled value a caller may still hold: the
|
|
507
|
+
// later chunk's decoration must not have reached back into it.
|
|
508
|
+
expect(JSON.parse(JSON.stringify(partial))).toEqual(partialSnapshot);
|
|
509
|
+
expect(partialBlocks.map(getMarkdownSourceDecorations)).toEqual(
|
|
510
|
+
partialBlocks.map(() => []),
|
|
511
|
+
);
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
it('copies caller ranges so later mutation cannot change decorations', () => {
|
|
515
|
+
const paragraph = spanOf('Ada shipped the parser.');
|
|
516
|
+
const payload = {reviewer: 'ada'};
|
|
517
|
+
const ranges = [{...paragraph, data: payload}];
|
|
518
|
+
const transform = createMarkdownSourceDecoration({name: 'hit', ranges});
|
|
519
|
+
payload.reviewer = 'linus';
|
|
520
|
+
ranges.push({start: 0, end: 4, data: payload});
|
|
521
|
+
|
|
522
|
+
const seen: Seen = new Map();
|
|
523
|
+
parseMarkdown(SOURCE, {
|
|
524
|
+
plugins: [
|
|
525
|
+
decorationPlugin('immutable-options', transform),
|
|
526
|
+
observerPlugin('immutable-options-observer', seen),
|
|
527
|
+
],
|
|
528
|
+
});
|
|
529
|
+
|
|
530
|
+
expect(seen.get('1:paragraph')).toEqual([
|
|
531
|
+
{name: 'hit', data: {reviewer: 'ada'}},
|
|
532
|
+
]);
|
|
533
|
+
expect(seen.get('0:heading')).toEqual([]);
|
|
534
|
+
});
|
|
535
|
+
|
|
536
|
+
it('leaves plugin extension nodes to their owner', () => {
|
|
537
|
+
type NoteNode = MarkdownExtensionNode<
|
|
538
|
+
'decoration-notes',
|
|
539
|
+
'note',
|
|
540
|
+
{readonly body: string},
|
|
541
|
+
'block'
|
|
542
|
+
>;
|
|
543
|
+
const source = ':::note\nOwned by the plugin\n:::\n\nAda shipped it.';
|
|
544
|
+
const notes = createMarkdownPlugin<'decoration-notes', NoteNode>({
|
|
545
|
+
name: 'decoration-notes',
|
|
546
|
+
apiVersion: 1,
|
|
547
|
+
parseKey: 'v1',
|
|
548
|
+
syntax: {
|
|
549
|
+
block: [
|
|
550
|
+
{
|
|
551
|
+
startsWith: [':::note'],
|
|
552
|
+
maxSpan: 200,
|
|
553
|
+
tokenize({source: input, offset, end, isFinal}) {
|
|
554
|
+
const close = input.indexOf('\n:::', offset + 7);
|
|
555
|
+
if (close < 0 || close + 4 > end) {
|
|
556
|
+
return isFinal ? {status: 'no-match'} : {status: 'defer'};
|
|
557
|
+
}
|
|
558
|
+
return {
|
|
559
|
+
status: 'match',
|
|
560
|
+
end: close + 4,
|
|
561
|
+
node: {
|
|
562
|
+
type: 'extension',
|
|
563
|
+
plugin: 'decoration-notes',
|
|
564
|
+
name: 'note',
|
|
565
|
+
display: 'block',
|
|
566
|
+
data: {body: input.slice(offset + 7, close).trim()},
|
|
567
|
+
},
|
|
568
|
+
};
|
|
569
|
+
},
|
|
570
|
+
},
|
|
571
|
+
],
|
|
572
|
+
},
|
|
573
|
+
renderers: {
|
|
574
|
+
note: {
|
|
575
|
+
render: ({node}) => node.data.body,
|
|
576
|
+
toText: node => node.data.body,
|
|
577
|
+
},
|
|
578
|
+
},
|
|
579
|
+
} satisfies MarkdownSyntaxPluginDefinition<'decoration-notes', NoteNode>);
|
|
580
|
+
|
|
581
|
+
const seen: Seen = new Map();
|
|
582
|
+
const blocks = parseMarkdown(source, {
|
|
583
|
+
plugins: [
|
|
584
|
+
notes,
|
|
585
|
+
decorationPlugin(
|
|
586
|
+
'extension-ownership',
|
|
587
|
+
createMarkdownSourceDecoration({
|
|
588
|
+
name: 'hit',
|
|
589
|
+
ranges: [{start: 0, end: source.length}],
|
|
590
|
+
}),
|
|
591
|
+
),
|
|
592
|
+
observerPlugin('extension-ownership-observer', seen),
|
|
593
|
+
],
|
|
594
|
+
});
|
|
595
|
+
|
|
596
|
+
expect(seen.get('0:extension')).toEqual([]);
|
|
597
|
+
expect(seen.get('1:paragraph')).toEqual([{name: 'hit'}]);
|
|
598
|
+
expect(blocks[0]).toMatchObject({
|
|
599
|
+
type: 'extension',
|
|
600
|
+
data: {body: 'Owned by the plugin'},
|
|
601
|
+
});
|
|
602
|
+
});
|
|
603
|
+
|
|
604
|
+
it('stores decorations in a versioned Core-owned envelope', () => {
|
|
605
|
+
const root = parseMarkdownAst(SOURCE, {
|
|
606
|
+
plugins: [
|
|
607
|
+
decorationPlugin(
|
|
608
|
+
'envelope-shape',
|
|
609
|
+
createMarkdownSourceDecoration({
|
|
610
|
+
name: 'hit',
|
|
611
|
+
ranges: [spanOf('Ada shipped the parser.')],
|
|
612
|
+
}),
|
|
613
|
+
),
|
|
614
|
+
],
|
|
615
|
+
});
|
|
616
|
+
|
|
617
|
+
expect(root.children[1]?.data).toEqual({
|
|
618
|
+
'astryx:sourceDecorations': {
|
|
619
|
+
kind: 'astryx.markdown.sourceDecorations',
|
|
620
|
+
version: 1,
|
|
621
|
+
entries: [{name: 'hit'}],
|
|
622
|
+
},
|
|
623
|
+
});
|
|
624
|
+
});
|
|
625
|
+
|
|
626
|
+
it('never merges into foreign data and reports the skip once', () => {
|
|
627
|
+
const warning = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
628
|
+
const seen: Seen = new Map();
|
|
629
|
+
// Two foreign shapes: an opaque scalar, and an unbranded array parked at
|
|
630
|
+
// the envelope key by another plugin.
|
|
631
|
+
const foreign = createMarkdownPlugin({
|
|
632
|
+
name: 'foreign-data',
|
|
633
|
+
apiVersion: 1,
|
|
634
|
+
transform(root) {
|
|
635
|
+
return {
|
|
636
|
+
...root,
|
|
637
|
+
children: root.children.map(block => {
|
|
638
|
+
if (block.type === 'heading') {
|
|
639
|
+
return {...block, data: 'opaque'};
|
|
640
|
+
}
|
|
641
|
+
if (block.type === 'blockquote') {
|
|
642
|
+
return {
|
|
643
|
+
...block,
|
|
644
|
+
data: {'astryx:sourceDecorations': [{name: 'forged'}]},
|
|
645
|
+
};
|
|
646
|
+
}
|
|
647
|
+
return block;
|
|
648
|
+
}),
|
|
649
|
+
};
|
|
650
|
+
},
|
|
651
|
+
});
|
|
652
|
+
|
|
653
|
+
parseMarkdown(SOURCE, {
|
|
654
|
+
plugins: [
|
|
655
|
+
foreign,
|
|
656
|
+
decorationPlugin(
|
|
657
|
+
'foreign-data-decoration',
|
|
658
|
+
createMarkdownSourceDecoration({
|
|
659
|
+
name: 'hit',
|
|
660
|
+
ranges: [{start: 0, end: SOURCE.length}],
|
|
661
|
+
}),
|
|
662
|
+
),
|
|
663
|
+
observerPlugin('foreign-data-observer', seen),
|
|
664
|
+
],
|
|
665
|
+
});
|
|
666
|
+
|
|
667
|
+
expect(seen.get('0:heading')).toEqual([]);
|
|
668
|
+
expect(seen.get('2:blockquote')).toEqual([]);
|
|
669
|
+
expect(seen.get('1:paragraph')).toEqual([{name: 'hit'}]);
|
|
670
|
+
expect(warning).toHaveBeenCalledTimes(1);
|
|
671
|
+
expect(warning.mock.calls[0]?.[0]).toContain('foreign-data-decoration');
|
|
672
|
+
warning.mockRestore();
|
|
673
|
+
});
|
|
674
|
+
|
|
675
|
+
it('keeps unrelated data on a node it decorates', () => {
|
|
676
|
+
const paragraph = spanOf('Ada shipped the parser.');
|
|
677
|
+
const annotate = createMarkdownPlugin({
|
|
678
|
+
name: 'annotate-first',
|
|
679
|
+
apiVersion: 1,
|
|
680
|
+
transform(root) {
|
|
681
|
+
return {
|
|
682
|
+
...root,
|
|
683
|
+
children: root.children.map(block =>
|
|
684
|
+
block.type === 'paragraph'
|
|
685
|
+
? {...block, data: {owner: 'ada'}}
|
|
686
|
+
: block,
|
|
687
|
+
),
|
|
688
|
+
};
|
|
689
|
+
},
|
|
690
|
+
});
|
|
691
|
+
|
|
692
|
+
const root = parseMarkdownAst(SOURCE, {
|
|
693
|
+
plugins: [
|
|
694
|
+
annotate,
|
|
695
|
+
decorationPlugin(
|
|
696
|
+
'annotate-decoration',
|
|
697
|
+
createMarkdownSourceDecoration({name: 'hit', ranges: [paragraph]}),
|
|
698
|
+
),
|
|
699
|
+
],
|
|
700
|
+
});
|
|
701
|
+
|
|
702
|
+
expect(root.children[1]?.data).toEqual({
|
|
703
|
+
owner: 'ada',
|
|
704
|
+
'astryx:sourceDecorations': {
|
|
705
|
+
kind: 'astryx.markdown.sourceDecorations',
|
|
706
|
+
version: 1,
|
|
707
|
+
entries: [{name: 'hit'}],
|
|
708
|
+
},
|
|
709
|
+
});
|
|
710
|
+
});
|
|
711
|
+
|
|
712
|
+
it('rejects invalid options at creation', () => {
|
|
713
|
+
expect(() =>
|
|
714
|
+
createMarkdownSourceDecoration({name: ' ', ranges: []}),
|
|
715
|
+
).toThrow(TypeError);
|
|
716
|
+
expect(() =>
|
|
717
|
+
createMarkdownSourceDecoration({
|
|
718
|
+
name: 'hit',
|
|
719
|
+
ranges: [{start: 4, end: 4}],
|
|
720
|
+
}),
|
|
721
|
+
).toThrow(TypeError);
|
|
722
|
+
expect(() =>
|
|
723
|
+
createMarkdownSourceDecoration({
|
|
724
|
+
name: 'hit',
|
|
725
|
+
ranges: [{start: -1, end: 4}],
|
|
726
|
+
}),
|
|
727
|
+
).toThrow(TypeError);
|
|
728
|
+
expect(() =>
|
|
729
|
+
createMarkdownSourceDecoration({
|
|
730
|
+
name: 'hit',
|
|
731
|
+
ranges: [{start: 0, end: 1.5}],
|
|
732
|
+
}),
|
|
733
|
+
).toThrow(TypeError);
|
|
734
|
+
expect(() =>
|
|
735
|
+
createMarkdownSourceDecoration({
|
|
736
|
+
name: 'hit',
|
|
737
|
+
ranges: [
|
|
738
|
+
{start: 0, end: 4, data: {render: (() => null) as unknown as string}},
|
|
739
|
+
],
|
|
740
|
+
}),
|
|
741
|
+
).toThrow(TypeError);
|
|
742
|
+
});
|
|
743
|
+
|
|
744
|
+
it('treats an empty range list as a no-op transform', () => {
|
|
745
|
+
const seen = decorationsFor(SOURCE, []);
|
|
746
|
+
expect([...seen.values()].flat()).toEqual([]);
|
|
747
|
+
});
|
|
748
|
+
|
|
749
|
+
it('types as an extension-free transform and reads only its own envelope', () => {
|
|
750
|
+
const transform = createMarkdownSourceDecoration({
|
|
751
|
+
name: 'hit',
|
|
752
|
+
ranges: [{start: 0, end: 4}],
|
|
753
|
+
});
|
|
754
|
+
expectTypeOf(transform).toExtend<MarkdownTransform<never>>();
|
|
755
|
+
// A decoration plugin introduces no extension nodes, so it needs no
|
|
756
|
+
// renderers.
|
|
757
|
+
const plugin = createMarkdownPlugin({
|
|
758
|
+
name: 'typed-decoration',
|
|
759
|
+
apiVersion: 1,
|
|
760
|
+
transform,
|
|
761
|
+
});
|
|
762
|
+
expectTypeOf(plugin.name).toEqualTypeOf<string>();
|
|
763
|
+
expectTypeOf(getMarkdownSourceDecorations({})).toEqualTypeOf<
|
|
764
|
+
ReadonlyArray<MarkdownSourceDecoration>
|
|
765
|
+
>();
|
|
766
|
+
|
|
767
|
+
expect(getMarkdownSourceDecorations({data: 'opaque'})).toEqual([]);
|
|
768
|
+
expect(
|
|
769
|
+
getMarkdownSourceDecorations({data: {decorations: [{name: 'x'}]}}),
|
|
770
|
+
).toEqual([]);
|
|
771
|
+
expect(
|
|
772
|
+
getMarkdownSourceDecorations({
|
|
773
|
+
data: {'astryx:sourceDecorations': [{name: 'x'}]},
|
|
774
|
+
}),
|
|
775
|
+
).toEqual([]);
|
|
776
|
+
expect(
|
|
777
|
+
getMarkdownSourceDecorations({
|
|
778
|
+
data: {
|
|
779
|
+
'astryx:sourceDecorations': {
|
|
780
|
+
kind: 'astryx.markdown.sourceDecorations',
|
|
781
|
+
version: 2,
|
|
782
|
+
entries: [{name: 'x'}],
|
|
783
|
+
},
|
|
784
|
+
},
|
|
785
|
+
}),
|
|
786
|
+
).toEqual([]);
|
|
787
|
+
expect(
|
|
788
|
+
getMarkdownSourceDecorations({
|
|
789
|
+
data: {
|
|
790
|
+
'astryx:sourceDecorations': {
|
|
791
|
+
kind: 'astryx.markdown.sourceDecorations',
|
|
792
|
+
version: 1,
|
|
793
|
+
entries: [{name: 'x'}],
|
|
794
|
+
},
|
|
795
|
+
},
|
|
796
|
+
}),
|
|
797
|
+
).toEqual([{name: 'x'}]);
|
|
798
|
+
});
|
|
799
|
+
});
|