@astryxdesign/core 0.6.1 → 0.6.2-canary.023c3e4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +33 -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/ChatComposerInput.d.ts.map +1 -1
- package/dist/Chat/ChatComposerInput.js +2 -2
- 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 +3 -0
- package/dist/DateRangeInput/DateRangeInput.d.ts.map +1 -1
- package/dist/DateRangeInput/DateRangeInput.js +20 -13
- 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 +1 -1
- package/dist/Dialog/DialogHeader.d.ts.map +1 -1
- package/dist/Dialog/DialogHeader.js +10 -7
- 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 +16 -10
- 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 +18 -4
- 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 +311 -114
- 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 +220 -38
- package/dist/Markdown/parser.d.ts.map +1 -1
- package/dist/Markdown/parser.js +1072 -188
- 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.map +1 -1
- package/dist/Selector/Selector.js +1 -1
- 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 +19 -10
- package/dist/Spinner/Spinner.d.ts +1 -1
- package/dist/Spinner/Spinner.d.ts.map +1 -1
- package/dist/Spinner/Spinner.js +45 -21
- 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/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 +2 -1
- package/dist/ToggleButton/ToggleButton.d.ts.map +1 -1
- package/dist/ToggleButton/ToggleButton.js +7 -1
- package/dist/Typeahead/BaseTypeahead.d.ts.map +1 -1
- package/dist/Typeahead/BaseTypeahead.js +16 -7
- package/dist/astryx.css +19 -8
- 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/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/en.json +16 -0
- package/locales/pseudo.json +12 -0
- package/package.json +22 -5
- package/scripts/agent-doc-state.mjs +1 -1
- 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/ChatComposerInput.doc.mjs +3 -3
- package/src/Chat/ChatComposerInput.tsx +10 -2
- 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.doc.mjs +35 -7
- package/src/DateRangeInput/DateRangeInput.spec.md +203 -0
- package/src/DateRangeInput/DateRangeInput.test.tsx +100 -4
- package/src/DateRangeInput/DateRangeInput.tsx +35 -21
- package/src/DateTimeInput/DateTimeInput.tsx +6 -1
- package/src/DateTimeInput/TouchDateTimeField.tsx +6 -1
- package/src/DateTimeInput/nativePickerSegmentStyles.ts +24 -8
- package/src/Dialog/Dialog.doc.mjs +3 -0
- package/src/Dialog/Dialog.spec.md +1 -1
- package/src/Dialog/DialogHeader.doc.mjs +38 -0
- package/src/Dialog/DialogHeader.test.tsx +49 -0
- package/src/Dialog/DialogHeader.tsx +23 -4
- package/src/Dialog/modules/DialogHeader.spec.md +152 -0
- package/src/Field/PanelSearchInput.tsx +5 -2
- package/src/FieldStatus/__tests__/StatusMessage.a11y.states.ts +1 -1
- package/src/FileInput/FileInput.doc.mjs +2 -0
- package/src/FileInput/FileInput.spec.md +199 -0
- package/src/FileInput/FileInput.test.tsx +14 -0
- package/src/FileInput/FileInput.tsx +24 -5
- 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/Markdown/Markdown.doc.mjs +439 -42
- 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 +280 -0
- package/src/Markdown/Markdown.spec.md +304 -72
- package/src/Markdown/Markdown.test.tsx +118 -3
- package/src/Markdown/Markdown.tsx +489 -129
- package/src/Markdown/ast.test.ts +307 -0
- package/src/Markdown/ast.ts +337 -0
- package/src/Markdown/incremental.test.ts +187 -7
- package/src/Markdown/index.ts +43 -1
- package/src/Markdown/modules/remark.spec.md +269 -0
- package/src/Markdown/parser/index.ts +64 -0
- package/src/Markdown/parser.perf.test.ts +3 -1
- package/src/Markdown/parser.public.test.ts +103 -0
- package/src/Markdown/parser.test.ts +122 -0
- package/src/Markdown/parser.ts +1721 -248
- 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 +19 -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.spec.md +12 -9
- package/src/Selector/Selector.tsx +6 -1
- 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 +27 -0
- package/src/Slider/Slider.spec.md +61 -47
- package/src/Slider/Slider.test.tsx +146 -0
- package/src/Slider/Slider.tsx +37 -13
- package/src/Spinner/Spinner.doc.mjs +6 -3
- package/src/Spinner/Spinner.test.tsx +41 -0
- package/src/Spinner/Spinner.tsx +62 -32
- 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.test.tsx +66 -0
- package/src/TabList/__tests__/Tabs.a11y.known-failures.ts +1 -34
- 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.test.tsx +133 -0
- package/src/ToggleButton/ToggleButton.tsx +9 -2
- package/src/ToggleButton/__tests__/ToggleButton.a11y.chromium.spec.ts +209 -0
- package/src/Tokenizer/Tokenizer.spec.md +142 -75
- package/src/TreeList/TreeList.spec.md +123 -75
- 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/theme/derivedVarRegistry.test.ts +6 -4
- package/src/utils/interactionOverlay.stylex.ts +20 -0
package/src/Markdown/parser.ts
CHANGED
|
@@ -2,29 +2,81 @@
|
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* @file parser.ts
|
|
5
|
-
* @input Markdown string
|
|
6
|
-
* @output
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
5
|
+
* @input Markdown string, released parse options, and optional ordered plugins
|
|
6
|
+
* @output Canonical MDAST-aligned nodes for public/server consumers plus unchanged
|
|
7
|
+
* released parser-node projections; shared heading slug helpers
|
|
8
|
+
* @position Core parser and compatibility boundary; consumed by public parser entry,
|
|
9
|
+
* Markdown, and Outline
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
+
import {
|
|
13
|
+
getMarkdownAstLegacyCodeLanguage,
|
|
14
|
+
markMarkdownAstLegacyCodeLanguage,
|
|
15
|
+
} from './ast';
|
|
16
|
+
import type {
|
|
17
|
+
MarkdownAstBlockContent,
|
|
18
|
+
MarkdownAstList,
|
|
19
|
+
MarkdownAstListItem,
|
|
20
|
+
MarkdownAstPhrasingContent,
|
|
21
|
+
MarkdownAstPosition,
|
|
22
|
+
MarkdownAstRoot,
|
|
23
|
+
MarkdownAstTableCell,
|
|
24
|
+
MarkdownAstTableRow,
|
|
25
|
+
} from './ast';
|
|
26
|
+
import {
|
|
27
|
+
applyMarkdownTransforms,
|
|
28
|
+
freezeMarkdownPluginData,
|
|
29
|
+
isMarkdownPluginData,
|
|
30
|
+
prepareMarkdownPlugins,
|
|
31
|
+
reportMarkdownPluginFailure,
|
|
32
|
+
} from './plugins/protocol';
|
|
33
|
+
import type {
|
|
34
|
+
MarkdownExtensionNode,
|
|
35
|
+
MarkdownExtensionsOf,
|
|
36
|
+
MarkdownPluginData,
|
|
37
|
+
MarkdownPluginEntry,
|
|
38
|
+
PreparedMarkdownPlugins,
|
|
39
|
+
PreparedSyntaxContribution,
|
|
40
|
+
} from './plugins/protocol';
|
|
41
|
+
import {isSafeMarkdownParserUrl} from './url';
|
|
42
|
+
|
|
12
43
|
// ---------------------------------------------------------------------------
|
|
13
44
|
// Types
|
|
14
45
|
// ---------------------------------------------------------------------------
|
|
15
46
|
|
|
16
|
-
|
|
47
|
+
/** Nodes returned by default and legacy parser calls. */
|
|
48
|
+
export type InlineNode<Extension extends MarkdownExtensionNode = never> =
|
|
49
|
+
| {type: 'text'; content: string}
|
|
50
|
+
| {type: 'bold'; children: InlineNode<Extension>[]}
|
|
51
|
+
| {type: 'italic'; children: InlineNode<Extension>[]}
|
|
52
|
+
| {type: 'strikethrough'; children: InlineNode<Extension>[]}
|
|
53
|
+
| {type: 'code'; content: string}
|
|
54
|
+
| {type: 'link'; href: string; children: InlineNode<Extension>[]}
|
|
55
|
+
| {type: 'image'; src: string; alt: string}
|
|
56
|
+
| {type: 'citation'; sourceId: string}
|
|
57
|
+
| {type: 'break'}
|
|
58
|
+
| Extract<Extension, {display: 'inline'}>;
|
|
59
|
+
|
|
60
|
+
/** The additional inline node returned only when parsing with `math: true`. */
|
|
61
|
+
export type MathInlineNode = {type: 'math'; value: string};
|
|
62
|
+
|
|
63
|
+
/** Nodes returned by an explicitly math-enabled inline parse. */
|
|
64
|
+
export type InlineNodeWithMath<
|
|
65
|
+
Extension extends MarkdownExtensionNode = never,
|
|
66
|
+
> =
|
|
17
67
|
| {type: 'text'; content: string}
|
|
18
|
-
| {type: 'bold'; children:
|
|
19
|
-
| {type: 'italic'; children:
|
|
20
|
-
| {type: 'strikethrough'; children:
|
|
68
|
+
| {type: 'bold'; children: InlineNodeWithMath<Extension>[]}
|
|
69
|
+
| {type: 'italic'; children: InlineNodeWithMath<Extension>[]}
|
|
70
|
+
| {type: 'strikethrough'; children: InlineNodeWithMath<Extension>[]}
|
|
21
71
|
| {type: 'code'; content: string}
|
|
22
|
-
|
|
|
72
|
+
| MathInlineNode
|
|
73
|
+
| {type: 'link'; href: string; children: InlineNodeWithMath<Extension>[]}
|
|
23
74
|
| {type: 'image'; src: string; alt: string}
|
|
24
75
|
| {type: 'citation'; sourceId: string}
|
|
25
|
-
| {type: 'break'}
|
|
76
|
+
| {type: 'break'}
|
|
77
|
+
| Extract<Extension, {display: 'inline'}>;
|
|
26
78
|
|
|
27
|
-
|
|
79
|
+
type BlockMetadata = {
|
|
28
80
|
/**
|
|
29
81
|
* Where this block came from in the source, when parsed with the
|
|
30
82
|
* `sourceRanges` option. Top-level blocks only.
|
|
@@ -32,11 +84,15 @@ export type BlockNode = BlockNodeKind & {
|
|
|
32
84
|
range?: SourceRange;
|
|
33
85
|
};
|
|
34
86
|
|
|
35
|
-
type
|
|
36
|
-
| {
|
|
37
|
-
|
|
87
|
+
type LegacyBlockNodeKind<Extension extends MarkdownExtensionNode = never> =
|
|
88
|
+
| {
|
|
89
|
+
type: 'heading';
|
|
90
|
+
level: 1 | 2 | 3 | 4 | 5 | 6;
|
|
91
|
+
children: InlineNode<Extension>[];
|
|
92
|
+
}
|
|
93
|
+
| {type: 'paragraph'; children: InlineNode<Extension>[]}
|
|
38
94
|
| {type: 'codeblock'; language: string; content: string}
|
|
39
|
-
| {type: 'blockquote'; children: BlockNode[]}
|
|
95
|
+
| {type: 'blockquote'; children: BlockNode<Extension>[]}
|
|
40
96
|
| {
|
|
41
97
|
type: 'list';
|
|
42
98
|
ordered: boolean;
|
|
@@ -44,16 +100,57 @@ type BlockNodeKind =
|
|
|
44
100
|
/** Ordered-list marker delimiter ('.' or ')'). Undefined for bullets. */
|
|
45
101
|
delimiter?: '.' | ')';
|
|
46
102
|
loose?: boolean;
|
|
47
|
-
items: ListItemNode[];
|
|
103
|
+
items: ListItemNode<Extension>[];
|
|
48
104
|
}
|
|
49
105
|
| {
|
|
50
106
|
type: 'table';
|
|
51
|
-
headers: TableCellNode[];
|
|
107
|
+
headers: TableCellNode<Extension>[];
|
|
52
108
|
alignments: TableAlignment[];
|
|
53
|
-
rows: TableCellNode[][];
|
|
109
|
+
rows: TableCellNode<Extension>[][];
|
|
54
110
|
}
|
|
55
111
|
| {type: 'hr'}
|
|
56
|
-
| {type: 'image'; src: string; alt: string}
|
|
112
|
+
| {type: 'image'; src: string; alt: string}
|
|
113
|
+
| Extract<Extension, {display: 'block'}>;
|
|
114
|
+
|
|
115
|
+
/** Blocks returned by default and legacy parser calls. */
|
|
116
|
+
export type BlockNode<Extension extends MarkdownExtensionNode = never> =
|
|
117
|
+
LegacyBlockNodeKind<Extension> & BlockMetadata;
|
|
118
|
+
|
|
119
|
+
/** The additional block returned only when parsing with `math: true`. */
|
|
120
|
+
export type MathBlockNode = {type: 'math'; value: string} & BlockMetadata;
|
|
121
|
+
|
|
122
|
+
type MathEnabledBlockNodeKind<Extension extends MarkdownExtensionNode = never> =
|
|
123
|
+
| {
|
|
124
|
+
type: 'heading';
|
|
125
|
+
level: 1 | 2 | 3 | 4 | 5 | 6;
|
|
126
|
+
children: InlineNodeWithMath<Extension>[];
|
|
127
|
+
}
|
|
128
|
+
| {type: 'paragraph'; children: InlineNodeWithMath<Extension>[]}
|
|
129
|
+
| {type: 'codeblock'; language: string; content: string}
|
|
130
|
+
| MathBlockNode
|
|
131
|
+
| {type: 'blockquote'; children: BlockNodeWithMath<Extension>[]}
|
|
132
|
+
| {
|
|
133
|
+
type: 'list';
|
|
134
|
+
ordered: boolean;
|
|
135
|
+
start?: number;
|
|
136
|
+
/** Ordered-list marker delimiter ('.' or ')'). Undefined for bullets. */
|
|
137
|
+
delimiter?: '.' | ')';
|
|
138
|
+
loose?: boolean;
|
|
139
|
+
items: ListItemNodeWithMath<Extension>[];
|
|
140
|
+
}
|
|
141
|
+
| {
|
|
142
|
+
type: 'table';
|
|
143
|
+
headers: TableCellNodeWithMath<Extension>[];
|
|
144
|
+
alignments: TableAlignment[];
|
|
145
|
+
rows: TableCellNodeWithMath<Extension>[][];
|
|
146
|
+
}
|
|
147
|
+
| {type: 'hr'}
|
|
148
|
+
| {type: 'image'; src: string; alt: string}
|
|
149
|
+
| Extract<Extension, {display: 'block'}>;
|
|
150
|
+
|
|
151
|
+
/** Blocks returned by an explicitly math-enabled block parse. */
|
|
152
|
+
export type BlockNodeWithMath<Extension extends MarkdownExtensionNode = never> =
|
|
153
|
+
MathEnabledBlockNodeKind<Extension> & BlockMetadata;
|
|
57
154
|
|
|
58
155
|
/**
|
|
59
156
|
* Where a block sits in the source string handed to `parseMarkdown`:
|
|
@@ -66,10 +163,288 @@ type BlockNodeKind =
|
|
|
66
163
|
*/
|
|
67
164
|
export type SourceRange = {readonly start: number; readonly end: number};
|
|
68
165
|
|
|
69
|
-
export type ListItemNode =
|
|
70
|
-
|
|
166
|
+
export type ListItemNode<Extension extends MarkdownExtensionNode = never> = {
|
|
167
|
+
checked?: boolean;
|
|
168
|
+
children: BlockNode<Extension>[];
|
|
169
|
+
};
|
|
170
|
+
type ListItemNodeWithMath<Extension extends MarkdownExtensionNode = never> = {
|
|
171
|
+
checked?: boolean;
|
|
172
|
+
children: BlockNodeWithMath<Extension>[];
|
|
173
|
+
};
|
|
174
|
+
export type TableCellNode<Extension extends MarkdownExtensionNode = never> = {
|
|
175
|
+
children: InlineNode<Extension>[];
|
|
176
|
+
};
|
|
177
|
+
type TableCellNodeWithMath<Extension extends MarkdownExtensionNode = never> = {
|
|
178
|
+
children: InlineNodeWithMath<Extension>[];
|
|
179
|
+
};
|
|
71
180
|
export type TableAlignment = 'left' | 'center' | 'right' | null;
|
|
72
181
|
|
|
182
|
+
type RuntimeExtensionNode =
|
|
183
|
+
| MarkdownExtensionNode<string, string, MarkdownPluginData, 'inline'>
|
|
184
|
+
| MarkdownExtensionNode<string, string, MarkdownPluginData, 'block'>;
|
|
185
|
+
type RuntimeInlineNode = InlineNodeWithMath<RuntimeExtensionNode>;
|
|
186
|
+
type RuntimeBlockNode = BlockNodeWithMath<RuntimeExtensionNode>;
|
|
187
|
+
|
|
188
|
+
type LegacyProjectionCache = WeakMap<object, object>;
|
|
189
|
+
|
|
190
|
+
function projectRange(
|
|
191
|
+
position: MarkdownAstPosition | undefined,
|
|
192
|
+
): BlockMetadata {
|
|
193
|
+
const start = position?.start.offset;
|
|
194
|
+
const end = position?.end.offset;
|
|
195
|
+
return start == null || end == null ? {} : {range: {start, end}};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Canonical nodes always carry Core-authored positions; the released block
|
|
200
|
+
* shape exposes them as `range` only when the caller passed `sourceRanges`,
|
|
201
|
+
* so this projection stays byte-for-byte what it has always returned.
|
|
202
|
+
*
|
|
203
|
+
* An extension node is the one kind the projection passes through instead of
|
|
204
|
+
* rebuilding field by field, so it is the one kind that could carry a
|
|
205
|
+
* canonical `position` out into released output. Strip it unless the caller
|
|
206
|
+
* asked for provenance, in which case the node keeps exactly the `position`
|
|
207
|
+
* it has always exposed.
|
|
208
|
+
*/
|
|
209
|
+
function projectExtensionNode<Node extends RuntimeExtensionNode>(
|
|
210
|
+
node: Node,
|
|
211
|
+
withRanges: boolean,
|
|
212
|
+
): Node {
|
|
213
|
+
if (withRanges || node.position === undefined) {
|
|
214
|
+
return node;
|
|
215
|
+
}
|
|
216
|
+
const {position: _canonicalPosition, ...released} = node;
|
|
217
|
+
return released as Node;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function projectInlineNode(
|
|
221
|
+
node: MarkdownAstPhrasingContent<RuntimeExtensionNode>,
|
|
222
|
+
cache: LegacyProjectionCache,
|
|
223
|
+
withRanges: boolean,
|
|
224
|
+
): RuntimeInlineNode {
|
|
225
|
+
const cached = cache.get(node);
|
|
226
|
+
if (cached != null) {
|
|
227
|
+
return cached as RuntimeInlineNode;
|
|
228
|
+
}
|
|
229
|
+
let projected: RuntimeInlineNode;
|
|
230
|
+
switch (node.type) {
|
|
231
|
+
case 'text':
|
|
232
|
+
projected = {type: 'text', content: node.value};
|
|
233
|
+
break;
|
|
234
|
+
case 'strong':
|
|
235
|
+
projected = {
|
|
236
|
+
type: 'bold',
|
|
237
|
+
children: node.children.map(child =>
|
|
238
|
+
projectInlineNode(child, cache, withRanges),
|
|
239
|
+
),
|
|
240
|
+
};
|
|
241
|
+
break;
|
|
242
|
+
case 'emphasis':
|
|
243
|
+
projected = {
|
|
244
|
+
type: 'italic',
|
|
245
|
+
children: node.children.map(child =>
|
|
246
|
+
projectInlineNode(child, cache, withRanges),
|
|
247
|
+
),
|
|
248
|
+
};
|
|
249
|
+
break;
|
|
250
|
+
case 'delete':
|
|
251
|
+
projected = {
|
|
252
|
+
type: 'strikethrough',
|
|
253
|
+
children: node.children.map(child =>
|
|
254
|
+
projectInlineNode(child, cache, withRanges),
|
|
255
|
+
),
|
|
256
|
+
};
|
|
257
|
+
break;
|
|
258
|
+
case 'inlineCode':
|
|
259
|
+
projected = {type: 'code', content: node.value};
|
|
260
|
+
break;
|
|
261
|
+
case 'inlineMath':
|
|
262
|
+
projected = {type: 'math', value: node.value};
|
|
263
|
+
break;
|
|
264
|
+
case 'link':
|
|
265
|
+
projected = {
|
|
266
|
+
type: 'link',
|
|
267
|
+
href: node.url,
|
|
268
|
+
children: node.children.map(child =>
|
|
269
|
+
projectInlineNode(child, cache, withRanges),
|
|
270
|
+
),
|
|
271
|
+
};
|
|
272
|
+
break;
|
|
273
|
+
case 'image':
|
|
274
|
+
projected = {type: 'image', src: node.url, alt: node.alt};
|
|
275
|
+
break;
|
|
276
|
+
case 'citation':
|
|
277
|
+
projected = {type: 'citation', sourceId: node.sourceId};
|
|
278
|
+
break;
|
|
279
|
+
case 'break':
|
|
280
|
+
projected = {type: 'break'};
|
|
281
|
+
break;
|
|
282
|
+
case 'extension':
|
|
283
|
+
projected = projectExtensionNode(node, withRanges);
|
|
284
|
+
break;
|
|
285
|
+
}
|
|
286
|
+
cache.set(node, projected);
|
|
287
|
+
return projected;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function projectTableCell(
|
|
291
|
+
node: MarkdownAstTableCell<RuntimeExtensionNode>,
|
|
292
|
+
cache: LegacyProjectionCache,
|
|
293
|
+
withRanges: boolean,
|
|
294
|
+
): TableCellNodeWithMath<RuntimeExtensionNode> {
|
|
295
|
+
const cached = cache.get(node);
|
|
296
|
+
if (cached != null) {
|
|
297
|
+
return cached as TableCellNodeWithMath<RuntimeExtensionNode>;
|
|
298
|
+
}
|
|
299
|
+
const projected = {
|
|
300
|
+
children: node.children.map(child =>
|
|
301
|
+
projectInlineNode(child, cache, withRanges),
|
|
302
|
+
),
|
|
303
|
+
};
|
|
304
|
+
cache.set(node, projected);
|
|
305
|
+
return projected;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function projectListItem(
|
|
309
|
+
node: MarkdownAstListItem<RuntimeExtensionNode>,
|
|
310
|
+
cache: LegacyProjectionCache,
|
|
311
|
+
withRanges: boolean,
|
|
312
|
+
): ListItemNodeWithMath<RuntimeExtensionNode> {
|
|
313
|
+
const cached = cache.get(node);
|
|
314
|
+
if (cached != null) {
|
|
315
|
+
return cached as ListItemNodeWithMath<RuntimeExtensionNode>;
|
|
316
|
+
}
|
|
317
|
+
const projected = {
|
|
318
|
+
checked: node.checked,
|
|
319
|
+
children: node.children.map(child =>
|
|
320
|
+
projectBlockNode(child, cache, withRanges),
|
|
321
|
+
),
|
|
322
|
+
};
|
|
323
|
+
cache.set(node, projected);
|
|
324
|
+
return projected;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function projectBlockNode(
|
|
328
|
+
node: MarkdownAstBlockContent<RuntimeExtensionNode>,
|
|
329
|
+
cache: LegacyProjectionCache,
|
|
330
|
+
withRanges: boolean,
|
|
331
|
+
): RuntimeBlockNode {
|
|
332
|
+
const cached = cache.get(node);
|
|
333
|
+
if (cached != null) {
|
|
334
|
+
return cached as RuntimeBlockNode;
|
|
335
|
+
}
|
|
336
|
+
const metadata = withRanges ? projectRange(node.position) : {};
|
|
337
|
+
let projected: RuntimeBlockNode;
|
|
338
|
+
switch (node.type) {
|
|
339
|
+
case 'heading':
|
|
340
|
+
projected = {
|
|
341
|
+
type: 'heading',
|
|
342
|
+
level: node.depth,
|
|
343
|
+
children: node.children.map(child =>
|
|
344
|
+
projectInlineNode(child, cache, withRanges),
|
|
345
|
+
),
|
|
346
|
+
...metadata,
|
|
347
|
+
};
|
|
348
|
+
break;
|
|
349
|
+
case 'paragraph':
|
|
350
|
+
projected = {
|
|
351
|
+
type: 'paragraph',
|
|
352
|
+
children: node.children.map(child =>
|
|
353
|
+
projectInlineNode(child, cache, withRanges),
|
|
354
|
+
),
|
|
355
|
+
...metadata,
|
|
356
|
+
};
|
|
357
|
+
break;
|
|
358
|
+
case 'code':
|
|
359
|
+
projected = {
|
|
360
|
+
type: 'codeblock',
|
|
361
|
+
language: getMarkdownAstLegacyCodeLanguage(node) ?? 'plaintext',
|
|
362
|
+
content: node.value,
|
|
363
|
+
...metadata,
|
|
364
|
+
};
|
|
365
|
+
break;
|
|
366
|
+
case 'math':
|
|
367
|
+
projected = {type: 'math', value: node.value, ...metadata};
|
|
368
|
+
break;
|
|
369
|
+
case 'blockquote':
|
|
370
|
+
projected = {
|
|
371
|
+
type: 'blockquote',
|
|
372
|
+
children: node.children.map(child =>
|
|
373
|
+
projectBlockNode(child, cache, withRanges),
|
|
374
|
+
),
|
|
375
|
+
...metadata,
|
|
376
|
+
};
|
|
377
|
+
break;
|
|
378
|
+
case 'list':
|
|
379
|
+
projected = {
|
|
380
|
+
type: 'list',
|
|
381
|
+
ordered: node.ordered,
|
|
382
|
+
start: node.start,
|
|
383
|
+
delimiter: node.delimiter,
|
|
384
|
+
loose: node.spread,
|
|
385
|
+
items: node.children.map(item =>
|
|
386
|
+
projectListItem(item, cache, withRanges),
|
|
387
|
+
),
|
|
388
|
+
...metadata,
|
|
389
|
+
};
|
|
390
|
+
break;
|
|
391
|
+
case 'table': {
|
|
392
|
+
const [header = {type: 'tableRow' as const, children: []}, ...rows] =
|
|
393
|
+
node.children;
|
|
394
|
+
projected = {
|
|
395
|
+
type: 'table',
|
|
396
|
+
headers: header.children.map(cell =>
|
|
397
|
+
projectTableCell(cell, cache, withRanges),
|
|
398
|
+
),
|
|
399
|
+
alignments: [...node.align],
|
|
400
|
+
rows: rows.map(row =>
|
|
401
|
+
row.children.map(cell => projectTableCell(cell, cache, withRanges)),
|
|
402
|
+
),
|
|
403
|
+
...metadata,
|
|
404
|
+
};
|
|
405
|
+
break;
|
|
406
|
+
}
|
|
407
|
+
case 'thematicBreak':
|
|
408
|
+
projected = {type: 'hr', ...metadata};
|
|
409
|
+
break;
|
|
410
|
+
case 'image':
|
|
411
|
+
projected = {type: 'image', alt: node.alt, src: node.url, ...metadata};
|
|
412
|
+
break;
|
|
413
|
+
case 'extension':
|
|
414
|
+
projected = projectExtensionNode(node, withRanges);
|
|
415
|
+
break;
|
|
416
|
+
}
|
|
417
|
+
cache.set(node, projected);
|
|
418
|
+
return projected;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
function projectInlineNodes(
|
|
422
|
+
nodes: ReadonlyArray<MarkdownAstPhrasingContent<RuntimeExtensionNode>>,
|
|
423
|
+
withRanges: boolean,
|
|
424
|
+
cache: LegacyProjectionCache = new WeakMap(),
|
|
425
|
+
): RuntimeInlineNode[] {
|
|
426
|
+
return nodes.map(node => projectInlineNode(node, cache, withRanges));
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
function projectMarkdownRoot(
|
|
430
|
+
root: MarkdownAstRoot<RuntimeExtensionNode>,
|
|
431
|
+
withRanges: boolean,
|
|
432
|
+
cache: LegacyProjectionCache = new WeakMap(),
|
|
433
|
+
): RuntimeBlockNode[] {
|
|
434
|
+
return root.children.map(node => projectBlockNode(node, cache, withRanges));
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/** Whether a caller asked for the released `range` field on its blocks. */
|
|
438
|
+
function wantsLegacyRanges(
|
|
439
|
+
arg: ReadonlySet<string> | RuntimeParseOptions | undefined,
|
|
440
|
+
): boolean {
|
|
441
|
+
return (
|
|
442
|
+
arg != null &&
|
|
443
|
+
typeof (arg as {has?: unknown}).has !== 'function' &&
|
|
444
|
+
(arg as RuntimeParseOptions).sourceRanges === true
|
|
445
|
+
);
|
|
446
|
+
}
|
|
447
|
+
|
|
73
448
|
// ---------------------------------------------------------------------------
|
|
74
449
|
// Parse options
|
|
75
450
|
// ---------------------------------------------------------------------------
|
|
@@ -81,10 +456,14 @@ export type TableAlignment = 'left' | 'center' | 'right' | null;
|
|
|
81
456
|
* `parseMarkdownIncremental` functions also accept the legacy
|
|
82
457
|
* `ReadonlySet<string>` shape as the second argument.
|
|
83
458
|
*/
|
|
84
|
-
|
|
459
|
+
type CommonParseOptions<
|
|
460
|
+
Plugins extends ReadonlyArray<MarkdownPluginEntry> = readonly [],
|
|
461
|
+
> = {
|
|
85
462
|
/** Set of citation source ids — `[id]` / `【id】` markers in this set
|
|
86
463
|
* become citation nodes instead of plain text / links. */
|
|
87
464
|
sourceIds?: ReadonlySet<string>;
|
|
465
|
+
/** Ordered opt-in Markdown extensions. */
|
|
466
|
+
plugins?: Plugins;
|
|
88
467
|
/**
|
|
89
468
|
* Autolink mode. When set to `'gfm'`, the parser turns bare
|
|
90
469
|
* `https?://…` / `www.…` URLs, `<URL>` / `<email>` angle-bracket
|
|
@@ -109,10 +488,57 @@ export type ParseOptions = {
|
|
|
109
488
|
sourceRanges?: boolean;
|
|
110
489
|
};
|
|
111
490
|
|
|
491
|
+
/** Options for default and legacy parser results. */
|
|
492
|
+
export type ParseOptions<
|
|
493
|
+
Plugins extends ReadonlyArray<MarkdownPluginEntry> = readonly [],
|
|
494
|
+
> = CommonParseOptions<Plugins> & {math?: false | undefined};
|
|
495
|
+
|
|
496
|
+
/**
|
|
497
|
+
* Options that explicitly parse `$…$` and `$$…$$` into math-enabled result
|
|
498
|
+
* unions. Keeping this separate prevents a legacy `ParseOptions` annotation
|
|
499
|
+
* from silently widening an exhaustive node switch.
|
|
500
|
+
*/
|
|
501
|
+
export type MathParseOptions<
|
|
502
|
+
Plugins extends ReadonlyArray<MarkdownPluginEntry> = readonly [],
|
|
503
|
+
> = CommonParseOptions<Plugins> & {math: true};
|
|
504
|
+
|
|
505
|
+
export type IncrementalParseOptions<
|
|
506
|
+
Plugins extends ReadonlyArray<MarkdownPluginEntry> = readonly [],
|
|
507
|
+
> = ParseOptions<Plugins> & {
|
|
508
|
+
/** False while more source may arrive; true for the terminal snapshot. */
|
|
509
|
+
readonly isFinal?: boolean;
|
|
510
|
+
};
|
|
511
|
+
|
|
512
|
+
export type IncrementalMathParseOptions<
|
|
513
|
+
Plugins extends ReadonlyArray<MarkdownPluginEntry> = readonly [],
|
|
514
|
+
> = MathParseOptions<Plugins> & {
|
|
515
|
+
/** False while more source may arrive; true for the terminal snapshot. */
|
|
516
|
+
readonly isFinal?: boolean;
|
|
517
|
+
};
|
|
518
|
+
|
|
519
|
+
type RuntimeParseOptions = CommonParseOptions<
|
|
520
|
+
ReadonlyArray<MarkdownPluginEntry>
|
|
521
|
+
> & {
|
|
522
|
+
math?: boolean;
|
|
523
|
+
isFinal?: boolean;
|
|
524
|
+
};
|
|
525
|
+
|
|
112
526
|
type ResolvedOptions = {
|
|
113
527
|
readonly sourceIds: ReadonlySet<string> | undefined;
|
|
114
528
|
readonly autolink: 'gfm' | undefined;
|
|
529
|
+
readonly math?: boolean;
|
|
530
|
+
/** Whether the caller asked for the released `range` projection. */
|
|
115
531
|
readonly sourceRanges?: boolean;
|
|
532
|
+
/**
|
|
533
|
+
* Whether this parse authors canonical `position` offsets on its blocks.
|
|
534
|
+
* Internal only, and independent of `sourceRanges`: the canonical tree
|
|
535
|
+
* transforms observe always carries provenance, while `sourceRanges` alone
|
|
536
|
+
* decides whether the released projection exposes it as `range`.
|
|
537
|
+
*/
|
|
538
|
+
readonly astPositions?: boolean;
|
|
539
|
+
readonly plugins?: PreparedMarkdownPlugins;
|
|
540
|
+
readonly isFinal: boolean;
|
|
541
|
+
readonly allowBlockSyntax?: boolean;
|
|
116
542
|
/**
|
|
117
543
|
* Offset of this parse's input within the document the ranges are reported
|
|
118
544
|
* against. Internal only — the incremental parser parses slices and needs
|
|
@@ -128,13 +554,42 @@ type ResolvedOptions = {
|
|
|
128
554
|
readonly linkDefs?: ReadonlyMap<string, string>;
|
|
129
555
|
};
|
|
130
556
|
|
|
131
|
-
|
|
557
|
+
/**
|
|
558
|
+
* Every resolved options object is built here, so all of them share one key
|
|
559
|
+
* set in one order — and therefore one hidden class. The parser reads these
|
|
560
|
+
* fields in its hottest loops; letting an omitted-options parse and an
|
|
561
|
+
* options-bag parse produce differently-shaped objects makes those reads
|
|
562
|
+
* polymorphic in any process that does both, which is exactly what a caller
|
|
563
|
+
* comparing an empty pipeline against a configured one does.
|
|
564
|
+
*/
|
|
565
|
+
function makeResolvedOptions(
|
|
566
|
+
fields: Partial<ResolvedOptions> & {readonly isFinal: boolean},
|
|
567
|
+
): ResolvedOptions {
|
|
568
|
+
return {
|
|
569
|
+
sourceIds: fields.sourceIds,
|
|
570
|
+
autolink: fields.autolink,
|
|
571
|
+
math: fields.math,
|
|
572
|
+
sourceRanges: fields.sourceRanges,
|
|
573
|
+
astPositions: fields.astPositions ?? true,
|
|
574
|
+
plugins: fields.plugins,
|
|
575
|
+
isFinal: fields.isFinal,
|
|
576
|
+
allowBlockSyntax: fields.allowBlockSyntax ?? true,
|
|
577
|
+
baseOffset: fields.baseOffset,
|
|
578
|
+
linkDefs: fields.linkDefs,
|
|
579
|
+
};
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
const EMPTY_OPTS: ResolvedOptions = makeResolvedOptions({isFinal: true});
|
|
583
|
+
const EMPTY_INCREMENTAL_OPTS: ResolvedOptions = makeResolvedOptions({
|
|
584
|
+
isFinal: false,
|
|
585
|
+
});
|
|
132
586
|
|
|
133
587
|
function resolveOptions(
|
|
134
|
-
arg: ReadonlySet<string> |
|
|
588
|
+
arg: ReadonlySet<string> | RuntimeParseOptions | undefined,
|
|
589
|
+
incremental = false,
|
|
135
590
|
): ResolvedOptions {
|
|
136
591
|
if (arg == null) {
|
|
137
|
-
return EMPTY_OPTS;
|
|
592
|
+
return incremental ? EMPTY_INCREMENTAL_OPTS : EMPTY_OPTS;
|
|
138
593
|
}
|
|
139
594
|
// Duck-type the legacy `ReadonlySet<string>` form: any object whose
|
|
140
595
|
// `.has` is callable is treated as the legacy sourceIds set. This is
|
|
@@ -142,14 +597,23 @@ function resolveOptions(
|
|
|
142
597
|
// or polyfilled `ReadonlySet` implementations as a `ParseOptions` bag
|
|
143
598
|
// and silently lose citation resolution.
|
|
144
599
|
if (typeof (arg as {has?: unknown}).has === 'function') {
|
|
145
|
-
return {
|
|
600
|
+
return makeResolvedOptions({
|
|
601
|
+
sourceIds: arg as ReadonlySet<string>,
|
|
602
|
+
isFinal: !incremental,
|
|
603
|
+
});
|
|
146
604
|
}
|
|
147
|
-
const opts = arg as
|
|
148
|
-
return {
|
|
605
|
+
const opts = arg as RuntimeParseOptions;
|
|
606
|
+
return makeResolvedOptions({
|
|
149
607
|
sourceIds: opts.sourceIds,
|
|
150
608
|
autolink: opts.autolink,
|
|
609
|
+
math: opts.math === true ? true : undefined,
|
|
151
610
|
sourceRanges: opts.sourceRanges,
|
|
152
|
-
|
|
611
|
+
plugins:
|
|
612
|
+
opts.plugins != null && opts.plugins.length > 0
|
|
613
|
+
? prepareMarkdownPlugins(opts.plugins)
|
|
614
|
+
: undefined,
|
|
615
|
+
isFinal: incremental ? opts.isFinal === true : true,
|
|
616
|
+
});
|
|
153
617
|
}
|
|
154
618
|
|
|
155
619
|
// ---------------------------------------------------------------------------
|
|
@@ -175,6 +639,149 @@ function normalizeLinkLabel(label: string): string {
|
|
|
175
639
|
return label.trim().replace(/\s+/g, ' ').toLowerCase();
|
|
176
640
|
}
|
|
177
641
|
|
|
642
|
+
type DisplayMathContainer = {
|
|
643
|
+
outerQuoteDepth: number;
|
|
644
|
+
listBaseIndent: number | null;
|
|
645
|
+
innerQuoteDepth: number;
|
|
646
|
+
};
|
|
647
|
+
|
|
648
|
+
type DisplayMathMatch = {
|
|
649
|
+
value: string;
|
|
650
|
+
nextIndex: number;
|
|
651
|
+
endLine: number;
|
|
652
|
+
};
|
|
653
|
+
|
|
654
|
+
function stripBlockquoteMarkers(line: string): {
|
|
655
|
+
content: string;
|
|
656
|
+
quoteDepth: number;
|
|
657
|
+
} {
|
|
658
|
+
let content = line.endsWith('\r') ? line.slice(0, -1) : line;
|
|
659
|
+
let quoteDepth = 0;
|
|
660
|
+
while (true) {
|
|
661
|
+
const marker = /^ {0,3}> ?/.exec(content);
|
|
662
|
+
if (marker == null) {
|
|
663
|
+
return {content, quoteDepth};
|
|
664
|
+
}
|
|
665
|
+
content = content.slice(marker[0].length);
|
|
666
|
+
quoteDepth++;
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
function stripExactBlockquoteDepth(
|
|
671
|
+
line: string,
|
|
672
|
+
quoteDepth: number,
|
|
673
|
+
): string | null {
|
|
674
|
+
let content = line;
|
|
675
|
+
for (let depth = 0; depth < quoteDepth; depth++) {
|
|
676
|
+
const marker = /^ {0,3}> ?/.exec(content);
|
|
677
|
+
if (marker == null) {
|
|
678
|
+
return null;
|
|
679
|
+
}
|
|
680
|
+
content = content.slice(marker[0].length);
|
|
681
|
+
}
|
|
682
|
+
return content;
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
/**
|
|
686
|
+
* Recognize a standalone display-math marker at the current container boundary.
|
|
687
|
+
* A list opener carries its base indent so an indented continuation marker can
|
|
688
|
+
* close it; blockquotes must keep the same quote depth.
|
|
689
|
+
*/
|
|
690
|
+
function displayMathContainer(line: string): DisplayMathContainer | null {
|
|
691
|
+
const outer = stripBlockquoteMarkers(line);
|
|
692
|
+
const listMarker = /^( {0,9})(?:[-*+]|\d+[.)]) +(.*)$/.exec(outer.content);
|
|
693
|
+
if (listMarker != null) {
|
|
694
|
+
const taskMarker = /^\[[ xX]\] +(.*)$/.exec(listMarker[2]);
|
|
695
|
+
const inner = stripBlockquoteMarkers(taskMarker?.[1] ?? listMarker[2]);
|
|
696
|
+
return inner.content.trim() === '$$'
|
|
697
|
+
? {
|
|
698
|
+
outerQuoteDepth: outer.quoteDepth,
|
|
699
|
+
listBaseIndent: listMarker[1].length,
|
|
700
|
+
innerQuoteDepth: inner.quoteDepth,
|
|
701
|
+
}
|
|
702
|
+
: null;
|
|
703
|
+
}
|
|
704
|
+
return outer.content.trim() === '$$'
|
|
705
|
+
? {
|
|
706
|
+
outerQuoteDepth: outer.quoteDepth,
|
|
707
|
+
listBaseIndent: null,
|
|
708
|
+
innerQuoteDepth: 0,
|
|
709
|
+
}
|
|
710
|
+
: null;
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
type DisplayMathLineState = 'close' | 'inside' | 'outside';
|
|
714
|
+
|
|
715
|
+
function displayMathLineState(
|
|
716
|
+
line: string,
|
|
717
|
+
container: DisplayMathContainer,
|
|
718
|
+
): DisplayMathLineState {
|
|
719
|
+
const outerContent = stripExactBlockquoteDepth(
|
|
720
|
+
line,
|
|
721
|
+
container.outerQuoteDepth,
|
|
722
|
+
);
|
|
723
|
+
if (outerContent == null) {
|
|
724
|
+
return 'outside';
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
if (container.listBaseIndent == null) {
|
|
728
|
+
// A deeper quote starts a different container. It cannot close or continue
|
|
729
|
+
// the math expression owned by the shallower quote.
|
|
730
|
+
if (/^ {0,3}> ?/.test(outerContent)) {
|
|
731
|
+
return 'outside';
|
|
732
|
+
}
|
|
733
|
+
return outerContent.trim() === '$$' ? 'close' : 'inside';
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
const continuationIndent =
|
|
737
|
+
outerContent.length - outerContent.trimStart().length;
|
|
738
|
+
if (continuationIndent <= container.listBaseIndent) {
|
|
739
|
+
return 'outside';
|
|
740
|
+
}
|
|
741
|
+
const innerContent = stripExactBlockquoteDepth(
|
|
742
|
+
outerContent.trimStart(),
|
|
743
|
+
container.innerQuoteDepth,
|
|
744
|
+
);
|
|
745
|
+
if (innerContent == null || /^ {0,3}> ?/.test(innerContent)) {
|
|
746
|
+
return 'outside';
|
|
747
|
+
}
|
|
748
|
+
return innerContent.trim() === '$$' ? 'close' : 'inside';
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
/** Match a complete `$$…$$` display-math block without consuming partial input. */
|
|
752
|
+
function matchDisplayMathBlock(
|
|
753
|
+
lines: string[],
|
|
754
|
+
lineIndex: number,
|
|
755
|
+
): DisplayMathMatch | null {
|
|
756
|
+
const trimmed = lines[lineIndex].trim();
|
|
757
|
+
if (
|
|
758
|
+
trimmed.length > 4 &&
|
|
759
|
+
trimmed.startsWith('$$') &&
|
|
760
|
+
trimmed.endsWith('$$')
|
|
761
|
+
) {
|
|
762
|
+
const value = trimmed.slice(2, -2);
|
|
763
|
+
return value.trim() === ''
|
|
764
|
+
? null
|
|
765
|
+
: {value, nextIndex: lineIndex + 1, endLine: lineIndex};
|
|
766
|
+
}
|
|
767
|
+
if (trimmed !== '$$') {
|
|
768
|
+
return null;
|
|
769
|
+
}
|
|
770
|
+
for (let index = lineIndex + 1; index < lines.length; index++) {
|
|
771
|
+
if (lines[index].trim() === '$$') {
|
|
772
|
+
const value = lines.slice(lineIndex + 1, index).join('\n');
|
|
773
|
+
return value.trim() === ''
|
|
774
|
+
? null
|
|
775
|
+
: {
|
|
776
|
+
value,
|
|
777
|
+
nextIndex: index + 1,
|
|
778
|
+
endLine: index,
|
|
779
|
+
};
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
return null;
|
|
783
|
+
}
|
|
784
|
+
|
|
178
785
|
function matchLinkDefinition(
|
|
179
786
|
line: string,
|
|
180
787
|
): {label: string; destination: string; hasTitle: boolean} | null {
|
|
@@ -211,7 +818,10 @@ function matchLinkDefinition(
|
|
|
211
818
|
* where every definition is global. Separating a footer definition block with
|
|
212
819
|
* a blank line — the usual form — always works.
|
|
213
820
|
*/
|
|
214
|
-
function extractLinkDefinitions(
|
|
821
|
+
function extractLinkDefinitions(
|
|
822
|
+
input: string,
|
|
823
|
+
math = false,
|
|
824
|
+
): {
|
|
215
825
|
defs: ReadonlyMap<string, string>;
|
|
216
826
|
cleaned: string;
|
|
217
827
|
/**
|
|
@@ -240,6 +850,16 @@ function extractLinkDefinitions(input: string): {
|
|
|
240
850
|
}
|
|
241
851
|
continue;
|
|
242
852
|
}
|
|
853
|
+
if (math) {
|
|
854
|
+
const displayMath = matchDisplayMathBlock(lines, index);
|
|
855
|
+
if (displayMath != null) {
|
|
856
|
+
// Math is opaque Markdown content: definition-shaped TeX must not leak
|
|
857
|
+
// into the document-wide link-definition map.
|
|
858
|
+
index = displayMath.endLine;
|
|
859
|
+
atBoundary = true;
|
|
860
|
+
continue;
|
|
861
|
+
}
|
|
862
|
+
}
|
|
243
863
|
const fenceMatch = line.match(/^(`{3,}|~{3,})/);
|
|
244
864
|
if (fenceMatch) {
|
|
245
865
|
inFence = true;
|
|
@@ -290,6 +910,15 @@ function extractLinkDefinitions(input: string): {
|
|
|
290
910
|
return {defs, cleaned, lineMap};
|
|
291
911
|
}
|
|
292
912
|
|
|
913
|
+
/** Order-independent signature of a citation-source set, for cache checks. */
|
|
914
|
+
function sourceIdsSignature(
|
|
915
|
+
sourceIds: ReadonlySet<string> | undefined,
|
|
916
|
+
): string {
|
|
917
|
+
return sourceIds == null || sourceIds.size === 0
|
|
918
|
+
? ''
|
|
919
|
+
: [...sourceIds].sort().join('\u0000');
|
|
920
|
+
}
|
|
921
|
+
|
|
293
922
|
/** Order-independent signature of a link-definition set, for cache checks. */
|
|
294
923
|
function linkDefsSignature(defs: ReadonlyMap<string, string>): string {
|
|
295
924
|
if (defs.size === 0) {
|
|
@@ -307,12 +936,19 @@ function linkDefsSignature(defs: ReadonlyMap<string, string>): string {
|
|
|
307
936
|
* Returns the node plus the index just past the reference, or null when it is
|
|
308
937
|
* not a resolvable reference (caller falls through to literal handling).
|
|
309
938
|
*/
|
|
939
|
+
function protectedInlineOptions(opts: ResolvedOptions): ResolvedOptions {
|
|
940
|
+
return opts.plugins == null ? opts : {...opts, plugins: undefined};
|
|
941
|
+
}
|
|
942
|
+
|
|
310
943
|
function matchReferenceLink(
|
|
311
944
|
text: string,
|
|
312
945
|
start: number,
|
|
313
946
|
linkDefs: ReadonlyMap<string, string>,
|
|
314
947
|
opts: ResolvedOptions,
|
|
315
|
-
): {
|
|
948
|
+
): {
|
|
949
|
+
node: MarkdownAstPhrasingContent<RuntimeExtensionNode>;
|
|
950
|
+
end: number;
|
|
951
|
+
} | null {
|
|
316
952
|
const textClose = text.indexOf(']', start + 1);
|
|
317
953
|
if (textClose === -1) {
|
|
318
954
|
return null;
|
|
@@ -328,9 +964,13 @@ function matchReferenceLink(
|
|
|
328
964
|
// matches nothing.
|
|
329
965
|
const label = rawLabel === '' ? linkText : rawLabel;
|
|
330
966
|
const href = linkDefs.get(normalizeLinkLabel(label));
|
|
331
|
-
if (href != null &&
|
|
967
|
+
if (href != null && isSafeMarkdownParserUrl(href)) {
|
|
332
968
|
return {
|
|
333
|
-
node: {
|
|
969
|
+
node: {
|
|
970
|
+
type: 'link',
|
|
971
|
+
url: href,
|
|
972
|
+
children: parseInlineImpl(linkText, protectedInlineOptions(opts)),
|
|
973
|
+
},
|
|
334
974
|
end: labelClose + 1,
|
|
335
975
|
};
|
|
336
976
|
}
|
|
@@ -343,11 +983,15 @@ function matchReferenceLink(
|
|
|
343
983
|
return null;
|
|
344
984
|
}
|
|
345
985
|
const href = linkDefs.get(normalizeLinkLabel(linkText));
|
|
346
|
-
if (href == null || !
|
|
986
|
+
if (href == null || !isSafeMarkdownParserUrl(href)) {
|
|
347
987
|
return null;
|
|
348
988
|
}
|
|
349
989
|
return {
|
|
350
|
-
node: {
|
|
990
|
+
node: {
|
|
991
|
+
type: 'link',
|
|
992
|
+
url: href,
|
|
993
|
+
children: parseInlineImpl(linkText, protectedInlineOptions(opts)),
|
|
994
|
+
},
|
|
351
995
|
end: textClose + 1,
|
|
352
996
|
};
|
|
353
997
|
}
|
|
@@ -357,7 +1001,10 @@ function matchReferenceImage(
|
|
|
357
1001
|
text: string,
|
|
358
1002
|
start: number,
|
|
359
1003
|
linkDefs: ReadonlyMap<string, string>,
|
|
360
|
-
): {
|
|
1004
|
+
): {
|
|
1005
|
+
node: MarkdownAstPhrasingContent<RuntimeExtensionNode>;
|
|
1006
|
+
end: number;
|
|
1007
|
+
} | null {
|
|
361
1008
|
const altClose = text.indexOf(']', start + 2);
|
|
362
1009
|
if (altClose === -1) {
|
|
363
1010
|
return null;
|
|
@@ -369,8 +1016,8 @@ function matchReferenceImage(
|
|
|
369
1016
|
const rawLabel = text.slice(altClose + 2, labelClose);
|
|
370
1017
|
const label = rawLabel === '' ? alt : rawLabel;
|
|
371
1018
|
const src = linkDefs.get(normalizeLinkLabel(label));
|
|
372
|
-
if (src != null &&
|
|
373
|
-
return {node: {type: 'image', src, alt}, end: labelClose + 1};
|
|
1019
|
+
if (src != null && isSafeMarkdownParserUrl(src)) {
|
|
1020
|
+
return {node: {type: 'image', url: src, alt}, end: labelClose + 1};
|
|
374
1021
|
}
|
|
375
1022
|
// No match — fall back to a shortcut `![alt]`.
|
|
376
1023
|
}
|
|
@@ -379,10 +1026,10 @@ function matchReferenceImage(
|
|
|
379
1026
|
return null;
|
|
380
1027
|
}
|
|
381
1028
|
const src = linkDefs.get(normalizeLinkLabel(alt));
|
|
382
|
-
if (src == null || !
|
|
1029
|
+
if (src == null || !isSafeMarkdownParserUrl(src)) {
|
|
383
1030
|
return null;
|
|
384
1031
|
}
|
|
385
|
-
return {node: {type: 'image', src, alt}, end: altClose + 1};
|
|
1032
|
+
return {node: {type: 'image', url: src, alt}, end: altClose + 1};
|
|
386
1033
|
}
|
|
387
1034
|
|
|
388
1035
|
// ---------------------------------------------------------------------------
|
|
@@ -412,31 +1059,65 @@ function isWordChar(ch: string | undefined): boolean {
|
|
|
412
1059
|
return /\w/.test(ch);
|
|
413
1060
|
}
|
|
414
1061
|
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
1062
|
+
/** True when the character at `index` is preceded by an odd backslash run. */
|
|
1063
|
+
function isEscaped(text: string, index: number): boolean {
|
|
1064
|
+
let backslashes = 0;
|
|
1065
|
+
for (let i = index - 1; i >= 0 && text[i] === '\\'; i--) {
|
|
1066
|
+
backslashes++;
|
|
1067
|
+
}
|
|
1068
|
+
return backslashes % 2 === 1;
|
|
1069
|
+
}
|
|
418
1070
|
|
|
419
1071
|
/**
|
|
420
|
-
*
|
|
421
|
-
*
|
|
422
|
-
*
|
|
1072
|
+
* Find the closing delimiter for `$…$` math on the same line.
|
|
1073
|
+
*
|
|
1074
|
+
* The whitespace and numeric-edge rules mirror common dollar-math parsers:
|
|
1075
|
+
* whitespace cannot hug the delimiters, a digit cannot sit immediately before
|
|
1076
|
+
* the opener or after the closer, and `$$` is reserved for display math. The
|
|
1077
|
+
* numeric guard prevents ordinary prose such as "$20 and $30" from becoming a
|
|
1078
|
+
* formula even in a math-enabled document.
|
|
423
1079
|
*/
|
|
424
|
-
function
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
1080
|
+
function isInlineMathStart(text: string, index: number): boolean {
|
|
1081
|
+
return (
|
|
1082
|
+
text[index] === '$' &&
|
|
1083
|
+
text[index - 1] !== '$' &&
|
|
1084
|
+
text[index + 1] !== '$' &&
|
|
1085
|
+
text[index + 1] != null &&
|
|
1086
|
+
!/\s/.test(text[index + 1]) &&
|
|
1087
|
+
!/\d/.test(text[index - 1] ?? '') &&
|
|
1088
|
+
!isEscaped(text, index)
|
|
1089
|
+
);
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
function findInlineMathEnd(text: string, start: number): number {
|
|
1093
|
+
if (!isInlineMathStart(text, start)) {
|
|
1094
|
+
return -1;
|
|
436
1095
|
}
|
|
437
|
-
|
|
1096
|
+
|
|
1097
|
+
for (let index = start + 1; index < text.length; index++) {
|
|
1098
|
+
if (text[index] === '\n') {
|
|
1099
|
+
return -1;
|
|
1100
|
+
}
|
|
1101
|
+
if (text[index] !== '$' || isEscaped(text, index)) {
|
|
1102
|
+
continue;
|
|
1103
|
+
}
|
|
1104
|
+
// An unescaped dollar ends this candidate: it either forms a valid closer
|
|
1105
|
+
// or makes the whole span literal. Never skip over one and pair with a
|
|
1106
|
+
// later dollar, which would swallow currency or another expression.
|
|
1107
|
+
if (
|
|
1108
|
+
text[index - 1] === '$' ||
|
|
1109
|
+
text[index + 1] === '$' ||
|
|
1110
|
+
/\s/.test(text[index - 1]) ||
|
|
1111
|
+
/\d/.test(text[index + 1] ?? '')
|
|
1112
|
+
) {
|
|
1113
|
+
return -1;
|
|
1114
|
+
}
|
|
1115
|
+
return index;
|
|
1116
|
+
}
|
|
1117
|
+
return -1;
|
|
438
1118
|
}
|
|
439
1119
|
|
|
1120
|
+
// ---------------------------------------------------------------------------
|
|
440
1121
|
// ---------------------------------------------------------------------------
|
|
441
1122
|
// Inline parser
|
|
442
1123
|
// ---------------------------------------------------------------------------
|
|
@@ -490,16 +1171,246 @@ function matchBracketCitation(
|
|
|
490
1171
|
return {sourceId: id, end: closeIndex + 1};
|
|
491
1172
|
}
|
|
492
1173
|
|
|
1174
|
+
type ExtensionMatch =
|
|
1175
|
+
| {readonly status: 'none'}
|
|
1176
|
+
| {readonly status: 'defer'}
|
|
1177
|
+
| {
|
|
1178
|
+
readonly status: 'match';
|
|
1179
|
+
readonly node: RuntimeExtensionNode;
|
|
1180
|
+
readonly end: number;
|
|
1181
|
+
};
|
|
1182
|
+
|
|
1183
|
+
function currentLineStart(source: string, offset: number): number {
|
|
1184
|
+
return source.lastIndexOf('\n', offset - 1) + 1;
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
function matchExtensionSyntax(
|
|
1188
|
+
source: string,
|
|
1189
|
+
offset: number,
|
|
1190
|
+
context: 'inline' | 'block',
|
|
1191
|
+
opts: ResolvedOptions,
|
|
1192
|
+
): ExtensionMatch {
|
|
1193
|
+
const byFirstCharacter =
|
|
1194
|
+
context === 'inline'
|
|
1195
|
+
? opts.plugins?.inlineByFirstCharacter
|
|
1196
|
+
: opts.allowBlockSyntax === false
|
|
1197
|
+
? undefined
|
|
1198
|
+
: opts.plugins?.blockByFirstCharacter;
|
|
1199
|
+
const candidates = byFirstCharacter?.get(source[offset]);
|
|
1200
|
+
if (candidates == null) {
|
|
1201
|
+
return {status: 'none'};
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
const seen = new Set<PreparedSyntaxContribution>();
|
|
1205
|
+
for (const candidate of candidates) {
|
|
1206
|
+
if (seen.has(candidate)) {
|
|
1207
|
+
continue;
|
|
1208
|
+
}
|
|
1209
|
+
seen.add(candidate);
|
|
1210
|
+
let fullPrefix = false;
|
|
1211
|
+
for (const prefix of candidate.contribution.startsWith) {
|
|
1212
|
+
if (source.startsWith(prefix, offset)) {
|
|
1213
|
+
fullPrefix = true;
|
|
1214
|
+
break;
|
|
1215
|
+
}
|
|
1216
|
+
if (!opts.isFinal) {
|
|
1217
|
+
const remaining = source.slice(offset);
|
|
1218
|
+
if (remaining.length < prefix.length && prefix.startsWith(remaining)) {
|
|
1219
|
+
return {status: 'defer'};
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
}
|
|
1223
|
+
if (!fullPrefix) {
|
|
1224
|
+
continue;
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
const end = Math.min(
|
|
1228
|
+
source.length,
|
|
1229
|
+
offset + candidate.contribution.maxSpan,
|
|
1230
|
+
);
|
|
1231
|
+
let result: ReturnType<typeof candidate.contribution.tokenize>;
|
|
1232
|
+
try {
|
|
1233
|
+
result = candidate.contribution.tokenize({
|
|
1234
|
+
source: end === source.length ? source : source.slice(0, end),
|
|
1235
|
+
offset,
|
|
1236
|
+
end,
|
|
1237
|
+
isFinal: opts.isFinal,
|
|
1238
|
+
context,
|
|
1239
|
+
lineStart: currentLineStart(source, offset),
|
|
1240
|
+
column: offset - currentLineStart(source, offset),
|
|
1241
|
+
});
|
|
1242
|
+
} catch (error) {
|
|
1243
|
+
reportMarkdownPluginFailure(candidate.pluginName, 'syntax', error);
|
|
1244
|
+
continue;
|
|
1245
|
+
}
|
|
1246
|
+
if (
|
|
1247
|
+
result == null ||
|
|
1248
|
+
typeof result !== 'object' ||
|
|
1249
|
+
typeof (result as {then?: unknown}).then === 'function'
|
|
1250
|
+
) {
|
|
1251
|
+
reportMarkdownPluginFailure(
|
|
1252
|
+
candidate.pluginName,
|
|
1253
|
+
'syntax',
|
|
1254
|
+
new TypeError('Tokenizer returned an invalid result'),
|
|
1255
|
+
);
|
|
1256
|
+
continue;
|
|
1257
|
+
}
|
|
1258
|
+
if (result.status === 'no-match') {
|
|
1259
|
+
continue;
|
|
1260
|
+
}
|
|
1261
|
+
if (result.status === 'defer') {
|
|
1262
|
+
return !opts.isFinal &&
|
|
1263
|
+
end === source.length &&
|
|
1264
|
+
source.length - offset < candidate.contribution.maxSpan
|
|
1265
|
+
? {status: 'defer'}
|
|
1266
|
+
: {status: 'none'};
|
|
1267
|
+
}
|
|
1268
|
+
if (
|
|
1269
|
+
result.status !== 'match' ||
|
|
1270
|
+
!Number.isInteger(result.end) ||
|
|
1271
|
+
result.end <= offset ||
|
|
1272
|
+
result.end > end ||
|
|
1273
|
+
result.node.type !== 'extension' ||
|
|
1274
|
+
result.node.plugin !== candidate.pluginName ||
|
|
1275
|
+
result.node.display !== context ||
|
|
1276
|
+
result.node.name.trim() === '' ||
|
|
1277
|
+
!isMarkdownPluginData(result.node.data) ||
|
|
1278
|
+
!opts.plugins?.renderers.has(
|
|
1279
|
+
`${candidate.pluginName}\0${result.node.name}`,
|
|
1280
|
+
)
|
|
1281
|
+
) {
|
|
1282
|
+
reportMarkdownPluginFailure(
|
|
1283
|
+
candidate.pluginName,
|
|
1284
|
+
'syntax',
|
|
1285
|
+
new TypeError('Tokenizer returned an invalid extension node'),
|
|
1286
|
+
);
|
|
1287
|
+
continue;
|
|
1288
|
+
}
|
|
1289
|
+
const rawNode = result.node as typeof result.node & {
|
|
1290
|
+
readonly source?: unknown;
|
|
1291
|
+
readonly position?: unknown;
|
|
1292
|
+
};
|
|
1293
|
+
const {
|
|
1294
|
+
source: _ignoredSource,
|
|
1295
|
+
position: _ignoredPosition,
|
|
1296
|
+
...safeNode
|
|
1297
|
+
} = rawNode;
|
|
1298
|
+
const absoluteStart = (opts.baseOffset ?? 0) + offset;
|
|
1299
|
+
const absoluteEnd = (opts.baseOffset ?? 0) + result.end;
|
|
1300
|
+
return {
|
|
1301
|
+
status: 'match',
|
|
1302
|
+
end: result.end,
|
|
1303
|
+
node: Object.freeze({
|
|
1304
|
+
...safeNode,
|
|
1305
|
+
data: freezeMarkdownPluginData(result.node.data),
|
|
1306
|
+
source: source.slice(offset, result.end),
|
|
1307
|
+
...(opts.sourceRanges && context === 'block'
|
|
1308
|
+
? {
|
|
1309
|
+
position: Object.freeze({
|
|
1310
|
+
start: Object.freeze({offset: absoluteStart}),
|
|
1311
|
+
end: Object.freeze({offset: absoluteEnd}),
|
|
1312
|
+
}),
|
|
1313
|
+
}
|
|
1314
|
+
: null),
|
|
1315
|
+
}) as RuntimeExtensionNode,
|
|
1316
|
+
};
|
|
1317
|
+
}
|
|
1318
|
+
return {status: 'none'};
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1321
|
+
type ParseOptionsWithoutPlugins = Omit<ParseOptions, 'plugins'>;
|
|
1322
|
+
type MathParseOptionsWithoutPlugins = Omit<MathParseOptions, 'plugins'>;
|
|
1323
|
+
type IncrementalParseOptionsWithoutPlugins = Omit<
|
|
1324
|
+
IncrementalParseOptions,
|
|
1325
|
+
'plugins'
|
|
1326
|
+
>;
|
|
1327
|
+
type IncrementalMathParseOptionsWithoutPlugins = Omit<
|
|
1328
|
+
IncrementalMathParseOptions,
|
|
1329
|
+
'plugins'
|
|
1330
|
+
>;
|
|
1331
|
+
|
|
493
1332
|
export function parseInline(
|
|
494
1333
|
text: string,
|
|
495
1334
|
sourceIds?: ReadonlySet<string>,
|
|
496
1335
|
): InlineNode[];
|
|
497
|
-
export function parseInline(text: string, options: ParseOptions): InlineNode[];
|
|
498
1336
|
export function parseInline(
|
|
499
1337
|
text: string,
|
|
500
|
-
|
|
501
|
-
):
|
|
502
|
-
|
|
1338
|
+
options: MathParseOptionsWithoutPlugins,
|
|
1339
|
+
): InlineNodeWithMath[];
|
|
1340
|
+
export function parseInline(
|
|
1341
|
+
text: string,
|
|
1342
|
+
options: ParseOptionsWithoutPlugins,
|
|
1343
|
+
): InlineNode[];
|
|
1344
|
+
export function parseInline<
|
|
1345
|
+
const Plugins extends ReadonlyArray<MarkdownPluginEntry>,
|
|
1346
|
+
>(
|
|
1347
|
+
text: string,
|
|
1348
|
+
options: ParseOptionsWithoutPlugins & {plugins: Plugins},
|
|
1349
|
+
): InlineNode<MarkdownExtensionsOf<Plugins>>[];
|
|
1350
|
+
export function parseInline<
|
|
1351
|
+
const Plugins extends ReadonlyArray<MarkdownPluginEntry>,
|
|
1352
|
+
>(
|
|
1353
|
+
text: string,
|
|
1354
|
+
options: MathParseOptionsWithoutPlugins & {plugins: Plugins},
|
|
1355
|
+
): InlineNodeWithMath<MarkdownExtensionsOf<Plugins>>[];
|
|
1356
|
+
export function parseInline(
|
|
1357
|
+
text: string,
|
|
1358
|
+
arg?: ReadonlySet<string> | RuntimeParseOptions,
|
|
1359
|
+
): RuntimeInlineNode[] {
|
|
1360
|
+
return projectInlineNodes(
|
|
1361
|
+
parseInlineAstRuntime(text, arg),
|
|
1362
|
+
wantsLegacyRanges(arg),
|
|
1363
|
+
);
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1366
|
+
/**
|
|
1367
|
+
* Parse inline Markdown into the canonical immutable Astryx AST.
|
|
1368
|
+
*
|
|
1369
|
+
* Unlike `parseInline`, this returns MDAST-aligned node names and fields. Math
|
|
1370
|
+
* nodes are present only when `math: true`, and plugin extension nodes are
|
|
1371
|
+
* inferred from the ordered `plugins` tuple.
|
|
1372
|
+
*/
|
|
1373
|
+
export function parseInlineAst(
|
|
1374
|
+
text: string,
|
|
1375
|
+
sourceIds?: ReadonlySet<string>,
|
|
1376
|
+
): MarkdownAstPhrasingContent[];
|
|
1377
|
+
export function parseInlineAst(
|
|
1378
|
+
text: string,
|
|
1379
|
+
options: ParseOptionsWithoutPlugins | MathParseOptionsWithoutPlugins,
|
|
1380
|
+
): MarkdownAstPhrasingContent[];
|
|
1381
|
+
export function parseInlineAst<
|
|
1382
|
+
const Plugins extends ReadonlyArray<MarkdownPluginEntry>,
|
|
1383
|
+
>(
|
|
1384
|
+
text: string,
|
|
1385
|
+
options: (ParseOptionsWithoutPlugins | MathParseOptionsWithoutPlugins) & {
|
|
1386
|
+
plugins: Plugins;
|
|
1387
|
+
},
|
|
1388
|
+
): MarkdownAstPhrasingContent<MarkdownExtensionsOf<Plugins>>[];
|
|
1389
|
+
export function parseInlineAst(
|
|
1390
|
+
text: string,
|
|
1391
|
+
arg?: ReadonlySet<string> | RuntimeParseOptions,
|
|
1392
|
+
): MarkdownAstPhrasingContent<RuntimeExtensionNode>[] {
|
|
1393
|
+
return parseInlineAstRuntime(text, arg);
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1396
|
+
function parseInlineAstRuntime(
|
|
1397
|
+
text: string,
|
|
1398
|
+
arg?: ReadonlySet<string> | RuntimeParseOptions,
|
|
1399
|
+
): MarkdownAstPhrasingContent<RuntimeExtensionNode>[] {
|
|
1400
|
+
const opts = resolveOptions(arg);
|
|
1401
|
+
const nodes = parseInlineEntry(text, opts);
|
|
1402
|
+
if ((opts.plugins?.transforms.length ?? 0) === 0) {
|
|
1403
|
+
return nodes;
|
|
1404
|
+
}
|
|
1405
|
+
const transformed = applyMarkdownTransforms(
|
|
1406
|
+
{type: 'root', children: [{type: 'paragraph', children: nodes}]},
|
|
1407
|
+
opts.plugins,
|
|
1408
|
+
text,
|
|
1409
|
+
opts.isFinal,
|
|
1410
|
+
'inline',
|
|
1411
|
+
);
|
|
1412
|
+
const paragraph = transformed.children[0];
|
|
1413
|
+
return paragraph?.type === 'paragraph' ? [...paragraph.children] : nodes;
|
|
503
1414
|
}
|
|
504
1415
|
|
|
505
1416
|
/**
|
|
@@ -509,21 +1420,35 @@ export function parseInline(
|
|
|
509
1420
|
* wrapper and call `parseInlineImpl` directly so the transform runs only on
|
|
510
1421
|
* the outermost block's inline tree — letting `transformAutolinks` decide
|
|
511
1422
|
* which subtrees to descend into (text, bold, italic, strikethrough) and
|
|
512
|
-
* which to skip (link, code, image, citation, break).
|
|
1423
|
+
* which to skip (link, code, math, image, citation, break).
|
|
513
1424
|
*/
|
|
514
|
-
function parseInlineEntry(
|
|
1425
|
+
function parseInlineEntry(
|
|
1426
|
+
text: string,
|
|
1427
|
+
opts: ResolvedOptions,
|
|
1428
|
+
): MarkdownAstPhrasingContent<RuntimeExtensionNode>[] {
|
|
515
1429
|
const nodes = parseInlineImpl(text, opts);
|
|
516
1430
|
return opts.autolink === 'gfm' ? transformAutolinks(nodes) : nodes;
|
|
517
1431
|
}
|
|
518
1432
|
|
|
519
|
-
function parseInlineImpl(
|
|
520
|
-
|
|
1433
|
+
function parseInlineImpl(
|
|
1434
|
+
text: string,
|
|
1435
|
+
opts: ResolvedOptions,
|
|
1436
|
+
): MarkdownAstPhrasingContent<RuntimeExtensionNode>[] {
|
|
1437
|
+
const nodes: MarkdownAstPhrasingContent<RuntimeExtensionNode>[] = [];
|
|
1438
|
+
// Only a plugin that actually contributes INLINE syntax may cost anything
|
|
1439
|
+
// per source position. A transform-only list contributes none, so it takes
|
|
1440
|
+
// the same path as an omitted or empty one: no candidate probe per
|
|
1441
|
+
// position, and no map lookup per character in the plain-text scan below.
|
|
1442
|
+
const inlineExtensionStarts =
|
|
1443
|
+
opts.plugins != null && opts.plugins.inlineByFirstCharacter.size > 0
|
|
1444
|
+
? opts.plugins.inlineByFirstCharacter
|
|
1445
|
+
: undefined;
|
|
521
1446
|
let i = 0;
|
|
522
1447
|
|
|
523
1448
|
while (i < text.length) {
|
|
524
1449
|
// --- Escape ---
|
|
525
1450
|
if (text[i] === '\\' && i + 1 < text.length) {
|
|
526
|
-
nodes.push({type: 'text',
|
|
1451
|
+
nodes.push({type: 'text', value: text[i + 1]});
|
|
527
1452
|
i += 2;
|
|
528
1453
|
continue;
|
|
529
1454
|
}
|
|
@@ -534,12 +1459,28 @@ function parseInlineImpl(text: string, opts: ResolvedOptions): InlineNode[] {
|
|
|
534
1459
|
const openIndex = i + tickCount;
|
|
535
1460
|
const closeIndex = text.indexOf('`'.repeat(tickCount), openIndex);
|
|
536
1461
|
if (closeIndex !== -1) {
|
|
537
|
-
nodes.push({
|
|
1462
|
+
nodes.push({
|
|
1463
|
+
type: 'inlineCode',
|
|
1464
|
+
value: text.slice(openIndex, closeIndex),
|
|
1465
|
+
});
|
|
538
1466
|
i = closeIndex + tickCount;
|
|
539
1467
|
continue;
|
|
540
1468
|
}
|
|
541
1469
|
}
|
|
542
1470
|
|
|
1471
|
+
// --- Inline math (opt-in; code takes precedence) ---
|
|
1472
|
+
if (opts.math && text[i] === '$') {
|
|
1473
|
+
const closeIndex = findInlineMathEnd(text, i);
|
|
1474
|
+
if (closeIndex !== -1) {
|
|
1475
|
+
nodes.push({
|
|
1476
|
+
type: 'inlineMath',
|
|
1477
|
+
value: text.slice(i + 1, closeIndex),
|
|
1478
|
+
});
|
|
1479
|
+
i = closeIndex + 1;
|
|
1480
|
+
continue;
|
|
1481
|
+
}
|
|
1482
|
+
}
|
|
1483
|
+
|
|
543
1484
|
// --- Citation: fullwidth 【id】 ---
|
|
544
1485
|
{
|
|
545
1486
|
const citation = matchFullwidthCitation(text, i, opts);
|
|
@@ -557,13 +1498,13 @@ function parseInlineImpl(text: string, opts: ResolvedOptions): InlineNode[] {
|
|
|
557
1498
|
const srcClose = findClosingParen(text, altClose + 2);
|
|
558
1499
|
if (srcClose !== -1) {
|
|
559
1500
|
const src = text.slice(altClose + 2, srcClose);
|
|
560
|
-
if (!
|
|
1501
|
+
if (!isSafeMarkdownParserUrl(src)) {
|
|
561
1502
|
// Dangerous scheme — emit as plain text.
|
|
562
|
-
nodes.push({type: 'text',
|
|
1503
|
+
nodes.push({type: 'text', value: text.slice(i, srcClose + 1)});
|
|
563
1504
|
} else {
|
|
564
1505
|
nodes.push({
|
|
565
1506
|
type: 'image',
|
|
566
|
-
src,
|
|
1507
|
+
url: src,
|
|
567
1508
|
alt: text.slice(i + 2, altClose),
|
|
568
1509
|
});
|
|
569
1510
|
}
|
|
@@ -600,14 +1541,17 @@ function parseInlineImpl(text: string, opts: ResolvedOptions): InlineNode[] {
|
|
|
600
1541
|
const urlClose = findClosingParen(text, textClose + 2);
|
|
601
1542
|
if (urlClose !== -1) {
|
|
602
1543
|
const href = text.slice(textClose + 2, urlClose);
|
|
603
|
-
if (!
|
|
1544
|
+
if (!isSafeMarkdownParserUrl(href)) {
|
|
604
1545
|
// Dangerous scheme — emit as plain text instead of a link.
|
|
605
|
-
nodes.push({type: 'text',
|
|
1546
|
+
nodes.push({type: 'text', value: text.slice(i, urlClose + 1)});
|
|
606
1547
|
} else {
|
|
607
1548
|
nodes.push({
|
|
608
1549
|
type: 'link',
|
|
609
|
-
href,
|
|
610
|
-
children: parseInlineImpl(
|
|
1550
|
+
url: href,
|
|
1551
|
+
children: parseInlineImpl(
|
|
1552
|
+
text.slice(i + 1, textClose),
|
|
1553
|
+
protectedInlineOptions(opts),
|
|
1554
|
+
),
|
|
611
1555
|
});
|
|
612
1556
|
}
|
|
613
1557
|
i = urlClose + 1;
|
|
@@ -642,10 +1586,10 @@ function parseInlineImpl(text: string, opts: ResolvedOptions): InlineNode[] {
|
|
|
642
1586
|
(!isUnderscore || !isWordChar(text[closeIndex + 3]))
|
|
643
1587
|
) {
|
|
644
1588
|
nodes.push({
|
|
645
|
-
type: '
|
|
1589
|
+
type: 'strong',
|
|
646
1590
|
children: [
|
|
647
1591
|
{
|
|
648
|
-
type: '
|
|
1592
|
+
type: 'emphasis',
|
|
649
1593
|
children: parseInlineImpl(text.slice(i + 3, closeIndex), opts),
|
|
650
1594
|
},
|
|
651
1595
|
],
|
|
@@ -672,7 +1616,7 @@ function parseInlineImpl(text: string, opts: ResolvedOptions): InlineNode[] {
|
|
|
672
1616
|
(!isUnderscore || !isWordChar(text[closeIndex + 2]))
|
|
673
1617
|
) {
|
|
674
1618
|
nodes.push({
|
|
675
|
-
type: '
|
|
1619
|
+
type: 'strong',
|
|
676
1620
|
children: parseInlineImpl(text.slice(i + 2, closeIndex), opts),
|
|
677
1621
|
});
|
|
678
1622
|
i = closeIndex + 2;
|
|
@@ -686,7 +1630,7 @@ function parseInlineImpl(text: string, opts: ResolvedOptions): InlineNode[] {
|
|
|
686
1630
|
const closeIndex = text.indexOf('~~', i + 2);
|
|
687
1631
|
if (closeIndex !== -1) {
|
|
688
1632
|
nodes.push({
|
|
689
|
-
type: '
|
|
1633
|
+
type: 'delete',
|
|
690
1634
|
children: parseInlineImpl(text.slice(i + 2, closeIndex), opts),
|
|
691
1635
|
});
|
|
692
1636
|
i = closeIndex + 2;
|
|
@@ -707,7 +1651,7 @@ function parseInlineImpl(text: string, opts: ResolvedOptions): InlineNode[] {
|
|
|
707
1651
|
(!isUnderscore || !isWordChar(text[closeIndex + 1]))
|
|
708
1652
|
) {
|
|
709
1653
|
nodes.push({
|
|
710
|
-
type: '
|
|
1654
|
+
type: 'emphasis',
|
|
711
1655
|
children: parseInlineImpl(text.slice(i + 1, closeIndex), opts),
|
|
712
1656
|
});
|
|
713
1657
|
i = closeIndex + 1;
|
|
@@ -716,9 +1660,30 @@ function parseInlineImpl(text: string, opts: ResolvedOptions): InlineNode[] {
|
|
|
716
1660
|
}
|
|
717
1661
|
}
|
|
718
1662
|
|
|
1663
|
+
// --- Extension syntax (built-ins and protected contexts win) ---
|
|
1664
|
+
if (inlineExtensionStarts !== undefined) {
|
|
1665
|
+
const extension = matchExtensionSyntax(text, i, 'inline', opts);
|
|
1666
|
+
if (extension.status === 'match') {
|
|
1667
|
+
nodes.push(
|
|
1668
|
+
extension.node as Extract<RuntimeExtensionNode, {display: 'inline'}>,
|
|
1669
|
+
);
|
|
1670
|
+
i = extension.end;
|
|
1671
|
+
continue;
|
|
1672
|
+
}
|
|
1673
|
+
if (extension.status === 'defer') {
|
|
1674
|
+
break;
|
|
1675
|
+
}
|
|
1676
|
+
}
|
|
1677
|
+
|
|
719
1678
|
// --- Plain text (with line-break detection) ---
|
|
720
1679
|
let end = i + 1;
|
|
721
|
-
while (
|
|
1680
|
+
while (
|
|
1681
|
+
end < text.length &&
|
|
1682
|
+
!'*_~`[!\\\n\u3010'.includes(text[end]) &&
|
|
1683
|
+
!(opts.math && text[end] === '$') &&
|
|
1684
|
+
(inlineExtensionStarts === undefined ||
|
|
1685
|
+
!inlineExtensionStarts.has(text[end]))
|
|
1686
|
+
) {
|
|
722
1687
|
end++;
|
|
723
1688
|
}
|
|
724
1689
|
|
|
@@ -731,9 +1696,12 @@ function parseInlineImpl(text: string, opts: ResolvedOptions): InlineNode[] {
|
|
|
731
1696
|
if (trimmed.length > 0) {
|
|
732
1697
|
const last = nodes[nodes.length - 1];
|
|
733
1698
|
if (last?.type === 'text') {
|
|
734
|
-
|
|
1699
|
+
nodes[nodes.length - 1] = {
|
|
1700
|
+
...last,
|
|
1701
|
+
value: last.value + trimmed,
|
|
1702
|
+
};
|
|
735
1703
|
} else {
|
|
736
|
-
nodes.push({type: 'text',
|
|
1704
|
+
nodes.push({type: 'text', value: trimmed});
|
|
737
1705
|
}
|
|
738
1706
|
}
|
|
739
1707
|
nodes.push({type: 'break'});
|
|
@@ -744,9 +1712,9 @@ function parseInlineImpl(text: string, opts: ResolvedOptions): InlineNode[] {
|
|
|
744
1712
|
|
|
745
1713
|
const last = nodes[nodes.length - 1];
|
|
746
1714
|
if (last?.type === 'text') {
|
|
747
|
-
last.
|
|
1715
|
+
nodes[nodes.length - 1] = {...last, value: last.value + content};
|
|
748
1716
|
} else {
|
|
749
|
-
nodes.push({type: 'text', content});
|
|
1717
|
+
nodes.push({type: 'text', value: content});
|
|
750
1718
|
}
|
|
751
1719
|
i = end;
|
|
752
1720
|
}
|
|
@@ -871,7 +1839,7 @@ function scanAutolinksInText(text: string): AutolinkMatch[] {
|
|
|
871
1839
|
while ((m = re.exec(text)) !== null) {
|
|
872
1840
|
const url = m[1];
|
|
873
1841
|
// Skip dangerous URL schemes (javascript:, vbscript:, data:text/html)
|
|
874
|
-
if (!
|
|
1842
|
+
if (!isSafeMarkdownParserUrl(url)) {
|
|
875
1843
|
continue;
|
|
876
1844
|
}
|
|
877
1845
|
matches.push({
|
|
@@ -991,26 +1959,28 @@ function scanAutolinksInText(text: string): AutolinkMatch[] {
|
|
|
991
1959
|
* Split a text-node `content` string into a sequence of text + link nodes
|
|
992
1960
|
* based on autolink matches.
|
|
993
1961
|
*/
|
|
994
|
-
function splitTextOnAutolinks(
|
|
1962
|
+
function splitTextOnAutolinks(
|
|
1963
|
+
content: string,
|
|
1964
|
+
): MarkdownAstPhrasingContent<RuntimeExtensionNode>[] {
|
|
995
1965
|
const matches = scanAutolinksInText(content);
|
|
996
1966
|
if (matches.length === 0) {
|
|
997
|
-
return [{type: 'text', content}];
|
|
1967
|
+
return [{type: 'text', value: content}];
|
|
998
1968
|
}
|
|
999
|
-
const out:
|
|
1969
|
+
const out: MarkdownAstPhrasingContent<RuntimeExtensionNode>[] = [];
|
|
1000
1970
|
let cursor = 0;
|
|
1001
1971
|
for (const m of matches) {
|
|
1002
1972
|
if (m.start > cursor) {
|
|
1003
|
-
out.push({type: 'text',
|
|
1973
|
+
out.push({type: 'text', value: content.slice(cursor, m.start)});
|
|
1004
1974
|
}
|
|
1005
1975
|
out.push({
|
|
1006
1976
|
type: 'link',
|
|
1007
|
-
|
|
1008
|
-
children: [{type: 'text',
|
|
1977
|
+
url: m.href,
|
|
1978
|
+
children: [{type: 'text', value: m.display}],
|
|
1009
1979
|
});
|
|
1010
1980
|
cursor = m.end;
|
|
1011
1981
|
}
|
|
1012
1982
|
if (cursor < content.length) {
|
|
1013
|
-
out.push({type: 'text',
|
|
1983
|
+
out.push({type: 'text', value: content.slice(cursor)});
|
|
1014
1984
|
}
|
|
1015
1985
|
return out;
|
|
1016
1986
|
}
|
|
@@ -1023,23 +1993,25 @@ function splitTextOnAutolinks(content: string): InlineNode[] {
|
|
|
1023
1993
|
* `image` alt text, `citation`, or `break`. Runs only on the outermost
|
|
1024
1994
|
* block's inline tree (see `parseInlineEntry`).
|
|
1025
1995
|
*/
|
|
1026
|
-
function transformAutolinks(
|
|
1027
|
-
|
|
1996
|
+
function transformAutolinks(
|
|
1997
|
+
nodes: ReadonlyArray<MarkdownAstPhrasingContent<RuntimeExtensionNode>>,
|
|
1998
|
+
): MarkdownAstPhrasingContent<RuntimeExtensionNode>[] {
|
|
1999
|
+
const out: MarkdownAstPhrasingContent<RuntimeExtensionNode>[] = [];
|
|
1028
2000
|
for (const node of nodes) {
|
|
1029
2001
|
if (node.type === 'text') {
|
|
1030
|
-
const split = splitTextOnAutolinks(node.
|
|
2002
|
+
const split = splitTextOnAutolinks(node.value);
|
|
1031
2003
|
for (const seg of split) {
|
|
1032
2004
|
const last = out[out.length - 1];
|
|
1033
2005
|
if (seg.type === 'text' && last?.type === 'text') {
|
|
1034
|
-
last.
|
|
2006
|
+
out[out.length - 1] = {...last, value: last.value + seg.value};
|
|
1035
2007
|
} else {
|
|
1036
2008
|
out.push(seg);
|
|
1037
2009
|
}
|
|
1038
2010
|
}
|
|
1039
2011
|
} else if (
|
|
1040
|
-
node.type === '
|
|
1041
|
-
node.type === '
|
|
1042
|
-
node.type === '
|
|
2012
|
+
node.type === 'strong' ||
|
|
2013
|
+
node.type === 'emphasis' ||
|
|
2014
|
+
node.type === 'delete'
|
|
1043
2015
|
) {
|
|
1044
2016
|
out.push({...node, children: transformAutolinks(node.children)});
|
|
1045
2017
|
} else {
|
|
@@ -1122,7 +2094,16 @@ function isTableSeparator(line: string): boolean {
|
|
|
1122
2094
|
* offset into it would not address the document.
|
|
1123
2095
|
*/
|
|
1124
2096
|
function nested(opts: ResolvedOptions): ResolvedOptions {
|
|
1125
|
-
|
|
2097
|
+
const nestedOptions =
|
|
2098
|
+
opts.allowBlockSyntax === false ? opts : {...opts, allowBlockSyntax: false};
|
|
2099
|
+
return nestedOptions.sourceRanges || nestedOptions.astPositions
|
|
2100
|
+
? {...nestedOptions, sourceRanges: false, astPositions: false}
|
|
2101
|
+
: nestedOptions;
|
|
2102
|
+
}
|
|
2103
|
+
|
|
2104
|
+
function blockExtensionColumn(line: string): number | null {
|
|
2105
|
+
const indentation = line.length - line.trimStart().length;
|
|
2106
|
+
return indentation <= 3 ? indentation : null;
|
|
1126
2107
|
}
|
|
1127
2108
|
|
|
1128
2109
|
/**
|
|
@@ -1198,10 +2179,10 @@ function parseTable(
|
|
|
1198
2179
|
lines: string[],
|
|
1199
2180
|
lineIndex: number,
|
|
1200
2181
|
opts: ResolvedOptions,
|
|
1201
|
-
): {node:
|
|
1202
|
-
const headers:
|
|
1203
|
-
|
|
1204
|
-
);
|
|
2182
|
+
): {node: MarkdownAstBlockContent<RuntimeExtensionNode>; nextIndex: number} {
|
|
2183
|
+
const headers: MarkdownAstTableCell<RuntimeExtensionNode>[] = splitTableRow(
|
|
2184
|
+
lines[lineIndex],
|
|
2185
|
+
).map(cell => ({type: 'tableCell', children: parseInlineEntry(cell, opts)}));
|
|
1205
2186
|
const alignments: TableAlignment[] = splitTableRow(lines[lineIndex + 1]).map(
|
|
1206
2187
|
cell => {
|
|
1207
2188
|
const trimmed = cell.trim();
|
|
@@ -1216,22 +2197,32 @@ function parseTable(
|
|
|
1216
2197
|
: null;
|
|
1217
2198
|
},
|
|
1218
2199
|
);
|
|
1219
|
-
const rows:
|
|
2200
|
+
const rows: MarkdownAstTableRow<RuntimeExtensionNode>[] = [];
|
|
1220
2201
|
let rowIndex = lineIndex + 2;
|
|
1221
2202
|
while (
|
|
1222
2203
|
rowIndex < lines.length &&
|
|
1223
2204
|
lines[rowIndex].includes('|') &&
|
|
1224
2205
|
lines[rowIndex].trim() !== ''
|
|
1225
2206
|
) {
|
|
1226
|
-
rows.push(
|
|
1227
|
-
|
|
2207
|
+
rows.push({
|
|
2208
|
+
type: 'tableRow',
|
|
2209
|
+
children: splitTableRow(lines[rowIndex]).map(cell => ({
|
|
2210
|
+
type: 'tableCell',
|
|
1228
2211
|
children: parseInlineEntry(cell, opts),
|
|
1229
2212
|
})),
|
|
1230
|
-
);
|
|
2213
|
+
});
|
|
1231
2214
|
rowIndex++;
|
|
1232
2215
|
}
|
|
2216
|
+
const header: MarkdownAstTableRow<RuntimeExtensionNode> = {
|
|
2217
|
+
type: 'tableRow',
|
|
2218
|
+
children: headers,
|
|
2219
|
+
};
|
|
1233
2220
|
return {
|
|
1234
|
-
node: {
|
|
2221
|
+
node: {
|
|
2222
|
+
type: 'table',
|
|
2223
|
+
align: alignments,
|
|
2224
|
+
children: [header, ...rows],
|
|
2225
|
+
},
|
|
1235
2226
|
nextIndex: rowIndex,
|
|
1236
2227
|
};
|
|
1237
2228
|
}
|
|
@@ -1241,8 +2232,8 @@ function parseList(
|
|
|
1241
2232
|
startIndex: number,
|
|
1242
2233
|
ordered: boolean,
|
|
1243
2234
|
opts: ResolvedOptions,
|
|
1244
|
-
): {node:
|
|
1245
|
-
const items:
|
|
2235
|
+
): {node: MarkdownAstBlockContent<RuntimeExtensionNode>; nextIndex: number} {
|
|
2236
|
+
const items: MarkdownAstListItem<RuntimeExtensionNode>[] = [];
|
|
1246
2237
|
const baseIndent = getIndent(lines[startIndex]);
|
|
1247
2238
|
// Ordered lists may use either '.' or ')' as the marker delimiter
|
|
1248
2239
|
// (CommonMark 5.2). Capture which one this list starts with so its items
|
|
@@ -1296,7 +2287,11 @@ function parseList(
|
|
|
1296
2287
|
itemText += '\n' + deindented.join('\n');
|
|
1297
2288
|
}
|
|
1298
2289
|
|
|
1299
|
-
items.push({
|
|
2290
|
+
items.push({
|
|
2291
|
+
type: 'listItem',
|
|
2292
|
+
checked,
|
|
2293
|
+
children: parseMarkdownImpl(itemText, nested(opts)),
|
|
2294
|
+
});
|
|
1300
2295
|
|
|
1301
2296
|
// CommonMark loose list: blank line(s) between items of the same style
|
|
1302
2297
|
// and indent still form one list. Skip the blanks and continue if the
|
|
@@ -1314,17 +2309,15 @@ function parseList(
|
|
|
1314
2309
|
index = lookahead;
|
|
1315
2310
|
}
|
|
1316
2311
|
}
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
items,
|
|
1325
|
-
},
|
|
1326
|
-
nextIndex: index,
|
|
2312
|
+
const node: MarkdownAstList<RuntimeExtensionNode> = {
|
|
2313
|
+
type: 'list',
|
|
2314
|
+
ordered,
|
|
2315
|
+
start,
|
|
2316
|
+
delimiter: ordered ? (delim as '.' | ')') : undefined,
|
|
2317
|
+
spread: loose || undefined,
|
|
2318
|
+
children: items,
|
|
1327
2319
|
};
|
|
2320
|
+
return {node, nextIndex: index};
|
|
1328
2321
|
}
|
|
1329
2322
|
|
|
1330
2323
|
// ---------------------------------------------------------------------------
|
|
@@ -1337,19 +2330,89 @@ export function parseMarkdown(
|
|
|
1337
2330
|
): BlockNode[];
|
|
1338
2331
|
export function parseMarkdown(
|
|
1339
2332
|
input: string,
|
|
1340
|
-
options:
|
|
2333
|
+
options: MathParseOptionsWithoutPlugins,
|
|
2334
|
+
): BlockNodeWithMath[];
|
|
2335
|
+
export function parseMarkdown(
|
|
2336
|
+
input: string,
|
|
2337
|
+
options: ParseOptionsWithoutPlugins,
|
|
1341
2338
|
): BlockNode[];
|
|
2339
|
+
export function parseMarkdown<
|
|
2340
|
+
const Plugins extends ReadonlyArray<MarkdownPluginEntry>,
|
|
2341
|
+
>(
|
|
2342
|
+
input: string,
|
|
2343
|
+
options: ParseOptionsWithoutPlugins & {plugins: Plugins},
|
|
2344
|
+
): BlockNode<MarkdownExtensionsOf<Plugins>>[];
|
|
2345
|
+
export function parseMarkdown<
|
|
2346
|
+
const Plugins extends ReadonlyArray<MarkdownPluginEntry>,
|
|
2347
|
+
>(
|
|
2348
|
+
input: string,
|
|
2349
|
+
options: MathParseOptionsWithoutPlugins & {plugins: Plugins},
|
|
2350
|
+
): BlockNodeWithMath<MarkdownExtensionsOf<Plugins>>[];
|
|
1342
2351
|
export function parseMarkdown(
|
|
1343
2352
|
input: string,
|
|
1344
|
-
arg?: ReadonlySet<string> |
|
|
1345
|
-
):
|
|
1346
|
-
return
|
|
2353
|
+
arg?: ReadonlySet<string> | RuntimeParseOptions,
|
|
2354
|
+
): RuntimeBlockNode[] {
|
|
2355
|
+
return projectMarkdownRoot(
|
|
2356
|
+
parseMarkdownAstInternal(input, arg, true),
|
|
2357
|
+
wantsLegacyRanges(arg),
|
|
2358
|
+
);
|
|
2359
|
+
}
|
|
2360
|
+
|
|
2361
|
+
/**
|
|
2362
|
+
* Parse block Markdown into the canonical immutable Astryx AST.
|
|
2363
|
+
*
|
|
2364
|
+
* Unlike `parseMarkdown`, this returns the MDAST-aligned root used by Markdown,
|
|
2365
|
+
* transforms, and Outline. Math nodes are present only when `math: true`, and
|
|
2366
|
+
* plugin extension nodes are inferred from the ordered `plugins` tuple.
|
|
2367
|
+
*/
|
|
2368
|
+
export function parseMarkdownAst(
|
|
2369
|
+
input: string,
|
|
2370
|
+
sourceIds?: ReadonlySet<string>,
|
|
2371
|
+
): MarkdownAstRoot;
|
|
2372
|
+
export function parseMarkdownAst(
|
|
2373
|
+
input: string,
|
|
2374
|
+
options: ParseOptionsWithoutPlugins | MathParseOptionsWithoutPlugins,
|
|
2375
|
+
): MarkdownAstRoot;
|
|
2376
|
+
export function parseMarkdownAst<
|
|
2377
|
+
const Plugins extends ReadonlyArray<MarkdownPluginEntry>,
|
|
2378
|
+
>(
|
|
2379
|
+
input: string,
|
|
2380
|
+
options: (ParseOptionsWithoutPlugins | MathParseOptionsWithoutPlugins) & {
|
|
2381
|
+
plugins: Plugins;
|
|
2382
|
+
},
|
|
2383
|
+
): MarkdownAstRoot<MarkdownExtensionsOf<Plugins>>;
|
|
2384
|
+
export function parseMarkdownAst(
|
|
2385
|
+
input: string,
|
|
2386
|
+
arg?: ReadonlySet<string> | RuntimeParseOptions,
|
|
2387
|
+
): MarkdownAstRoot<RuntimeExtensionNode> {
|
|
2388
|
+
return parseMarkdownAstInternal(input, arg, true);
|
|
2389
|
+
}
|
|
2390
|
+
|
|
2391
|
+
/** @internal Full-parse entry point with streaming finality for Outline. */
|
|
2392
|
+
export function parseMarkdownAstInternal(
|
|
2393
|
+
input: string,
|
|
2394
|
+
arg: ReadonlySet<string> | RuntimeParseOptions | undefined,
|
|
2395
|
+
isFinal: boolean,
|
|
2396
|
+
): MarkdownAstRoot<RuntimeExtensionNode> {
|
|
2397
|
+
const resolved = resolveOptions(arg);
|
|
2398
|
+
const opts = isFinal ? resolved : {...resolved, isFinal: false};
|
|
2399
|
+
const root: MarkdownAstRoot<RuntimeExtensionNode> = {
|
|
2400
|
+
type: 'root',
|
|
2401
|
+
children: parseMarkdownImpl(input, opts),
|
|
2402
|
+
};
|
|
2403
|
+
return applyMarkdownTransforms(
|
|
2404
|
+
root,
|
|
2405
|
+
opts.plugins,
|
|
2406
|
+
input,
|
|
2407
|
+
opts.isFinal,
|
|
2408
|
+
'block',
|
|
2409
|
+
);
|
|
1347
2410
|
}
|
|
1348
2411
|
|
|
1349
2412
|
function parseMarkdownImpl(
|
|
1350
2413
|
input: string,
|
|
1351
2414
|
baseOpts: ResolvedOptions,
|
|
1352
|
-
):
|
|
2415
|
+
): MarkdownAstBlockContent<RuntimeExtensionNode>[] {
|
|
1353
2416
|
// Collect this input's link reference definitions and strip their lines,
|
|
1354
2417
|
// then merge them with any definitions inherited from an enclosing parse
|
|
1355
2418
|
// (the incremental parser passes the whole document's definitions in; a
|
|
@@ -1357,7 +2420,7 @@ function parseMarkdownImpl(
|
|
|
1357
2420
|
// definitions win on conflict, matching CommonMark's first-definition-wins
|
|
1358
2421
|
// in document order; locally-nested definitions still resolve within this
|
|
1359
2422
|
// parse.
|
|
1360
|
-
const {defs, cleaned, lineMap} = extractLinkDefinitions(input);
|
|
2423
|
+
const {defs, cleaned, lineMap} = extractLinkDefinitions(input, baseOpts.math);
|
|
1361
2424
|
const inherited = baseOpts.linkDefs;
|
|
1362
2425
|
let linkDefs: ReadonlyMap<string, string> | undefined;
|
|
1363
2426
|
if (defs.size === 0) {
|
|
@@ -1370,18 +2433,35 @@ function parseMarkdownImpl(
|
|
|
1370
2433
|
const opts: ResolvedOptions =
|
|
1371
2434
|
linkDefs != null ? {...baseOpts, linkDefs} : baseOpts;
|
|
1372
2435
|
const lines = cleaned.split('\n');
|
|
1373
|
-
const
|
|
2436
|
+
const hasBlockExtensionSyntax =
|
|
2437
|
+
opts.allowBlockSyntax !== false &&
|
|
2438
|
+
(opts.plugins?.blockByFirstCharacter.size ?? 0) > 0;
|
|
2439
|
+
// Derived from the already-split lines rather than a second character
|
|
2440
|
+
// scan: every line contributes its own length plus the newline it ended on.
|
|
2441
|
+
const lineOffsets = [0];
|
|
2442
|
+
if (opts.astPositions === true || hasBlockExtensionSyntax) {
|
|
2443
|
+
let offset = 0;
|
|
2444
|
+
for (let index = 0; index < lines.length; index++) {
|
|
2445
|
+
offset += lines[index].length + 1;
|
|
2446
|
+
lineOffsets.push(offset);
|
|
2447
|
+
}
|
|
2448
|
+
}
|
|
2449
|
+
const blockExtensionMatches = new Map<number, ExtensionMatch>();
|
|
2450
|
+
const blocks: MarkdownAstBlockContent<RuntimeExtensionNode>[] = [];
|
|
1374
2451
|
// The line each block started on, parallel to `blocks`. Only collected when
|
|
1375
2452
|
// ranges were asked for; a block's end is resolved after the loop, since the
|
|
1376
2453
|
// branch that produced it has already moved `index` past whatever it read.
|
|
1377
|
-
const blockStartLines: number[] | null =
|
|
2454
|
+
const blockStartLines: number[] | null =
|
|
2455
|
+
opts.astPositions === true ? [] : null;
|
|
1378
2456
|
// Set only by a block that consumes blank lines as content, where the
|
|
1379
2457
|
// positional end derivation would trim them away.
|
|
1380
|
-
const blockEndLines: (number | undefined)[] | null =
|
|
1381
|
-
? []
|
|
1382
|
-
: null;
|
|
2458
|
+
const blockEndLines: (number | undefined)[] | null =
|
|
2459
|
+
opts.astPositions === true ? [] : null;
|
|
1383
2460
|
let blockStartLine = 0;
|
|
1384
|
-
const pushBlock = (
|
|
2461
|
+
const pushBlock = (
|
|
2462
|
+
node: MarkdownAstBlockContent<RuntimeExtensionNode>,
|
|
2463
|
+
endLine?: number,
|
|
2464
|
+
) => {
|
|
1385
2465
|
blocks.push(node);
|
|
1386
2466
|
blockStartLines?.push(blockStartLine);
|
|
1387
2467
|
blockEndLines?.push(endLine);
|
|
@@ -1397,10 +2477,16 @@ function parseMarkdownImpl(
|
|
|
1397
2477
|
}
|
|
1398
2478
|
|
|
1399
2479
|
// --- Fenced code block ---
|
|
1400
|
-
const fenceMatch = line.match(/^(`{3,}|~{3,})
|
|
2480
|
+
const fenceMatch = line.match(/^(`{3,}|~{3,})/);
|
|
1401
2481
|
if (fenceMatch) {
|
|
1402
2482
|
const fence = fenceMatch[1];
|
|
1403
|
-
const
|
|
2483
|
+
const info = line.slice(fence.length);
|
|
2484
|
+
const language = info.match(/^(\S+)/)?.[1] ?? null;
|
|
2485
|
+
const legacyLanguage = info.match(/^(\w*)/)?.[1] || null;
|
|
2486
|
+
const meta =
|
|
2487
|
+
language == null
|
|
2488
|
+
? undefined
|
|
2489
|
+
: info.slice(language.length).trim() || undefined;
|
|
1404
2490
|
const codeLines: string[] = [];
|
|
1405
2491
|
index++;
|
|
1406
2492
|
while (index < lines.length && !lines[index].startsWith(fence)) {
|
|
@@ -1412,18 +2498,39 @@ function parseMarkdownImpl(
|
|
|
1412
2498
|
// can end on them, so it states its own end rather than letting the
|
|
1413
2499
|
// positional derivation trim them off.
|
|
1414
2500
|
pushBlock(
|
|
1415
|
-
|
|
2501
|
+
markMarkdownAstLegacyCodeLanguage(
|
|
2502
|
+
{
|
|
2503
|
+
type: 'code',
|
|
2504
|
+
lang: language,
|
|
2505
|
+
...(meta == null ? {} : {meta}),
|
|
2506
|
+
value: codeLines.join('\n'),
|
|
2507
|
+
},
|
|
2508
|
+
legacyLanguage,
|
|
2509
|
+
),
|
|
1416
2510
|
Math.min(index, lines.length) - 1,
|
|
1417
2511
|
);
|
|
1418
2512
|
continue;
|
|
1419
2513
|
}
|
|
1420
2514
|
|
|
2515
|
+
// --- Display math (opt-in; fenced code takes precedence) ---
|
|
2516
|
+
if (opts.math) {
|
|
2517
|
+
const displayMath = matchDisplayMathBlock(lines, index);
|
|
2518
|
+
if (displayMath != null) {
|
|
2519
|
+
pushBlock(
|
|
2520
|
+
{type: 'math', value: displayMath.value},
|
|
2521
|
+
displayMath.endLine,
|
|
2522
|
+
);
|
|
2523
|
+
index = displayMath.nextIndex;
|
|
2524
|
+
continue;
|
|
2525
|
+
}
|
|
2526
|
+
}
|
|
2527
|
+
|
|
1421
2528
|
// --- Heading ---
|
|
1422
2529
|
const headingMatch = line.match(/^(#{1,6}) +(.*)/);
|
|
1423
2530
|
if (headingMatch) {
|
|
1424
2531
|
pushBlock({
|
|
1425
2532
|
type: 'heading',
|
|
1426
|
-
|
|
2533
|
+
depth: headingMatch[1].length as 1 | 2 | 3 | 4 | 5 | 6,
|
|
1427
2534
|
children: parseInlineEntry(headingMatch[2], opts),
|
|
1428
2535
|
});
|
|
1429
2536
|
index++;
|
|
@@ -1432,7 +2539,7 @@ function parseMarkdownImpl(
|
|
|
1432
2539
|
|
|
1433
2540
|
// --- HR (must precede list check to handle `- - -`, `* * *`, `_ _ _`) ---
|
|
1434
2541
|
if (isHorizontalRule(line)) {
|
|
1435
|
-
pushBlock({type: '
|
|
2542
|
+
pushBlock({type: 'thematicBreak'});
|
|
1436
2543
|
index++;
|
|
1437
2544
|
continue;
|
|
1438
2545
|
}
|
|
@@ -1444,9 +2551,9 @@ function parseMarkdownImpl(
|
|
|
1444
2551
|
if (
|
|
1445
2552
|
imageMatch &&
|
|
1446
2553
|
line.trim() === imageMatch[0] &&
|
|
1447
|
-
|
|
2554
|
+
isSafeMarkdownParserUrl(imageMatch[2])
|
|
1448
2555
|
) {
|
|
1449
|
-
pushBlock({type: 'image', alt: imageMatch[1],
|
|
2556
|
+
pushBlock({type: 'image', alt: imageMatch[1], url: imageMatch[2]});
|
|
1450
2557
|
index++;
|
|
1451
2558
|
continue;
|
|
1452
2559
|
}
|
|
@@ -1496,15 +2603,80 @@ function parseMarkdownImpl(
|
|
|
1496
2603
|
continue;
|
|
1497
2604
|
}
|
|
1498
2605
|
|
|
2606
|
+
// --- Extension block syntax (built-in blocks take precedence) ---
|
|
2607
|
+
if (hasBlockExtensionSyntax) {
|
|
2608
|
+
const extensionColumn = blockExtensionColumn(line);
|
|
2609
|
+
const extensionOffset =
|
|
2610
|
+
extensionColumn == null
|
|
2611
|
+
? lineOffsets[index]
|
|
2612
|
+
: lineOffsets[index] + extensionColumn;
|
|
2613
|
+
const extension =
|
|
2614
|
+
blockExtensionMatches.get(extensionOffset) ??
|
|
2615
|
+
(extensionColumn == null
|
|
2616
|
+
? {status: 'none' as const}
|
|
2617
|
+
: matchExtensionSyntax(cleaned, extensionOffset, 'block', opts));
|
|
2618
|
+
blockExtensionMatches.delete(extensionOffset);
|
|
2619
|
+
if (extension.status === 'match') {
|
|
2620
|
+
const consumedEnd = extension.end;
|
|
2621
|
+
const consumed = cleaned.slice(lineOffsets[index], consumedEnd);
|
|
2622
|
+
if (
|
|
2623
|
+
consumedEnd < cleaned.length &&
|
|
2624
|
+
cleaned[consumedEnd] !== '\n' &&
|
|
2625
|
+
cleaned[consumedEnd - 1] !== '\n'
|
|
2626
|
+
) {
|
|
2627
|
+
reportMarkdownPluginFailure(
|
|
2628
|
+
extension.node.plugin,
|
|
2629
|
+
'syntax',
|
|
2630
|
+
new TypeError('Block syntax must consume complete lines'),
|
|
2631
|
+
);
|
|
2632
|
+
} else {
|
|
2633
|
+
const newlineCount = consumed.split('\n').length - 1;
|
|
2634
|
+
const consumedLines = Math.max(
|
|
2635
|
+
1,
|
|
2636
|
+
newlineCount + (consumed.endsWith('\n') ? 0 : 1),
|
|
2637
|
+
);
|
|
2638
|
+
pushBlock(
|
|
2639
|
+
extension.node as Extract<RuntimeExtensionNode, {display: 'block'}>,
|
|
2640
|
+
index + consumedLines - 1,
|
|
2641
|
+
);
|
|
2642
|
+
index += consumedLines;
|
|
2643
|
+
continue;
|
|
2644
|
+
}
|
|
2645
|
+
}
|
|
2646
|
+
if (extension.status === 'defer') {
|
|
2647
|
+
break;
|
|
2648
|
+
}
|
|
2649
|
+
}
|
|
2650
|
+
|
|
1499
2651
|
// --- Paragraph ---
|
|
1500
2652
|
const paraLines: string[] = [line];
|
|
1501
2653
|
index++;
|
|
1502
|
-
while (
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
2654
|
+
while (index < lines.length) {
|
|
2655
|
+
const nextLine = lines[index];
|
|
2656
|
+
if (
|
|
2657
|
+
isBlockStart(nextLine) ||
|
|
2658
|
+
(opts.math && matchDisplayMathBlock(lines, index) != null) ||
|
|
2659
|
+
nextLine.trim() === ''
|
|
2660
|
+
) {
|
|
2661
|
+
break;
|
|
2662
|
+
}
|
|
2663
|
+
if (hasBlockExtensionSyntax) {
|
|
2664
|
+
const extensionColumn = blockExtensionColumn(nextLine);
|
|
2665
|
+
if (extensionColumn != null) {
|
|
2666
|
+
const extensionOffset = lineOffsets[index] + extensionColumn;
|
|
2667
|
+
const extension = matchExtensionSyntax(
|
|
2668
|
+
cleaned,
|
|
2669
|
+
extensionOffset,
|
|
2670
|
+
'block',
|
|
2671
|
+
opts,
|
|
2672
|
+
);
|
|
2673
|
+
if (extension.status !== 'none') {
|
|
2674
|
+
blockExtensionMatches.set(extensionOffset, extension);
|
|
2675
|
+
break;
|
|
2676
|
+
}
|
|
2677
|
+
}
|
|
2678
|
+
}
|
|
2679
|
+
paraLines.push(nextLine);
|
|
1508
2680
|
index++;
|
|
1509
2681
|
}
|
|
1510
2682
|
pushBlock({
|
|
@@ -1536,7 +2708,7 @@ function parseMarkdownImpl(
|
|
|
1536
2708
|
* `lineMap` says which input line each surviving line came from.
|
|
1537
2709
|
*/
|
|
1538
2710
|
function stampSourceRanges(
|
|
1539
|
-
blocks:
|
|
2711
|
+
blocks: MarkdownAstBlockContent<RuntimeExtensionNode>[],
|
|
1540
2712
|
blockStartLines: number[],
|
|
1541
2713
|
blockEndLines: (number | undefined)[],
|
|
1542
2714
|
lines: string[],
|
|
@@ -1573,7 +2745,13 @@ function stampSourceRanges(
|
|
|
1573
2745
|
// range that dropped it would slice to something that re-parses
|
|
1574
2746
|
// differently.
|
|
1575
2747
|
const end = lineStart(endLine) + lines[endLine].length;
|
|
1576
|
-
blocks[i] = {
|
|
2748
|
+
blocks[i] = {
|
|
2749
|
+
...blocks[i],
|
|
2750
|
+
position: {
|
|
2751
|
+
start: {offset: lineStart(startLine)},
|
|
2752
|
+
end: {offset: end},
|
|
2753
|
+
},
|
|
2754
|
+
};
|
|
1577
2755
|
}
|
|
1578
2756
|
}
|
|
1579
2757
|
|
|
@@ -1581,10 +2759,17 @@ function stampSourceRanges(
|
|
|
1581
2759
|
// Incremental parsing
|
|
1582
2760
|
// ---------------------------------------------------------------------------
|
|
1583
2761
|
|
|
1584
|
-
|
|
2762
|
+
type IncrementalBlockNode<MathEnabled extends boolean> =
|
|
2763
|
+
MathEnabled extends true ? BlockNodeWithMath : BlockNode;
|
|
2764
|
+
|
|
2765
|
+
declare const incrementalStateMode: unique symbol;
|
|
2766
|
+
|
|
2767
|
+
export interface IncrementalState<MathEnabled extends boolean = false> {
|
|
2768
|
+
/** @internal Nominally couples a factory-created cache to its node union. */
|
|
2769
|
+
readonly [incrementalStateMode]: MathEnabled;
|
|
1585
2770
|
prevInput: string;
|
|
1586
2771
|
settledText: string;
|
|
1587
|
-
settledBlocks:
|
|
2772
|
+
settledBlocks: IncrementalBlockNode<MathEnabled>[];
|
|
1588
2773
|
settledUpTo: number;
|
|
1589
2774
|
/**
|
|
1590
2775
|
* The `autolink` option the cached `settledBlocks` were parsed with.
|
|
@@ -1593,12 +2778,18 @@ export interface IncrementalState {
|
|
|
1593
2778
|
* with newly-arriving content.
|
|
1594
2779
|
*/
|
|
1595
2780
|
autolink?: 'gfm';
|
|
2781
|
+
/** Whether the cached settled blocks were parsed with math enabled. */
|
|
2782
|
+
math?: MathEnabled;
|
|
1596
2783
|
/**
|
|
1597
2784
|
* The `sourceRanges` option the cached `settledBlocks` were parsed with.
|
|
1598
2785
|
* Flipping it invalidates them the same way `autolink` does: they either
|
|
1599
2786
|
* lack the ranges the caller now asks for, or carry ones it did not.
|
|
1600
2787
|
*/
|
|
1601
2788
|
sourceRanges?: boolean;
|
|
2789
|
+
/** Identity of citation sources used by settled nodes. */
|
|
2790
|
+
sourceIdsKey?: string;
|
|
2791
|
+
/** Ordered syntax-only identity used by settled nodes. */
|
|
2792
|
+
pluginSyntaxIdentity?: string;
|
|
1602
2793
|
/**
|
|
1603
2794
|
* Signature of the link reference definitions the cached `settledBlocks`
|
|
1604
2795
|
* were parsed with. Definitions are document-global and typically arrive
|
|
@@ -1629,19 +2820,36 @@ type IncrementalCache = {
|
|
|
1629
2820
|
/** The effective document-global definitions used by slice parses. */
|
|
1630
2821
|
linkDefs: ReadonlyMap<string, string>;
|
|
1631
2822
|
linkDefsKey: string;
|
|
2823
|
+
/** Canonical settled blocks shared by rendering and compatibility projection. */
|
|
2824
|
+
settledAstBlocks: MarkdownAstBlockContent<RuntimeExtensionNode>[];
|
|
2825
|
+
settledRevision: number;
|
|
2826
|
+
projectedRevision: number;
|
|
2827
|
+
projectedSettledBlocks: RuntimeBlockNode[];
|
|
2828
|
+
/** Preserves released settled-node identity across projected snapshots. */
|
|
2829
|
+
projectionCache: LegacyProjectionCache;
|
|
1632
2830
|
work: IncrementalWork;
|
|
1633
2831
|
};
|
|
1634
2832
|
|
|
1635
|
-
const incrementalCaches = new WeakMap<
|
|
2833
|
+
const incrementalCaches = new WeakMap<
|
|
2834
|
+
IncrementalState<boolean>,
|
|
2835
|
+
IncrementalCache
|
|
2836
|
+
>();
|
|
1636
2837
|
|
|
1637
|
-
function makeIncrementalCache(
|
|
1638
|
-
|
|
2838
|
+
function makeIncrementalCache(
|
|
2839
|
+
state: IncrementalState<boolean>,
|
|
2840
|
+
): IncrementalCache {
|
|
2841
|
+
const {defs} = extractLinkDefinitions(state.settledText, state.math);
|
|
1639
2842
|
const cache: IncrementalCache = {
|
|
1640
2843
|
settledEnd: state.settledText.length,
|
|
1641
2844
|
settledLinkDefs: new Map(defs),
|
|
1642
2845
|
tailLinkDefs: new Map(),
|
|
1643
2846
|
linkDefs: defs,
|
|
1644
2847
|
linkDefsKey: linkDefsSignature(defs),
|
|
2848
|
+
settledAstBlocks: [],
|
|
2849
|
+
settledRevision: 0,
|
|
2850
|
+
projectedRevision: -1,
|
|
2851
|
+
projectedSettledBlocks: [],
|
|
2852
|
+
projectionCache: new WeakMap(),
|
|
1645
2853
|
work: {
|
|
1646
2854
|
splitCharacters: 0,
|
|
1647
2855
|
boundaryLines: 0,
|
|
@@ -1653,13 +2861,19 @@ function makeIncrementalCache(state: IncrementalState): IncrementalCache {
|
|
|
1653
2861
|
return cache;
|
|
1654
2862
|
}
|
|
1655
2863
|
|
|
1656
|
-
|
|
1657
|
-
|
|
2864
|
+
/**
|
|
2865
|
+
* Create an incremental parser cache. Use the `<true>` type argument with
|
|
2866
|
+
* `MathParseOptions` so the cache and returned nodes share the math contract.
|
|
2867
|
+
*/
|
|
2868
|
+
export function createIncrementalState<
|
|
2869
|
+
MathEnabled extends boolean = false,
|
|
2870
|
+
>(): IncrementalState<MathEnabled> {
|
|
2871
|
+
const state = {
|
|
1658
2872
|
prevInput: '',
|
|
1659
2873
|
settledText: '',
|
|
1660
2874
|
settledBlocks: [],
|
|
1661
2875
|
settledUpTo: 0,
|
|
1662
|
-
}
|
|
2876
|
+
} as unknown as IncrementalState<MathEnabled>;
|
|
1663
2877
|
makeIncrementalCache(state);
|
|
1664
2878
|
return state;
|
|
1665
2879
|
}
|
|
@@ -1669,7 +2883,7 @@ export function createIncrementalState(): IncrementalState {
|
|
|
1669
2883
|
* @internal Exported from this module for performance regression tests only.
|
|
1670
2884
|
*/
|
|
1671
2885
|
export function getIncrementalParseWork(
|
|
1672
|
-
state: IncrementalState
|
|
2886
|
+
state: IncrementalState<boolean>,
|
|
1673
2887
|
): IncrementalWork {
|
|
1674
2888
|
return (
|
|
1675
2889
|
incrementalCaches.get(state)?.work ?? {
|
|
@@ -1695,47 +2909,91 @@ export function getIncrementalParseWork(
|
|
|
1695
2909
|
* the boundary to -1 even though the content before the fence opened cannot be
|
|
1696
2910
|
* changed by anything typed inside it.
|
|
1697
2911
|
*/
|
|
1698
|
-
function findSettledBoundary(
|
|
2912
|
+
function findSettledBoundary(
|
|
2913
|
+
lines: string[],
|
|
2914
|
+
math = false,
|
|
2915
|
+
): {
|
|
1699
2916
|
boundary: number;
|
|
1700
2917
|
openFence: boolean;
|
|
2918
|
+
openMath: boolean;
|
|
1701
2919
|
} {
|
|
1702
2920
|
let inFence = false;
|
|
1703
2921
|
let fenceMarker = '';
|
|
2922
|
+
let mathContainer: DisplayMathContainer | null = null;
|
|
2923
|
+
let suppressMathUntilBoundary = false;
|
|
1704
2924
|
let lastBoundary = -1;
|
|
1705
2925
|
let boundaryBeforeFence = -1;
|
|
2926
|
+
let boundaryBeforeMath = -1;
|
|
1706
2927
|
|
|
1707
2928
|
for (let lineIndex = 0; lineIndex < lines.length; lineIndex++) {
|
|
1708
2929
|
const line = lines[lineIndex];
|
|
1709
2930
|
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
inFence = true;
|
|
1715
|
-
fenceMarker = fenceMatch[1];
|
|
1716
|
-
boundaryBeforeFence = lastBoundary;
|
|
1717
|
-
} else if (
|
|
2931
|
+
if (inFence) {
|
|
2932
|
+
const fenceMatch = line.match(/^(`{3,}|~{3,})/);
|
|
2933
|
+
if (
|
|
2934
|
+
fenceMatch &&
|
|
1718
2935
|
fenceMatch[1].startsWith(fenceMarker[0]) &&
|
|
1719
2936
|
fenceMatch[1].length >= fenceMarker.length
|
|
1720
2937
|
) {
|
|
1721
2938
|
inFence = false;
|
|
1722
2939
|
fenceMarker = '';
|
|
1723
2940
|
}
|
|
2941
|
+
continue;
|
|
1724
2942
|
}
|
|
1725
2943
|
|
|
1726
|
-
if (
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
2944
|
+
if (mathContainer != null) {
|
|
2945
|
+
const state = displayMathLineState(line, mathContainer);
|
|
2946
|
+
if (state === 'close') {
|
|
2947
|
+
mathContainer = null;
|
|
2948
|
+
continue;
|
|
2949
|
+
}
|
|
2950
|
+
if (state === 'inside') {
|
|
2951
|
+
continue;
|
|
2952
|
+
}
|
|
2953
|
+
// The list item or blockquote ended before a closer arrived. The parser
|
|
2954
|
+
// treats that unmatched opener literally, so resume ordinary boundary
|
|
2955
|
+
// detection on this first line outside the container.
|
|
2956
|
+
mathContainer = null;
|
|
2957
|
+
suppressMathUntilBoundary = true;
|
|
2958
|
+
}
|
|
2959
|
+
|
|
2960
|
+
// A complete same-line `$$…$$` expression never changes boundary state.
|
|
2961
|
+
// A standalone marker may belong to the top level, a blockquote, or one
|
|
2962
|
+
// list item; remember that container so its continuation marker closes the
|
|
2963
|
+
// same expression instead of opening a new one.
|
|
2964
|
+
if (math && !suppressMathUntilBoundary) {
|
|
2965
|
+
const container = displayMathContainer(line);
|
|
2966
|
+
if (container != null) {
|
|
2967
|
+
mathContainer = container;
|
|
2968
|
+
boundaryBeforeMath = lastBoundary;
|
|
2969
|
+
continue;
|
|
2970
|
+
}
|
|
2971
|
+
}
|
|
2972
|
+
|
|
2973
|
+
const fenceMatch = line.match(/^(`{3,}|~{3,})/);
|
|
2974
|
+
if (fenceMatch) {
|
|
2975
|
+
inFence = true;
|
|
2976
|
+
fenceMarker = fenceMatch[1];
|
|
2977
|
+
boundaryBeforeFence = lastBoundary;
|
|
2978
|
+
continue;
|
|
2979
|
+
}
|
|
2980
|
+
|
|
2981
|
+
if (line.trim() === '') {
|
|
2982
|
+
suppressMathUntilBoundary = false;
|
|
2983
|
+
if (lineIndex > 0 && lineIndex < lines.length - 1) {
|
|
2984
|
+
lastBoundary = lineIndex;
|
|
2985
|
+
}
|
|
1733
2986
|
}
|
|
1734
2987
|
}
|
|
1735
2988
|
|
|
1736
2989
|
return {
|
|
1737
|
-
boundary: inFence
|
|
2990
|
+
boundary: inFence
|
|
2991
|
+
? boundaryBeforeFence
|
|
2992
|
+
: mathContainer != null
|
|
2993
|
+
? boundaryBeforeMath
|
|
2994
|
+
: lastBoundary,
|
|
1738
2995
|
openFence: inFence,
|
|
2996
|
+
openMath: mathContainer != null,
|
|
1739
2997
|
};
|
|
1740
2998
|
}
|
|
1741
2999
|
|
|
@@ -1743,10 +3001,55 @@ function findSettledBoundary(lines: string[]): {
|
|
|
1743
3001
|
* Strip trailing incomplete inline syntax that appears during streaming.
|
|
1744
3002
|
* Only affects the tail of the last line — safe to apply to the full string.
|
|
1745
3003
|
*/
|
|
1746
|
-
export function trimStreamingArtifacts(
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
3004
|
+
export function trimStreamingArtifacts(
|
|
3005
|
+
input: string,
|
|
3006
|
+
options?: {math?: boolean},
|
|
3007
|
+
): string {
|
|
3008
|
+
// First remove an incomplete display expression as one structural unit. This
|
|
3009
|
+
// full-input scan distinguishes a terminal nested closer from a new opener;
|
|
3010
|
+
// looking only at the final `$$` line cannot.
|
|
3011
|
+
const displayTrimmed = options?.math ? trimOpenDisplayMath(input) : input;
|
|
3012
|
+
const lastNL = displayTrimmed.lastIndexOf('\n');
|
|
3013
|
+
const prefix = lastNL === -1 ? '' : displayTrimmed.slice(0, lastNL + 1);
|
|
3014
|
+
let tail = lastNL === -1 ? displayTrimmed : displayTrimmed.slice(lastNL + 1);
|
|
3015
|
+
|
|
3016
|
+
if (options?.math) {
|
|
3017
|
+
// Hold an unmatched inline opener so raw TeX syntax does not flash while
|
|
3018
|
+
// streaming. If another unescaped dollar is already present but fails the
|
|
3019
|
+
// closing-boundary rule, keep both literal (the currency case).
|
|
3020
|
+
for (let index = 0; index < tail.length; index++) {
|
|
3021
|
+
if (
|
|
3022
|
+
tail[index] === '$' &&
|
|
3023
|
+
tail[index + 1] == null &&
|
|
3024
|
+
tail[index - 1] !== '$' &&
|
|
3025
|
+
!/\d/.test(tail[index - 1] ?? '') &&
|
|
3026
|
+
!isEscaped(tail, index)
|
|
3027
|
+
) {
|
|
3028
|
+
tail = tail.slice(0, index);
|
|
3029
|
+
break;
|
|
3030
|
+
}
|
|
3031
|
+
if (!isInlineMathStart(tail, index)) {
|
|
3032
|
+
continue;
|
|
3033
|
+
}
|
|
3034
|
+
const close = findInlineMathEnd(tail, index);
|
|
3035
|
+
if (close !== -1) {
|
|
3036
|
+
index = close;
|
|
3037
|
+
continue;
|
|
3038
|
+
}
|
|
3039
|
+
let laterDollar = -1;
|
|
3040
|
+
for (let next = index + 1; next < tail.length; next++) {
|
|
3041
|
+
if (tail[next] === '$' && !isEscaped(tail, next)) {
|
|
3042
|
+
laterDollar = next;
|
|
3043
|
+
break;
|
|
3044
|
+
}
|
|
3045
|
+
}
|
|
3046
|
+
if (laterDollar === -1) {
|
|
3047
|
+
tail = tail.slice(0, index);
|
|
3048
|
+
break;
|
|
3049
|
+
}
|
|
3050
|
+
index = laterDollar;
|
|
3051
|
+
}
|
|
3052
|
+
}
|
|
1750
3053
|
|
|
1751
3054
|
// Scan backwards for unclosed syntax markers — no regex to avoid ReDoS
|
|
1752
3055
|
// Find the last unclosed [ or ![ (link/image start)
|
|
@@ -1904,6 +3207,67 @@ export function trimStreamingArtifacts(input: string): string {
|
|
|
1904
3207
|
* Once a table is established (header + separator exist), new data rows
|
|
1905
3208
|
* render immediately — no suppression.
|
|
1906
3209
|
*/
|
|
3210
|
+
function trimOpenDisplayMath(text: string): string {
|
|
3211
|
+
const lines = text.split('\n');
|
|
3212
|
+
let inFence = false;
|
|
3213
|
+
let fenceMarker = '';
|
|
3214
|
+
let mathContainer: DisplayMathContainer | null = null;
|
|
3215
|
+
let suppressMathUntilBoundary = false;
|
|
3216
|
+
let mathStartLine = -1;
|
|
3217
|
+
|
|
3218
|
+
for (let index = 0; index < lines.length; index++) {
|
|
3219
|
+
const line = lines[index];
|
|
3220
|
+
if (inFence) {
|
|
3221
|
+
const fence = line.match(/^(`{3,}|~{3,})/);
|
|
3222
|
+
if (
|
|
3223
|
+
fence != null &&
|
|
3224
|
+
fence[1].startsWith(fenceMarker[0]) &&
|
|
3225
|
+
fence[1].length >= fenceMarker.length
|
|
3226
|
+
) {
|
|
3227
|
+
inFence = false;
|
|
3228
|
+
fenceMarker = '';
|
|
3229
|
+
}
|
|
3230
|
+
continue;
|
|
3231
|
+
}
|
|
3232
|
+
if (mathContainer != null) {
|
|
3233
|
+
const state = displayMathLineState(line, mathContainer);
|
|
3234
|
+
if (state === 'close') {
|
|
3235
|
+
mathContainer = null;
|
|
3236
|
+
mathStartLine = -1;
|
|
3237
|
+
continue;
|
|
3238
|
+
}
|
|
3239
|
+
if (state === 'inside') {
|
|
3240
|
+
continue;
|
|
3241
|
+
}
|
|
3242
|
+
mathContainer = null;
|
|
3243
|
+
mathStartLine = -1;
|
|
3244
|
+
suppressMathUntilBoundary = true;
|
|
3245
|
+
}
|
|
3246
|
+
|
|
3247
|
+
if (line.trim() === '') {
|
|
3248
|
+
suppressMathUntilBoundary = false;
|
|
3249
|
+
}
|
|
3250
|
+
|
|
3251
|
+
const fence = line.match(/^(`{3,}|~{3,})/);
|
|
3252
|
+
if (fence != null) {
|
|
3253
|
+
inFence = true;
|
|
3254
|
+
fenceMarker = fence[1];
|
|
3255
|
+
continue;
|
|
3256
|
+
}
|
|
3257
|
+
const container = suppressMathUntilBoundary
|
|
3258
|
+
? null
|
|
3259
|
+
: displayMathContainer(line);
|
|
3260
|
+
if (container != null) {
|
|
3261
|
+
mathContainer = container;
|
|
3262
|
+
mathStartLine = index;
|
|
3263
|
+
}
|
|
3264
|
+
}
|
|
3265
|
+
|
|
3266
|
+
return mathContainer != null && mathStartLine >= 0
|
|
3267
|
+
? lines.slice(0, mathStartLine).join('\n').trimEnd()
|
|
3268
|
+
: text;
|
|
3269
|
+
}
|
|
3270
|
+
|
|
1907
3271
|
function trimUnsettledStructural(text: string): string {
|
|
1908
3272
|
const lines = text.split('\n');
|
|
1909
3273
|
|
|
@@ -1970,7 +3334,7 @@ function trimUnsettledStructural(text: string): string {
|
|
|
1970
3334
|
* rather than into the slice.
|
|
1971
3335
|
*/
|
|
1972
3336
|
function atOffset(opts: ResolvedOptions, offset: number): ResolvedOptions {
|
|
1973
|
-
return opts.
|
|
3337
|
+
return opts.astPositions === true ? {...opts, baseOffset: offset} : opts;
|
|
1974
3338
|
}
|
|
1975
3339
|
|
|
1976
3340
|
/**
|
|
@@ -1981,9 +3345,9 @@ function atOffset(opts: ResolvedOptions, offset: number): ResolvedOptions {
|
|
|
1981
3345
|
* lists even though the full-text parser joins them per CommonMark §5.3.
|
|
1982
3346
|
*/
|
|
1983
3347
|
function mergeSettledBlocks(
|
|
1984
|
-
prev:
|
|
1985
|
-
delta:
|
|
1986
|
-
):
|
|
3348
|
+
prev: MarkdownAstBlockContent<RuntimeExtensionNode>[],
|
|
3349
|
+
delta: MarkdownAstBlockContent<RuntimeExtensionNode>[],
|
|
3350
|
+
): MarkdownAstBlockContent<RuntimeExtensionNode>[] {
|
|
1987
3351
|
if (prev.length === 0 || delta.length === 0) {
|
|
1988
3352
|
return [...prev, ...delta];
|
|
1989
3353
|
}
|
|
@@ -1995,17 +3359,21 @@ function mergeSettledBlocks(
|
|
|
1995
3359
|
prevLast.ordered === deltaFirst.ordered &&
|
|
1996
3360
|
prevLast.delimiter === deltaFirst.delimiter
|
|
1997
3361
|
) {
|
|
1998
|
-
const merged:
|
|
3362
|
+
const merged: MarkdownAstBlockContent<RuntimeExtensionNode> = {
|
|
1999
3363
|
type: 'list',
|
|
2000
3364
|
ordered: prevLast.ordered,
|
|
2001
3365
|
start: prevLast.start,
|
|
2002
3366
|
delimiter: prevLast.delimiter,
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
// One list now, so one
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
3367
|
+
spread: true,
|
|
3368
|
+
children: [...prevLast.children, ...deltaFirst.children],
|
|
3369
|
+
// One list now, so one position spans both halves.
|
|
3370
|
+
...(prevLast.position != null && deltaFirst.position != null
|
|
3371
|
+
? {
|
|
3372
|
+
position: {
|
|
3373
|
+
start: prevLast.position.start,
|
|
3374
|
+
end: deltaFirst.position.end,
|
|
3375
|
+
},
|
|
3376
|
+
}
|
|
2009
3377
|
: null),
|
|
2010
3378
|
};
|
|
2011
3379
|
return [...prev.slice(0, -1), merged, ...delta.slice(1)];
|
|
@@ -2014,13 +3382,16 @@ function mergeSettledBlocks(
|
|
|
2014
3382
|
}
|
|
2015
3383
|
|
|
2016
3384
|
/** Append a newly-settled slice without copying the already-settled prefix. */
|
|
2017
|
-
function appendSettledBlocks(
|
|
3385
|
+
function appendSettledBlocks(
|
|
3386
|
+
prev: MarkdownAstBlockContent<RuntimeExtensionNode>[],
|
|
3387
|
+
delta: MarkdownAstBlockContent<RuntimeExtensionNode>[],
|
|
3388
|
+
): boolean {
|
|
2018
3389
|
if (delta.length === 0) {
|
|
2019
|
-
return;
|
|
3390
|
+
return false;
|
|
2020
3391
|
}
|
|
2021
3392
|
if (prev.length === 0) {
|
|
2022
3393
|
prev.push(...delta);
|
|
2023
|
-
return;
|
|
3394
|
+
return false;
|
|
2024
3395
|
}
|
|
2025
3396
|
const prevLast = prev[prev.length - 1];
|
|
2026
3397
|
const deltaFirst = delta[0];
|
|
@@ -2035,16 +3406,22 @@ function appendSettledBlocks(prev: BlockNode[], delta: BlockNode[]): void {
|
|
|
2035
3406
|
ordered: prevLast.ordered,
|
|
2036
3407
|
start: prevLast.start,
|
|
2037
3408
|
delimiter: prevLast.delimiter,
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
...(prevLast.
|
|
2041
|
-
? {
|
|
3409
|
+
spread: true,
|
|
3410
|
+
children: [...prevLast.children, ...deltaFirst.children],
|
|
3411
|
+
...(prevLast.position != null && deltaFirst.position != null
|
|
3412
|
+
? {
|
|
3413
|
+
position: {
|
|
3414
|
+
start: prevLast.position.start,
|
|
3415
|
+
end: deltaFirst.position.end,
|
|
3416
|
+
},
|
|
3417
|
+
}
|
|
2042
3418
|
: null),
|
|
2043
3419
|
};
|
|
2044
3420
|
prev.push(...delta.slice(1));
|
|
2045
|
-
return;
|
|
3421
|
+
return true;
|
|
2046
3422
|
}
|
|
2047
3423
|
prev.push(...delta);
|
|
3424
|
+
return false;
|
|
2048
3425
|
}
|
|
2049
3426
|
|
|
2050
3427
|
function sameUnsettledDefinitions(
|
|
@@ -2066,7 +3443,7 @@ function sameUnsettledDefinitions(
|
|
|
2066
3443
|
}
|
|
2067
3444
|
|
|
2068
3445
|
function resetIncrementalCache(
|
|
2069
|
-
state: IncrementalState
|
|
3446
|
+
state: IncrementalState<boolean>,
|
|
2070
3447
|
cache: IncrementalCache,
|
|
2071
3448
|
): void {
|
|
2072
3449
|
state.prevInput = '';
|
|
@@ -2074,11 +3451,19 @@ function resetIncrementalCache(
|
|
|
2074
3451
|
state.settledBlocks = [];
|
|
2075
3452
|
state.settledUpTo = 0;
|
|
2076
3453
|
state.linkDefsKey = undefined;
|
|
3454
|
+
state.sourceIdsKey = undefined;
|
|
3455
|
+
state.pluginSyntaxIdentity = undefined;
|
|
3456
|
+
state.math = undefined;
|
|
2077
3457
|
cache.settledEnd = 0;
|
|
2078
3458
|
cache.settledLinkDefs.clear();
|
|
2079
3459
|
cache.tailLinkDefs = new Map();
|
|
2080
3460
|
cache.linkDefs = new Map();
|
|
2081
3461
|
cache.linkDefsKey = '';
|
|
3462
|
+
cache.settledAstBlocks = [];
|
|
3463
|
+
cache.settledRevision++;
|
|
3464
|
+
cache.projectedRevision = -1;
|
|
3465
|
+
cache.projectedSettledBlocks = [];
|
|
3466
|
+
cache.projectionCache = new WeakMap();
|
|
2082
3467
|
cache.work = {
|
|
2083
3468
|
splitCharacters: 0,
|
|
2084
3469
|
boundaryLines: 0,
|
|
@@ -2100,39 +3485,108 @@ function resetIncrementalCache(
|
|
|
2100
3485
|
*/
|
|
2101
3486
|
export function parseMarkdownIncremental(
|
|
2102
3487
|
input: string,
|
|
2103
|
-
state: IncrementalState
|
|
3488
|
+
state: IncrementalState<false>,
|
|
2104
3489
|
sourceIds?: ReadonlySet<string>,
|
|
2105
3490
|
): BlockNode[];
|
|
2106
3491
|
export function parseMarkdownIncremental(
|
|
2107
3492
|
input: string,
|
|
2108
|
-
state: IncrementalState
|
|
2109
|
-
options:
|
|
3493
|
+
state: IncrementalState<true>,
|
|
3494
|
+
options: IncrementalMathParseOptionsWithoutPlugins,
|
|
3495
|
+
): BlockNodeWithMath[];
|
|
3496
|
+
export function parseMarkdownIncremental(
|
|
3497
|
+
input: string,
|
|
3498
|
+
state: IncrementalState<false>,
|
|
3499
|
+
options: IncrementalParseOptionsWithoutPlugins,
|
|
2110
3500
|
): BlockNode[];
|
|
3501
|
+
export function parseMarkdownIncremental<
|
|
3502
|
+
const Plugins extends ReadonlyArray<MarkdownPluginEntry>,
|
|
3503
|
+
>(
|
|
3504
|
+
input: string,
|
|
3505
|
+
state: IncrementalState<false>,
|
|
3506
|
+
options: IncrementalParseOptionsWithoutPlugins & {plugins: Plugins},
|
|
3507
|
+
): BlockNode<MarkdownExtensionsOf<Plugins>>[];
|
|
3508
|
+
export function parseMarkdownIncremental<
|
|
3509
|
+
const Plugins extends ReadonlyArray<MarkdownPluginEntry>,
|
|
3510
|
+
>(
|
|
3511
|
+
input: string,
|
|
3512
|
+
state: IncrementalState<true>,
|
|
3513
|
+
options: IncrementalMathParseOptionsWithoutPlugins & {plugins: Plugins},
|
|
3514
|
+
): BlockNodeWithMath<MarkdownExtensionsOf<Plugins>>[];
|
|
2111
3515
|
export function parseMarkdownIncremental(
|
|
2112
3516
|
input: string,
|
|
2113
|
-
state: IncrementalState
|
|
2114
|
-
arg?: ReadonlySet<string> |
|
|
2115
|
-
):
|
|
2116
|
-
const
|
|
3517
|
+
state: IncrementalState<boolean>,
|
|
3518
|
+
arg?: ReadonlySet<string> | RuntimeParseOptions,
|
|
3519
|
+
): RuntimeBlockNode[] {
|
|
3520
|
+
const withRanges = wantsLegacyRanges(arg);
|
|
3521
|
+
const root = parseMarkdownAstIncremental(input, state, arg);
|
|
3522
|
+
const cache = incrementalCaches.get(state) ?? makeIncrementalCache(state);
|
|
3523
|
+
const projected = projectMarkdownRoot(
|
|
3524
|
+
root,
|
|
3525
|
+
withRanges,
|
|
3526
|
+
cache.projectionCache,
|
|
3527
|
+
);
|
|
3528
|
+
if (cache.projectedRevision !== cache.settledRevision) {
|
|
3529
|
+
cache.projectedSettledBlocks = cache.settledAstBlocks.map(block =>
|
|
3530
|
+
projectBlockNode(block, cache.projectionCache, withRanges),
|
|
3531
|
+
);
|
|
3532
|
+
cache.projectedRevision = cache.settledRevision;
|
|
3533
|
+
}
|
|
3534
|
+
state.settledBlocks =
|
|
3535
|
+
cache.projectedSettledBlocks as typeof state.settledBlocks;
|
|
3536
|
+
return projected;
|
|
3537
|
+
}
|
|
3538
|
+
|
|
3539
|
+
/** @internal Canonical incremental parse used by Markdown rendering. */
|
|
3540
|
+
export function parseMarkdownAstIncremental(
|
|
3541
|
+
input: string,
|
|
3542
|
+
state: IncrementalState<boolean>,
|
|
3543
|
+
arg?: ReadonlySet<string> | RuntimeParseOptions,
|
|
3544
|
+
): MarkdownAstRoot<RuntimeExtensionNode> {
|
|
3545
|
+
const opts = resolveOptions(arg, true);
|
|
3546
|
+
const root: MarkdownAstRoot<RuntimeExtensionNode> = {
|
|
3547
|
+
type: 'root',
|
|
3548
|
+
children: parseMarkdownIncrementalAstBlocks(input, state, opts),
|
|
3549
|
+
};
|
|
3550
|
+
return applyMarkdownTransforms(
|
|
3551
|
+
root,
|
|
3552
|
+
opts.plugins,
|
|
3553
|
+
input,
|
|
3554
|
+
opts.isFinal,
|
|
3555
|
+
'block',
|
|
3556
|
+
);
|
|
3557
|
+
}
|
|
3558
|
+
|
|
3559
|
+
function parseMarkdownIncrementalAstBlocks(
|
|
3560
|
+
input: string,
|
|
3561
|
+
state: IncrementalState<boolean>,
|
|
3562
|
+
opts: ResolvedOptions,
|
|
3563
|
+
): MarkdownAstBlockContent<RuntimeExtensionNode>[] {
|
|
2117
3564
|
const cache = incrementalCaches.get(state) ?? makeIncrementalCache(state);
|
|
2118
3565
|
let reparseSettled = false;
|
|
2119
3566
|
|
|
2120
|
-
|
|
3567
|
+
const nextSourceIdsKey = sourceIdsSignature(opts.sourceIds);
|
|
3568
|
+
const nextPluginSyntaxIdentity = opts.plugins?.syntaxIdentity ?? '';
|
|
3569
|
+
// Invalidate cache when an option that changes parsed nodes flips — cached
|
|
2121
3570
|
// settled blocks were parsed with the previous setting and would otherwise
|
|
2122
3571
|
// be reused unchanged.
|
|
2123
3572
|
if (
|
|
2124
3573
|
state.autolink !== opts.autolink ||
|
|
2125
|
-
|
|
3574
|
+
state.math !== opts.math ||
|
|
3575
|
+
Boolean(state.sourceRanges) !== Boolean(opts.sourceRanges) ||
|
|
3576
|
+
state.sourceIdsKey !== nextSourceIdsKey ||
|
|
3577
|
+
state.pluginSyntaxIdentity !== nextPluginSyntaxIdentity
|
|
2126
3578
|
) {
|
|
2127
3579
|
reparseSettled = true;
|
|
2128
3580
|
state.autolink = opts.autolink;
|
|
3581
|
+
state.math = opts.math;
|
|
2129
3582
|
state.sourceRanges = opts.sourceRanges;
|
|
3583
|
+
state.sourceIdsKey = nextSourceIdsKey;
|
|
3584
|
+
state.pluginSyntaxIdentity = nextPluginSyntaxIdentity;
|
|
2130
3585
|
}
|
|
2131
3586
|
if (input === '') {
|
|
2132
3587
|
resetIncrementalCache(state, cache);
|
|
2133
3588
|
return [];
|
|
2134
3589
|
}
|
|
2135
|
-
|
|
2136
3590
|
// The settled prefix is only reusable while the input still contains it
|
|
2137
3591
|
// verbatim. Lengths alone cannot tell: a same-length or longer replacement
|
|
2138
3592
|
// (new args after reusing a state) disagrees with the prefix without ever
|
|
@@ -2147,7 +3601,10 @@ export function parseMarkdownIncremental(
|
|
|
2147
3601
|
) {
|
|
2148
3602
|
resetIncrementalCache(state, cache);
|
|
2149
3603
|
state.autolink = opts.autolink;
|
|
3604
|
+
state.math = opts.math;
|
|
2150
3605
|
state.sourceRanges = opts.sourceRanges;
|
|
3606
|
+
state.sourceIdsKey = nextSourceIdsKey;
|
|
3607
|
+
state.pluginSyntaxIdentity = nextPluginSyntaxIdentity;
|
|
2151
3608
|
}
|
|
2152
3609
|
|
|
2153
3610
|
// The recurring parse costs are confined to the mutable suffix: splitting,
|
|
@@ -2156,7 +3613,10 @@ export function parseMarkdownIncremental(
|
|
|
2156
3613
|
const oldSettledEnd = cache.settledEnd;
|
|
2157
3614
|
const tailRaw = input.slice(oldSettledEnd);
|
|
2158
3615
|
const tailLines = tailRaw.split('\n');
|
|
2159
|
-
const {boundary, openFence} = findSettledBoundary(
|
|
3616
|
+
const {boundary, openFence, openMath} = findSettledBoundary(
|
|
3617
|
+
tailLines,
|
|
3618
|
+
opts.math,
|
|
3619
|
+
);
|
|
2160
3620
|
const settledDelta =
|
|
2161
3621
|
boundary >= 0 ? tailLines.slice(0, boundary).join('\n') : '';
|
|
2162
3622
|
const nextSettledEnd = oldSettledEnd + settledDelta.length;
|
|
@@ -2168,7 +3628,7 @@ export function parseMarkdownIncremental(
|
|
|
2168
3628
|
// blocks: document-global references may precede their footer definition.
|
|
2169
3629
|
let definitionsChanged = false;
|
|
2170
3630
|
if (settledDelta !== '') {
|
|
2171
|
-
const {defs: deltaDefs} = extractLinkDefinitions(settledDelta);
|
|
3631
|
+
const {defs: deltaDefs} = extractLinkDefinitions(settledDelta, opts.math);
|
|
2172
3632
|
for (const [label, destination] of deltaDefs) {
|
|
2173
3633
|
if (!cache.settledLinkDefs.has(label)) {
|
|
2174
3634
|
cache.settledLinkDefs.set(label, destination);
|
|
@@ -2178,7 +3638,10 @@ export function parseMarkdownIncremental(
|
|
|
2178
3638
|
}
|
|
2179
3639
|
}
|
|
2180
3640
|
}
|
|
2181
|
-
const {defs: tailLinkDefs} = extractLinkDefinitions(
|
|
3641
|
+
const {defs: tailLinkDefs} = extractLinkDefinitions(
|
|
3642
|
+
unsettledInput,
|
|
3643
|
+
opts.math,
|
|
3644
|
+
);
|
|
2182
3645
|
if (
|
|
2183
3646
|
!sameUnsettledDefinitions(
|
|
2184
3647
|
cache.tailLinkDefs,
|
|
@@ -2209,36 +3672,69 @@ export function parseMarkdownIncremental(
|
|
|
2209
3672
|
cache.settledEnd = nextSettledEnd;
|
|
2210
3673
|
}
|
|
2211
3674
|
|
|
2212
|
-
const
|
|
3675
|
+
const trimmedUnsettledInput = unsettledInput.trim();
|
|
3676
|
+
// String#trim removes the CR that belongs to the final content line of a
|
|
3677
|
+
// CRLF snapshot along with trailing blank lines. Keep that one byte so
|
|
3678
|
+
// source ranges and delimiter content remain identical to a full parse.
|
|
3679
|
+
const unsettledRaw =
|
|
3680
|
+
trimmedUnsettledInput !== '' && /\r(?:\n[\s]*)?$/.test(unsettledInput)
|
|
3681
|
+
? `${trimmedUnsettledInput}\r`
|
|
3682
|
+
: trimmedUnsettledInput;
|
|
2213
3683
|
// Structural trimming holds back lines that look like an incomplete list or
|
|
2214
3684
|
// table, which inside a fence is ordinary code: a TypeScript union or a `- `
|
|
2215
3685
|
// would disappear from the code block as it streams.
|
|
2216
3686
|
const unsettledText = openFence
|
|
2217
3687
|
? unsettledRaw
|
|
2218
|
-
:
|
|
3688
|
+
: openMath
|
|
3689
|
+
? trimOpenDisplayMath(unsettledRaw)
|
|
3690
|
+
: trimUnsettledStructural(unsettledRaw);
|
|
2219
3691
|
|
|
3692
|
+
const legacyRanges = opts.sourceRanges === true;
|
|
2220
3693
|
let parsedSettledBlocks = 0;
|
|
2221
3694
|
if (reparseSettled) {
|
|
2222
|
-
|
|
3695
|
+
cache.settledAstBlocks = state.settledText
|
|
2223
3696
|
? parseMarkdownImpl(state.settledText, parseOpts)
|
|
2224
3697
|
: [];
|
|
2225
|
-
parsedSettledBlocks =
|
|
3698
|
+
parsedSettledBlocks = cache.settledAstBlocks.length;
|
|
3699
|
+
cache.settledRevision++;
|
|
3700
|
+
cache.projectedSettledBlocks = cache.settledAstBlocks.map(block =>
|
|
3701
|
+
projectBlockNode(block, cache.projectionCache, legacyRanges),
|
|
3702
|
+
);
|
|
3703
|
+
cache.projectedRevision = cache.settledRevision;
|
|
2226
3704
|
} else if (settledDelta !== '') {
|
|
2227
3705
|
const deltaBlocks = parseMarkdownImpl(
|
|
2228
3706
|
settledDelta,
|
|
2229
3707
|
atOffset(parseOpts, oldSettledEnd),
|
|
2230
3708
|
);
|
|
2231
|
-
|
|
3709
|
+
const mergeIndex = cache.settledAstBlocks.length - 1;
|
|
3710
|
+
const mergedList = appendSettledBlocks(cache.settledAstBlocks, deltaBlocks);
|
|
2232
3711
|
parsedSettledBlocks = deltaBlocks.length;
|
|
3712
|
+
cache.settledRevision++;
|
|
3713
|
+
const projectedDelta = deltaBlocks.map(block =>
|
|
3714
|
+
projectBlockNode(block, cache.projectionCache, legacyRanges),
|
|
3715
|
+
);
|
|
3716
|
+
if (mergedList) {
|
|
3717
|
+
const canonicalMerged = cache.settledAstBlocks[mergeIndex];
|
|
3718
|
+
const projectedMerged = projectBlockNode(
|
|
3719
|
+
canonicalMerged,
|
|
3720
|
+
cache.projectionCache,
|
|
3721
|
+
legacyRanges,
|
|
3722
|
+
);
|
|
3723
|
+
cache.projectedSettledBlocks[mergeIndex] = projectedMerged;
|
|
3724
|
+
cache.projectedSettledBlocks.push(...projectedDelta.slice(1));
|
|
3725
|
+
} else {
|
|
3726
|
+
cache.projectedSettledBlocks.push(...projectedDelta);
|
|
3727
|
+
}
|
|
3728
|
+
cache.projectedRevision = cache.settledRevision;
|
|
2233
3729
|
}
|
|
2234
3730
|
|
|
2235
3731
|
// The unsettled tail is trimmed before parsing, so its offset in the
|
|
2236
3732
|
// document is where that trimmed text actually starts — not the boundary,
|
|
2237
|
-
// which is a line index. If it somehow
|
|
2238
|
-
//
|
|
2239
|
-
//
|
|
3733
|
+
// which is a line index. If it somehow cannot be located, parse it without
|
|
3734
|
+
// positions rather than report wrong ones. The search starts at the
|
|
3735
|
+
// settled end, so it scans the tail and never the prefix.
|
|
2240
3736
|
const unsettledStart =
|
|
2241
|
-
unsettledText && opts.
|
|
3737
|
+
unsettledText && opts.astPositions === true
|
|
2242
3738
|
? input.indexOf(unsettledText, cache.settledEnd)
|
|
2243
3739
|
: -1;
|
|
2244
3740
|
const unsettledBlocks = unsettledText
|
|
@@ -2263,7 +3759,7 @@ export function parseMarkdownIncremental(
|
|
|
2263
3759
|
definitionCharacters: settledDelta.length + unsettledInput.length,
|
|
2264
3760
|
renderedBlocks: parsedSettledBlocks + unsettledBlocks.length,
|
|
2265
3761
|
};
|
|
2266
|
-
return mergeSettledBlocks(
|
|
3762
|
+
return mergeSettledBlocks(cache.settledAstBlocks, unsettledBlocks);
|
|
2267
3763
|
}
|
|
2268
3764
|
|
|
2269
3765
|
// ---------------------------------------------------------------------------
|
|
@@ -2274,29 +3770,6 @@ export function parseMarkdownIncremental(
|
|
|
2274
3770
|
// derives its item ids from the same functions, so outline hash links always
|
|
2275
3771
|
// resolve to a rendered heading by construction.
|
|
2276
3772
|
|
|
2277
|
-
/** Flatten inline nodes into their plain text content. */
|
|
2278
|
-
export function inlineText(nodes: InlineNode[]): string {
|
|
2279
|
-
return nodes
|
|
2280
|
-
.map(node => {
|
|
2281
|
-
switch (node.type) {
|
|
2282
|
-
case 'text':
|
|
2283
|
-
case 'code':
|
|
2284
|
-
return node.content;
|
|
2285
|
-
case 'bold':
|
|
2286
|
-
case 'italic':
|
|
2287
|
-
case 'strikethrough':
|
|
2288
|
-
case 'link':
|
|
2289
|
-
return inlineText(node.children);
|
|
2290
|
-
case 'image':
|
|
2291
|
-
return node.alt;
|
|
2292
|
-
case 'citation':
|
|
2293
|
-
case 'break':
|
|
2294
|
-
return '';
|
|
2295
|
-
}
|
|
2296
|
-
})
|
|
2297
|
-
.join('');
|
|
2298
|
-
}
|
|
2299
|
-
|
|
2300
3773
|
/** Turn heading text into a URL-safe slug (lowercase, hyphen-separated). */
|
|
2301
3774
|
export function slugify(value: string): string {
|
|
2302
3775
|
return value
|