@astryxdesign/core 0.6.1 → 0.6.2-canary.0faf070
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/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/DateRangeInput/DateRangeInput.d.ts +3 -0
- package/dist/DateRangeInput/DateRangeInput.d.ts.map +1 -1
- package/dist/DateRangeInput/DateRangeInput.js +16 -9
- 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/FileInput/FileInput.d.ts.map +1 -1
- package/dist/FileInput/FileInput.js +9 -3
- 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 +10 -2
- package/dist/Markdown/Markdown.d.ts.map +1 -1
- package/dist/Markdown/Markdown.js +58 -14
- package/dist/Markdown/index.d.ts +1 -1
- package/dist/Markdown/index.d.ts.map +1 -1
- package/dist/Markdown/parser.d.ts +126 -12
- package/dist/Markdown/parser.d.ts.map +1 -1
- package/dist/Markdown/parser.js +369 -34
- package/dist/Markdown/utils.d.ts +1 -1
- package/dist/Markdown/utils.d.ts.map +1 -1
- package/dist/PowerSearch/PowerSearchEditPopover.d.ts.map +1 -1
- package/dist/PowerSearch/PowerSearchEditPopover.js +46 -30
- 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/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 +23 -15
- 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/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 +15 -6
- package/dist/astryx.css +13 -2
- 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 +7 -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/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/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 +29 -20
- 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/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 +13 -3
- 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/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 +167 -42
- package/src/Markdown/Markdown.public.test.ts +157 -0
- package/src/Markdown/Markdown.spec.md +255 -71
- package/src/Markdown/Markdown.test.tsx +107 -3
- package/src/Markdown/Markdown.tsx +116 -35
- package/src/Markdown/incremental.test.ts +175 -7
- package/src/Markdown/index.ts +6 -0
- package/src/Markdown/parser.perf.test.ts +3 -1
- package/src/Markdown/parser.test.ts +122 -0
- package/src/Markdown/parser.ts +609 -81
- package/src/Markdown/utils.ts +6 -0
- package/src/Outline/Outline.spec.md +1 -1
- package/src/Outline/modules/parseOutlineFromMarkdown.spec.md +142 -0
- package/src/PowerSearch/PowerSearchEditPopover.test.tsx +150 -1
- package/src/PowerSearch/PowerSearchEditPopover.tsx +51 -28
- 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/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 +37 -0
- package/src/Spinner/Spinner.tsx +31 -14
- 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/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/Typeahead/BaseTypeahead.spec.md +4 -3
- package/src/Typeahead/BaseTypeahead.tsx +15 -6
- 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/theme/derivedVarRegistry.test.ts +6 -4
- package/src/utils/interactionOverlay.stylex.ts +20 -0
|
@@ -3,41 +3,61 @@ schema_version: 3
|
|
|
3
3
|
template_version: 3
|
|
4
4
|
kind: component
|
|
5
5
|
id: component:Markdown
|
|
6
|
-
authority:
|
|
6
|
+
authority: current
|
|
7
7
|
archive_reason: null
|
|
8
8
|
superseded_by: null
|
|
9
|
-
approved_by:
|
|
10
|
-
approved_at:
|
|
9
|
+
approved_by: cixzhang
|
|
10
|
+
approved_at: 2026-09-15
|
|
11
11
|
owners: [cixzhang]
|
|
12
|
-
review_triggers: [theming]
|
|
12
|
+
review_triggers: [api, theming]
|
|
13
13
|
verified_by:
|
|
14
14
|
[
|
|
15
15
|
packages/core/src/Markdown/Markdown.test.tsx,
|
|
16
|
+
packages/core/src/Markdown/Markdown.public.test.ts,
|
|
17
|
+
packages/core/src/Markdown/parser.test.ts,
|
|
18
|
+
packages/core/src/Markdown/incremental.test.ts,
|
|
19
|
+
packages/core/src/Outline/parseOutlineFromMarkdown.test.ts,
|
|
16
20
|
packages/core/src/theme/themingTargets.test.ts,
|
|
17
21
|
scripts/check-knowledge.mjs,
|
|
18
22
|
]
|
|
19
23
|
modules: []
|
|
20
24
|
families: [family:navigation-destinations]
|
|
21
25
|
design_specs: []
|
|
22
|
-
architecture:
|
|
26
|
+
architecture:
|
|
27
|
+
[architecture:component-theming-surface, architecture:public-component-api]
|
|
23
28
|
contributing: []
|
|
24
|
-
system_specs:
|
|
29
|
+
system_specs:
|
|
30
|
+
[
|
|
31
|
+
spec:AST-002/DEC-1,
|
|
32
|
+
spec:AST-002/DEC-5,
|
|
33
|
+
spec:AST-005/DEC-1,
|
|
34
|
+
spec:AST-005/DEC-2,
|
|
35
|
+
spec:AST-036/DEC-1,
|
|
36
|
+
spec:AST-036/DEC-2,
|
|
37
|
+
spec:AST-036/DEC-3,
|
|
38
|
+
spec:AST-036/DEC-4,
|
|
39
|
+
]
|
|
25
40
|
---
|
|
26
41
|
|
|
27
42
|
# Markdown component contract
|
|
28
43
|
|
|
29
44
|
## Intent
|
|
30
45
|
|
|
31
|
-
Markdown renders parsed content in a Document with stable default block parts
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
46
|
+
Markdown renders parsed content in a Document with stable default block parts and
|
|
47
|
+
constrained renderer seams. Callers may opt into the canonical plugin protocol for
|
|
48
|
+
bounded source syntax, immutable document transformation, and typed extension
|
|
49
|
+
rendering. They may separately opt into dollar-delimited math by supplying one typed
|
|
50
|
+
renderer for both inline and display expressions. The parser accepts matching
|
|
51
|
+
explicit options. Existing parsing, rendering, styling, and streaming behavior
|
|
52
|
+
remain unchanged when plugins and math are absent.
|
|
35
53
|
|
|
36
54
|
## Compatibility and migration
|
|
37
55
|
|
|
38
56
|
- Released default preserved: `yes`
|
|
39
|
-
- Compatibility class: additive
|
|
40
|
-
targets,
|
|
57
|
+
- Compatibility class: additive, opt-in public API; existing parser nodes, DOM,
|
|
58
|
+
styling, targets, dollar-delimited text, `components`, and `inlinePlugins` remain
|
|
59
|
+
unchanged unless the caller supplies `plugins`, supplies `components.math`, or
|
|
60
|
+
passes the matching explicit parser option.
|
|
41
61
|
- Controlled/uncontrolled behavior: not applicable
|
|
42
62
|
- Migration decision: none
|
|
43
63
|
|
|
@@ -52,33 +72,92 @@ Consumer migration instructions belong in consumer docs and release notes.
|
|
|
52
72
|
Image block presentation and the eight current block targets documented below.
|
|
53
73
|
- Applying block spacing and reflected density (plus Heading level) to those
|
|
54
74
|
targets on the default render path.
|
|
75
|
+
- Opt-in recognition of `$…$` inline math and `$$…$$` display math, including
|
|
76
|
+
delimiter boundaries, escape behavior, parser nodes, and streaming parity.
|
|
77
|
+
- Passing each recognized expression as inert text to the caller's one math
|
|
78
|
+
renderer with an `inline` or `block` display value.
|
|
79
|
+
- Applying the canonical `plugins` protocol in the fixed syntax → immutable
|
|
80
|
+
transform → render order while preserving built-in lexical shields, Core-owned
|
|
81
|
+
semantics, and local readable fallback.
|
|
82
|
+
- Validating and freezing replacement document roots before later transforms or
|
|
83
|
+
rendering observe them.
|
|
84
|
+
- Sharing plugin-enabled parse configuration, transformed heading projection, and
|
|
85
|
+
collision-safe heading IDs with Markdown-derived Outline utilities.
|
|
55
86
|
|
|
56
87
|
**Does not own / non-goals**
|
|
57
88
|
|
|
58
|
-
- Output supplied by custom
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
block anatomy.
|
|
89
|
+
- Output supplied by custom renderers; each custom component owns its replacement's
|
|
90
|
+
structure, styling, and accessibility semantics.
|
|
91
|
+
- Inline emphasis, link, inline-code, citation, plugin, or math-renderer output as
|
|
92
|
+
additional default block anatomy.
|
|
63
93
|
- Nested anatomy or targets owned by CodeBlock, Blockquote, List, CheckboxList,
|
|
64
94
|
or Table.
|
|
65
|
-
-
|
|
66
|
-
|
|
95
|
+
- Executing or sanitizing a renderer's math or plugin output, raw HTML parsing,
|
|
96
|
+
mutable or unrestricted AST plugins, package discovery, or new
|
|
97
|
+
list/table/inline-style override slots.
|
|
67
98
|
|
|
68
99
|
## Public concepts
|
|
69
100
|
|
|
70
|
-
|
|
71
|
-
|
|
101
|
+
`plugins` is one optional ordered list of opaque entries created by
|
|
102
|
+
`createMarkdownPlugin()`. A plugin declares only the `syntax`, `transform`, and
|
|
103
|
+
`renderers` capabilities it uses. Syntax-bearing entries supply stable parse
|
|
104
|
+
identity. Parsing, transforms, rendering, and Outline use one stable, strictly typed,
|
|
105
|
+
MDAST-aligned canonical tree; `MarkdownAstNodeMap` and `visitMarkdownNodes` provide
|
|
106
|
+
node-kind narrowing. Released parser functions preserve their existing result shape
|
|
107
|
+
through a compatibility projection. Transforms return validated replacement roots
|
|
108
|
+
without entering parse identity. Every extension node introduced
|
|
109
|
+
by syntax or transformation has complete renderer ownership and a deterministic
|
|
110
|
+
text projection. Text matching, semantic fences, and source decoration are helpers
|
|
111
|
+
that compile to transforms rather than separate protocol phases. `spec:AST-036`
|
|
112
|
+
owns the shared protocol and limited Remark compatibility profile; this component
|
|
113
|
+
owns aggregate application and fallback.
|
|
114
|
+
|
|
115
|
+
### Acceptance and implementation state
|
|
116
|
+
|
|
117
|
+
The plugin clauses below are the accepted target contract for the AST-036 rollout,
|
|
118
|
+
not a claim that the APIs already ship. Until every clause's implementation and
|
|
119
|
+
verification land, the currently released no-plugin, parser, `components`, and
|
|
120
|
+
`inlinePlugins` behavior remains the only available contract. Each implementation PR
|
|
121
|
+
must identify the clauses it completes without weakening the zero-breaking baseline.
|
|
122
|
+
|
|
123
|
+
`MarkdownComponents.math` is one optional renderer with the signature
|
|
124
|
+
`({value: string, display: 'inline' | 'block'}) => ReactNode`. Supplying it opts
|
|
125
|
+
the component into math parsing because the caller owns both whether dollar
|
|
126
|
+
syntax means math and how formulas are rendered. Direct parser callers make the
|
|
127
|
+
same choice with `MathParseOptions` (`{math: true}`); incremental callers pair
|
|
128
|
+
that option with `createIncrementalState<true>()`, which returns the exported
|
|
129
|
+
`IncrementalParseState<true>`. Default calls and values
|
|
130
|
+
annotated as `ParseOptions` keep the released `InlineNode` and `BlockNode`
|
|
131
|
+
unions. Enabled calls return the explicit `InlineNodeWithMath` and
|
|
132
|
+
`BlockNodeWithMath` supersets, whose added leaves are `MathInlineNode` and
|
|
133
|
+
`MathBlockNode`. Exact syntax and examples remain in `Markdown.doc.mjs`.
|
|
72
134
|
|
|
73
135
|
## Behavioral and layout contract
|
|
74
136
|
|
|
75
|
-
| ID
|
|
76
|
-
|
|
|
77
|
-
| FR1
|
|
78
|
-
| FR2
|
|
79
|
-
| FR3
|
|
80
|
-
| FR4
|
|
81
|
-
| FR5
|
|
137
|
+
| ID | Invariant |
|
|
138
|
+
| ---- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
139
|
+
| FR1 | Block and inline displays render one Document root carrying the current `markdown` target. Inline display renders no block anatomy. |
|
|
140
|
+
| FR2 | On the default block render path, Heading, Paragraph, List, Code block, Blockquote, Table, Divider, and Image carry the eight current local block targets documented below. |
|
|
141
|
+
| FR3 | A supplied `heading`, `paragraph`, `code`, `blockquote`, `hr`, or safe-URL `image` renderer replaces the corresponding default part, so Markdown does not impose that part's local target on the replacement. |
|
|
142
|
+
| FR4 | The released Code block target remains `markdown-codeblock`; this compatibility anomaly is not renamed or aliased. |
|
|
143
|
+
| FR5 | Density and Heading level remain reflected capabilities on their owning targets. Display mode, streaming state, and renderer selection do not become separate anatomy entries. |
|
|
144
|
+
| FR6 | Without `components.math`, Markdown does not recognize math syntax. Default, legacy-set, `math: false`, and `ParseOptions`-annotated parser calls retain the released `InlineNode` / `BlockNode` result unions; only `MathParseOptions` returns the explicit math-enabled unions. |
|
|
145
|
+
| FR7 | With math enabled, `$…$` produces an inline `math` node and `$$…$$` produces a block `math` node. The renderer receives the delimiter-free source as `value` and its placement as `display`. |
|
|
146
|
+
| FR8 | Inline math stays on one line, cannot have whitespace touching either delimiter, and cannot open immediately after a digit or close immediately before one. `$$` is reserved for display math. These boundaries keep paired currency amounts literal. |
|
|
147
|
+
| FR9 | A backslash-escaped dollar is literal outside math and does not close math inside it. An unmatched inline or display delimiter remains literal in non-streaming output. |
|
|
148
|
+
| FR10 | Code spans and fenced code blocks are opaque to math parsing. Link destinations are opaque; link labels may contain inline math. Inline plugins run only on prose text and never inside math. |
|
|
149
|
+
| FR11 | Streaming converges to the same nodes as a full parse at every chunk boundary, including display math nested in ordinary lists, task lists, blockquotes, and their supported combinations, with LF or CRLF and with or without source ranges. Incomplete math is withheld only while its exact owning container remains open; a list/quote exit or quote-depth change restores literal parsing. Math-enabled incremental calls require `IncrementalParseState<true>`, so the cache and returned union share one contract. |
|
|
150
|
+
| FR12 | Omitting `plugins` and passing an empty list are one semantic empty pipeline with identical parser unions, AST, DOM, styling, targets, IDs, and streaming behavior. Core may skip empty preparation and allocation without creating a separate behavior model. `components`, `inlinePlugins`, citations, autolinking, sources, and math retain their released meaning. |
|
|
151
|
+
| FR13 | Plugin-enabled parsing follows built-in lexical shields and ordered syntax claims, then uses one stable, strictly typed, MDAST-aligned canonical tree for transforms, rendering, and Outline. `MarkdownAstNodeMap` and `visitMarkdownNodes` narrow callbacks by node kind. Released parser functions preserve their existing result shape through a compatibility projection. Every returned root is finite, acyclic, representable, validated, and frozen before later plugins or rendering observe it. |
|
|
152
|
+
| FR14 | Plugin failures preserve the last valid document and readable authored source. Core retains heading, navigation, image, list, table, and document semantics and exposes no raw-markup parser channel, registry, package discovery, mutable shared AST, or unrestricted DOM hook. URL-like plugin data remains untrusted; Astryx-owned sinks follow `family:navigation-destinations`. |
|
|
153
|
+
| FR15 | Incremental parse identity contains every parse-affecting Markdown option and only ordered syntax-bearing plugin name, protocol version, and `parseKey`. Transform or renderer changes reuse settled parse output, rerun transformation, and do not remount unaffected extension output. |
|
|
154
|
+
| FR16 | Plugin-enabled Markdown and Markdown-derived Outline use the same parse options, ordered transforms, extension text projection, slugger, and collision allocator so every visible heading, Outline label, heading ID, and target agree. A limited Remark adapter may run only synchronous transform plugins whose input and output round-trip through the documented supported MDAST subset. |
|
|
155
|
+
| FR17 | An extension node declares `content` as `'none'`, `'phrasing'`, `'flow'`, or an explicit `{allow, min?, max?}` allowlist that narrows the category its `display` implies. Markdown parses every container's inner source span under the same grammar and shields, validates children at each transform boundary, renders children through the same built-in renderers and `components` seams, counts nesting toward the built-in depth bound, leaves FR16 heading traversal unchanged, and renders children in place when a container renderer fails. |
|
|
156
|
+
| FR18 | A transform may read and remove another plugin's extension nodes, including a subtree containing them, and may insert or remove headings; it may not create, edit, internally reorder, or duplicate another plugin's nodes, change a source heading's depth, or forge or duplicate heading identity. `dependsOn` is validated at preparation; an unmet or misordered dependency skips only that plugin's transform. Every rejection names the rule and owning plugin. |
|
|
157
|
+
| FR19 | `onPluginDiagnostic` is available on the component and parser options and receives one source-free event — plugin, phase, stable code, severity — per failure, advisory, or silent degradation, in development and production, rate-limited with a suppression code. Admission, duplicate-name, and protocol-version failures behave identically through the component and every parser entrypoint: the call succeeds with the last valid configuration and never throws into the caller. |
|
|
158
|
+
| FR20 | `parseMarkdownAst()` and `parseInlineAst()` return the canonical tree and accept the same options and plugin list as the component; `@astryxdesign/core/Markdown/parser` exposes parsing, canonical AST types, and plugin admission with no client boundary. `createMarkdownPlugin()` infers the extension-node union, so no callsite needs explicit type arguments, and a declaration that yields no usable extension type is a type error rather than a silent `never`. |
|
|
159
|
+
| FR21 | A transform runs again for every streamed update and must be idempotent and convergent; transforms whose effect requires complete input use the final-input signal. Semantically equal plugin lists reuse prepared work whether or not the array reference is stable, and development reports one diagnostic when a recreated list prevents reuse. |
|
|
160
|
+
| FR22 | An extension renderer may opt into the Markdown-owned extension theme target so themes reach plugin output. Opting out leaves output untargeted. The target adds no default styling or anatomy beyond the block spacing and content width Core already applies. |
|
|
82
161
|
|
|
83
162
|
### Allowed variation
|
|
84
163
|
|
|
@@ -91,51 +170,92 @@ and usage remain documented in `Markdown.doc.mjs`.
|
|
|
91
170
|
- **AV4 — Nested primitives.** Astryx primitives used inside default blocks may
|
|
92
171
|
change internal element shape while preserving their own public contracts and
|
|
93
172
|
Markdown's outer block targets.
|
|
173
|
+
- **AV5 — Math renderer.** The caller may use any renderer that accepts the raw
|
|
174
|
+
expression and display value. Its DOM, styles, typesetting engine, error UI,
|
|
175
|
+
and accessibility representation are outside Markdown's ownership.
|
|
176
|
+
- **AV6 — Installed plugins.** A host may supply any ordered set of compatible
|
|
177
|
+
opaque plugin entries. Syntax, immutable transform behavior, renderer-owned
|
|
178
|
+
output, and helper implementation may vary while validation, readable fallback,
|
|
179
|
+
Core semantics, and heading identity stay fixed.
|
|
94
180
|
|
|
95
181
|
### Representative states
|
|
96
182
|
|
|
97
|
-
| State | Required invariant
|
|
98
|
-
| ---------------------- |
|
|
99
|
-
| Default block content | Every parsed block uses its corresponding current Markdown target.
|
|
100
|
-
| Custom block renderers | The replaced Heading, Paragraph, Code block, Blockquote, Divider, or Image lacks the corresponding Markdown target.
|
|
101
|
-
| Ordered/unordered list | List carries `markdown-list`.
|
|
102
|
-
| Task list | The outer List part carries `markdown-list`.
|
|
103
|
-
| Safe block image | Default Image carries `markdown-image`, or a custom image renderer replaces it.
|
|
104
|
-
| Unsafe block image URL | Markdown renders its fallback Image part with `markdown-image`; no custom image renderer receives the rejected URL.
|
|
105
|
-
| Inline display | Document carries `markdown`; no block target renders.
|
|
183
|
+
| State | Required invariant | Allowed variation |
|
|
184
|
+
| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------- |
|
|
185
|
+
| Default block content | Every parsed block uses its corresponding current Markdown target. | Block count, order, density, content width, and alignment. |
|
|
186
|
+
| Custom block renderers | The replaced Heading, Paragraph, Code block, Blockquote, Divider, or Image lacks the corresponding Markdown target. | Replacement structure and styling. |
|
|
187
|
+
| Ordered/unordered list | List carries `markdown-list`. | Marker kind, start value, item count, and nested content. |
|
|
188
|
+
| Task list | The outer List part carries `markdown-list`. | Checked values and item content. |
|
|
189
|
+
| Safe block image | Default Image carries `markdown-image`, or a custom image renderer replaces it. | Source and alternative text. |
|
|
190
|
+
| Unsafe block image URL | Markdown renders its fallback Image part with `markdown-image`; no custom image renderer receives the rejected URL. | Alternative text shown by the fallback. |
|
|
191
|
+
| Inline display | Document carries `markdown`; no block target renders. | Inline text, links, code, citations, plugins, and opt-in inline math. |
|
|
192
|
+
| Math renderer absent | Dollar-delimited source follows the released Markdown grammar and no `math` node or renderer output exists. | Currency, unmatched delimiters, and ordinary prose. |
|
|
193
|
+
| Math renderer present | Complete supported delimiters are opaque to Markdown formatting and are passed to the renderer as inert text. | Inline or block display and any renderer-owned output. |
|
|
194
|
+
| Streaming math | Incomplete math is withheld; once complete, the streamed nodes equal the full-parse nodes at top level and inside list/blockquote containers. | Delimiters and expression text may arrive in separate chunks; source ranges remain optional. |
|
|
195
|
+
| Plugins omitted | Released parser unions, AST, DOM, targets, heading IDs, and performance remain unchanged. | Omitted or empty list; both are one empty transform pipeline. |
|
|
196
|
+
| Plugins enabled | Fixed syntax → immutable transform → render order, validated roots, readable fallback, and matching Markdown/Outline heading identity remain invariant. | Syntax, transforms, renderers, helper execution plans, plugin order, and live post-parse state. |
|
|
106
197
|
|
|
107
198
|
### Transformation and precedence order
|
|
108
199
|
|
|
109
|
-
-
|
|
110
|
-
|
|
200
|
+
- Fenced and inline code claim their contents before math.
|
|
201
|
+
- Complete display math claims a block before headings, tables, lists, and
|
|
202
|
+
paragraphs. Complete inline math claims its source before citations, links,
|
|
203
|
+
emphasis, autolinks, and inline plugins.
|
|
204
|
+
- A custom renderer receives only the delimiter-free expression string and its
|
|
205
|
+
display value. Markdown never turns it into HTML or executes it.
|
|
206
|
+
- Existing URL sanitization remains in force for links and images; math adds no
|
|
207
|
+
navigation or raw-HTML sink.
|
|
208
|
+
- Built-in syntax and protected contexts claim first; extension syntax claims only
|
|
209
|
+
eligible source; ordered transforms then receive deeply readonly document roots;
|
|
210
|
+
Core validates each returned root before rendering.
|
|
211
|
+
- Text matching, semantic fences, and source decorations use transform helpers. Core
|
|
212
|
+
may compile those helpers into indexed internal plans without exposing additional
|
|
213
|
+
public phases.
|
|
111
214
|
|
|
112
215
|
### Performance and resources
|
|
113
216
|
|
|
114
|
-
-
|
|
217
|
+
- Math scanning is disabled unless requested.
|
|
218
|
+
- Inline matching is a bounded forward scan of one line. Display matching scans
|
|
219
|
+
only from a candidate `$$` opener to its closer.
|
|
220
|
+
- The incremental parser keeps completed blocks cached, treats an open display
|
|
221
|
+
expression like an open code fence, and tracks exact list and blockquote
|
|
222
|
+
container depth so an indented closer cannot become a new opener and a depth
|
|
223
|
+
transition cannot swallow literal content. The factory-created state carries
|
|
224
|
+
the same legacy or math-enabled node contract as the parser call.
|
|
225
|
+
- Stable and semantically equal plugin lists reuse prepared syntax and transform plans. Only syntax enters parse identity; transform and renderer changes reuse parsed output. Zero-work and representative transforms remain within `spec:AST-036` FR21–FR23 budgets, including the plugin-authored and streaming paths.
|
|
226
|
+
- Remark compatibility adapters, the conformance kit, and optional renderers stay outside Core bundles unless explicitly imported.
|
|
115
227
|
|
|
116
228
|
## Accessibility contract
|
|
117
229
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
230
|
+
The default document semantics, heading IDs, paragraph role, list semantics,
|
|
231
|
+
scrollable Table wrapper, and image alternative text remain unchanged. Math has
|
|
232
|
+
no Astryx-owned default output: the caller's renderer owns an accessible
|
|
233
|
+
representation appropriate to its typesetting engine (for example MathML or a
|
|
234
|
+
labelled `role="math"` element). Markdown adds no wrapper, ARIA attributes, or
|
|
235
|
+
HTML injection around renderer output. Plugin renderers likewise own their
|
|
236
|
+
complete documented semantic pattern, while Core preserves its own document,
|
|
237
|
+
heading, navigation, image, list, and table semantics. Transforms cannot erase
|
|
238
|
+
required accessible meaning or make meaning color-only.
|
|
121
239
|
|
|
122
240
|
## Design relationships
|
|
123
241
|
|
|
124
|
-
| Anatomy or state | Design requirement
|
|
125
|
-
| ---------------- |
|
|
126
|
-
| Document | Contains block or inline rendered Markdown content.
|
|
127
|
-
| Heading | Presents one parsed heading with its resolved level and optional generated ID.
|
|
128
|
-
| Paragraph | Presents one prose block using the default composition-safe paragraph structure.
|
|
129
|
-
| List | Presents ordered, unordered, or task-list items as one block.
|
|
130
|
-
| Code block | Presents fenced code and owns the outer spacing target on the default path.
|
|
131
|
-
| Blockquote | Presents quoted block content on the default path.
|
|
132
|
-
| Table | Presents parsed rows and columns in a keyboard-scrollable block wrapper.
|
|
133
|
-
| Divider | Presents a horizontal separation between blocks.
|
|
134
|
-
| Image | Presents a safe block image or the fallback for a rejected image URL.
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
242
|
+
| Anatomy or state | Design requirement | Representation authority | Hierarchy role | Component contract |
|
|
243
|
+
| ---------------- | ---------------------------------------------------------------------------------- | ------------------------------ | -------------- | ------------------ |
|
|
244
|
+
| Document | Contains block or inline rendered Markdown content. | Current source and public docs | Supporting | FR1, FR5 |
|
|
245
|
+
| Heading | Presents one parsed heading with its resolved level and optional generated ID. | Current source and public docs | Prominent | FR2, FR3, FR5 |
|
|
246
|
+
| Paragraph | Presents one prose block using the default composition-safe paragraph structure. | Current source and public docs | Prominent | FR2, FR3 |
|
|
247
|
+
| List | Presents ordered, unordered, or task-list items as one block. | Current source and public docs | Prominent | FR2, FR5 |
|
|
248
|
+
| Code block | Presents fenced code and owns the outer spacing target on the default path. | Current source and public docs | Prominent | FR2, FR3, FR4 |
|
|
249
|
+
| Blockquote | Presents quoted block content on the default path. | Current source and public docs | Prominent | FR2, FR3 |
|
|
250
|
+
| Table | Presents parsed rows and columns in a keyboard-scrollable block wrapper. | Current source and public docs | Prominent | FR2 |
|
|
251
|
+
| Divider | Presents a horizontal separation between blocks. | Current source and public docs | Supporting | FR2, FR3 |
|
|
252
|
+
| Image | Presents a safe block image or the fallback for a rejected image URL. | Current source and public docs | Prominent | FR2, FR3 |
|
|
253
|
+
| Math | Delegates an explicitly enabled expression to the caller's renderer as inert text. | Component contract | Supporting | FR6–FR11 |
|
|
254
|
+
|
|
255
|
+
Custom renderers replace the existing default parts rather than becoming nested
|
|
256
|
+
Markdown anatomy. The opt-in math renderer is also not default anatomy and gets no
|
|
257
|
+
Markdown theme target or wrapper. Lists and Tables have no corresponding custom
|
|
258
|
+
block renderer. The Document remains Markdown-owned in every display mode.
|
|
139
259
|
|
|
140
260
|
### Theming anatomy
|
|
141
261
|
|
|
@@ -167,33 +287,97 @@ and this change preserves the existing spelling exactly.
|
|
|
167
287
|
- `architecture:component-theming-surface` owns anatomy qualification, target
|
|
168
288
|
mapping, target-capability state, composition boundaries, and compatibility for
|
|
169
289
|
frozen targets.
|
|
290
|
+
- `architecture:public-component-api` and `spec:AST-002/DEC-1` own API
|
|
291
|
+
admission. The caller knows whether dollar syntax is math and must choose the
|
|
292
|
+
renderer; Markdown cannot derive either from the source without changing the
|
|
293
|
+
meaning of existing documents.
|
|
294
|
+
- `spec:AST-002/DEC-5` requires this accepted component-local contract to be
|
|
295
|
+
current with the implementation.
|
|
170
296
|
- `family:navigation-destinations` owns the shared accept/block result for parsed
|
|
171
297
|
links and every Astryx-owned navigation sink. `spec:AST-005/DEC-1` requires
|
|
172
298
|
Markdown navigation to remain conformant with Core link plumbing.
|
|
173
299
|
- `spec:AST-005/DEC-2` keeps embedded-resource policy separate. Markdown may
|
|
174
300
|
reject a broader set of image/resource URLs without narrowing the shared
|
|
175
301
|
navigation contract.
|
|
302
|
+
- `spec:AST-036` owns the opaque syntax/transform/renderer protocol, immutable AST
|
|
303
|
+
validation, limited Remark compatibility, performance, and resource boundaries.
|
|
304
|
+
This record owns aggregate Markdown behavior in FR12–FR22;
|
|
305
|
+
`module:Outline/parseOutlineFromMarkdown` owns the corresponding Outline
|
|
306
|
+
projection.
|
|
176
307
|
- Nested Astryx primitives retain ownership of their own anatomy and targets;
|
|
177
308
|
Markdown owns the outer block targets listed here.
|
|
178
309
|
|
|
179
310
|
## Verification map
|
|
180
311
|
|
|
181
|
-
| Contract
|
|
182
|
-
|
|
|
183
|
-
| FR1
|
|
184
|
-
|
|
|
185
|
-
|
|
|
186
|
-
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
312
|
+
| Contract | Verification | Representative states | Failure signal |
|
|
313
|
+
| ---------------------- | -------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
314
|
+
| FR1–FR5 | `Markdown.test.tsx`, theme-target tests, and `scripts/check-knowledge.mjs` | Default block/inline output and all current targets | Existing DOM, target, spacing, or renderer behavior changes. |
|
|
315
|
+
| FR6–FR10 | `parser.test.ts` and `Markdown.test.tsx` | Opt-out, inline/display math, escapes, currency, code, links, plugins | A delimiter is claimed without opt-in, TeX is formatted as Markdown, or opaque contexts leak. |
|
|
316
|
+
| FR11 | `incremental.test.ts` and `Markdown.test.tsx` | Every-character top-level/list/task-list/blockquote splits, CRLF, source ranges | Streaming diverges from a full parse, shows partial syntax, mistakes a nested closer for an opener, or crosses a closed container. |
|
|
317
|
+
| FR12–FR16 | plugin, transform, adapter, performance, and Outline parser tests | omitted/empty lists, immutable transforms, invalid outputs, live updates, one compatible Remark plugin, duplicate headings | Empty behavior forks, input mutates, invalid structure escapes, transforms reparse, adapter loses content, budgets fail, or heading targets diverge. |
|
|
318
|
+
| FR17–FR18 | container parse/validation, ownership, and dependency tests | leaf/container declarations, nested containers, foreign read/remove/mint/edit, unmet/misordered dependencies | Plugin-built parsed children, invalid content, lost fallback children, foreign mint/edit, or generic ownership codes. |
|
|
319
|
+
| FR19 | diagnostic-channel tests in development and production | every phase, advisory reports, rate suppression, no handler, malformed list, duplicate Core, version skew | A silent production failure, document content in a diagnostic, or one entrypoint throwing where another recovers. |
|
|
320
|
+
| FR20–FR22 | canonical/server imports, inference, streaming, preparation, theming tests | server imports, no explicit type args, chunk boundaries, recreated lists, themed/unthemed extensions | Client references, explicit-type workarounds, oscillation, per-render re-preparation, or unreachable opted-in output. |
|
|
321
|
+
| Public syntax/types | `Markdown.public.test.ts`, core typecheck, and `Markdown.doc.mjs` | Legacy exhaustive switches, math opt-ins, inferred extension-node unions | A released union widens, an enabled union loses nodes, or docs drift from declarations. |
|
|
322
|
+
| Security/accessibility | `parser.test.ts`, `Markdown.test.tsx`, and renderer guidance | Inert expression strings and renderer-owned semantics | Astryx executes math as HTML or silently claims renderer-owned accessibility. |
|
|
323
|
+
|
|
324
|
+
Focused tests continue to pin all nine current target names and default block
|
|
325
|
+
placement. Math intentionally adds no target and no default anatomy.
|
|
192
326
|
|
|
193
327
|
## Decision log
|
|
194
328
|
|
|
195
|
-
|
|
196
|
-
|
|
329
|
+
### DEC-1 — Math is an opt-in renderer contract
|
|
330
|
+
|
|
331
|
+
**Reference:** `component:Markdown/DEC-1`
|
|
332
|
+
**Decider:** `cixzhang`, `2026-09-13`
|
|
333
|
+
|
|
334
|
+
A caller that supplies `components.math` opts the component into the constrained
|
|
335
|
+
dollar-math grammar and receives every complete expression through one renderer
|
|
336
|
+
with its source value and inline/block placement. Direct parser callers use
|
|
337
|
+
`MathParseOptions`; incremental callers also create
|
|
338
|
+
`IncrementalParseState<true>` via `createIncrementalState<true>()` so the cache
|
|
339
|
+
and result expose the same math-enabled node union.
|
|
340
|
+
|
|
341
|
+
This passes API admission because otherwise identical dollar-delimited source may
|
|
342
|
+
be prose or math, only the document host knows which meaning applies, and Astryx
|
|
343
|
+
cannot choose a typesetting or accessibility implementation for the host. Tying
|
|
344
|
+
the opt-in to the required renderer prevents an enabled-but-unrenderable state.
|
|
345
|
+
The default remains exactly the released Markdown grammar.
|
|
346
|
+
|
|
347
|
+
Rejected: a generic AST/plugin escape hatch, raw HTML rendering, new list/table
|
|
348
|
+
slots without consumer evidence, or a separate boolean on the component that
|
|
349
|
+
could enable math without a renderer.
|
|
350
|
+
|
|
351
|
+
### DEC-2 — Immutable transformation is the canonical Markdown extension seam
|
|
352
|
+
|
|
353
|
+
**Reference:** `component:Markdown/DEC-2`
|
|
354
|
+
**Decider:** `cixzhang`, `2026-09-15`
|
|
355
|
+
|
|
356
|
+
Markdown accepts one ordered `plugins` list whose opaque entries are created by
|
|
357
|
+
`createMarkdownPlugin()`. The public protocol exposes only bounded `syntax`,
|
|
358
|
+
immutable `transform`, and typed `renderers`. Core owns deep-readonly input,
|
|
359
|
+
validation and freezing of replacement roots, readable fallback, syntax-only parse
|
|
360
|
+
identity, preparation reuse, shared heading identity, containers, diagnostics, canonical/server parsing, and theming in FR12–FR22. Existing
|
|
361
|
+
`components`, `inlinePlugins`, math, citations, autolinking, and parser calls remain
|
|
362
|
+
compatible.
|
|
363
|
+
|
|
364
|
+
Text matching, semantic fences, and source decoration are transform helpers rather
|
|
365
|
+
than separate protocol phases. A tree-shakeable adapter may run only synchronous
|
|
366
|
+
transform-only Remark plugins over the documented MDAST subset; unsupported behavior
|
|
367
|
+
fails closed rather than being approximated.
|
|
368
|
+
|
|
369
|
+
This projects `spec:AST-036/DEC-1` through `DEC-4` into the component owner. It rejects a registry, package discovery, mutable shared AST, raw markup, a second plugin prop, or an unrestricted Unified runtime.
|
|
370
|
+
|
|
371
|
+
### DEC-3 — Containers, diagnostics, and canonical APIs are Markdown-owned
|
|
372
|
+
|
|
373
|
+
**Reference:** `component:Markdown/DEC-3`
|
|
374
|
+
**Decider:** `cixzhang`, `2026-09-16`
|
|
375
|
+
|
|
376
|
+
Markdown parses every extension container's inner span itself and validates children against the plugin's declared content shape, so a callout holds real Markdown while heading identity, protected contexts, navigation policy, and Outline scope stay Core-owned. Containers change what a document can express, not which headings have identity: the released top-level traversal shared by heading IDs and Outline is untouched. A failed container renderer shows its children rather than literal source. Ownership rejections name the rule and owner; removal of another plugin's nodes is permitted and only minting, editing, internal reordering, duplication, and identity forgery are not.
|
|
377
|
+
|
|
378
|
+
Markdown also owns the protocol's observability and entry surface: `onPluginDiagnostic` makes every failure visible in production without carrying document content, admission failures degrade instead of throwing at any entrypoint, canonical parse entrypoints and a server-safe parser entry exist beside the released projection, extension types are inferred, and extension output may opt into one theme target without becoming default anatomy.
|
|
379
|
+
|
|
380
|
+
This projects `spec:AST-036/DEC-5` through `DEC-11` into the component owner in FR17–FR22. It rejects leaf-only extensions, plugin-authored parsed children, independent document shells, development-only or free-text diagnostics, parsers reachable only through a client barrel, required hand-written extension aliases, and default anatomy for plugin output.
|
|
197
381
|
|
|
198
382
|
## Open questions
|
|
199
383
|
|
|
@@ -1,10 +1,19 @@
|
|
|
1
1
|
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
describe,
|
|
5
|
+
it,
|
|
6
|
+
expect,
|
|
7
|
+
expectTypeOf,
|
|
8
|
+
vi,
|
|
9
|
+
beforeEach,
|
|
10
|
+
afterEach,
|
|
11
|
+
} from 'vitest';
|
|
4
12
|
import {render, screen, fireEvent} from '@testing-library/react';
|
|
5
|
-
import type {ReactNode} from 'react';
|
|
13
|
+
import type {ComponentProps, ReactNode} from 'react';
|
|
6
14
|
import {Markdown} from './Markdown';
|
|
7
|
-
import type {MarkdownInlinePlugin} from './Markdown';
|
|
15
|
+
import type {MarkdownComponents, MarkdownInlinePlugin} from './Markdown';
|
|
16
|
+
import type {ParseOptions} from './index';
|
|
8
17
|
import {stubMatchMedia} from '../__tests__/stubMatchMedia';
|
|
9
18
|
import {parseOutlineFromMarkdown} from '../Outline/parseOutlineFromMarkdown';
|
|
10
19
|
|
|
@@ -615,6 +624,53 @@ describe('Markdown', () => {
|
|
|
615
624
|
expect(links[0].getAttribute('href')).toBe('https://example.com');
|
|
616
625
|
expect(links[1].getAttribute('href')).toBe('/page');
|
|
617
626
|
});
|
|
627
|
+
|
|
628
|
+
it('preserves dollar-delimited text when no math renderer is supplied', () => {
|
|
629
|
+
const {container} = render(
|
|
630
|
+
<Markdown>{'Total $5 and formula $x_1 + *y*$.'}</Markdown>,
|
|
631
|
+
);
|
|
632
|
+
expect(container.textContent).toBe('Total $5 and formula $x_1 + y$.');
|
|
633
|
+
expect(container.querySelector('em')).toHaveTextContent('y');
|
|
634
|
+
expect(container.querySelector('[role="math"]')).toBeNull();
|
|
635
|
+
});
|
|
636
|
+
|
|
637
|
+
it('passes inline and display expressions to the custom math renderer', () => {
|
|
638
|
+
type MathRendererProps = ComponentProps<
|
|
639
|
+
NonNullable<MarkdownComponents['math']>
|
|
640
|
+
>;
|
|
641
|
+
function MathRenderer({value, display}: MathRendererProps) {
|
|
642
|
+
const Tag = display === 'block' ? 'div' : 'span';
|
|
643
|
+
return (
|
|
644
|
+
<Tag
|
|
645
|
+
role="math"
|
|
646
|
+
aria-label={`Formula: ${value}`}
|
|
647
|
+
data-testid={`${display}-math`}>
|
|
648
|
+
{value}
|
|
649
|
+
</Tag>
|
|
650
|
+
);
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
render(
|
|
654
|
+
<Markdown components={{math: MathRenderer}}>
|
|
655
|
+
{'Inline $x_1 + *y*$ here.\n\n$$\n\\sum_i x_i\n$$'}
|
|
656
|
+
</Markdown>,
|
|
657
|
+
);
|
|
658
|
+
|
|
659
|
+
expect(screen.getByTestId('inline-math')).toHaveTextContent('x_1 + *y*');
|
|
660
|
+
expect(screen.getByTestId('block-math')).toHaveTextContent('\\sum_i x_i');
|
|
661
|
+
expect(screen.getAllByRole('math')).toHaveLength(2);
|
|
662
|
+
});
|
|
663
|
+
|
|
664
|
+
it('exports the math renderer and parser option types', () => {
|
|
665
|
+
type MathRendererProps = ComponentProps<
|
|
666
|
+
NonNullable<MarkdownComponents['math']>
|
|
667
|
+
>;
|
|
668
|
+
expectTypeOf<MathRendererProps>().toEqualTypeOf<{
|
|
669
|
+
value: string;
|
|
670
|
+
display: 'inline' | 'block';
|
|
671
|
+
}>();
|
|
672
|
+
expectTypeOf<ParseOptions>().toMatchTypeOf<{math?: boolean}>();
|
|
673
|
+
});
|
|
618
674
|
});
|
|
619
675
|
|
|
620
676
|
// ---------------------------------------------------------------------------
|
|
@@ -667,6 +723,54 @@ describe('inlinePlugins', () => {
|
|
|
667
723
|
expect(link!.textContent).toBe('PROJ-123');
|
|
668
724
|
});
|
|
669
725
|
|
|
726
|
+
it('autolinks generic prefixed-number entities without rewriting source', () => {
|
|
727
|
+
const entityPlugin: MarkdownInlinePlugin = {
|
|
728
|
+
pattern: /\b([A-Z][A-Z0-9]+-\d+)\b/g,
|
|
729
|
+
render: (match, key) => (
|
|
730
|
+
<a key={key} href={`/entities/${match[1]}`} data-testid="entity-link">
|
|
731
|
+
{match[0]}
|
|
732
|
+
</a>
|
|
733
|
+
),
|
|
734
|
+
};
|
|
735
|
+
const {container} = render(
|
|
736
|
+
<Markdown inlinePlugins={[entityPlugin]}>
|
|
737
|
+
{'See DOC-2048, but keep `DOC-9999` literal.'}
|
|
738
|
+
</Markdown>,
|
|
739
|
+
);
|
|
740
|
+
const link = screen.getByTestId('entity-link');
|
|
741
|
+
expect(link).toHaveAttribute('href', '/entities/DOC-2048');
|
|
742
|
+
expect(link).toHaveTextContent('DOC-2048');
|
|
743
|
+
expect(container.querySelector('code')).toHaveTextContent('DOC-9999');
|
|
744
|
+
expect(
|
|
745
|
+
container.querySelectorAll('[data-testid="entity-link"]'),
|
|
746
|
+
).toHaveLength(1);
|
|
747
|
+
});
|
|
748
|
+
|
|
749
|
+
it('keeps math opaque to entity plugins while transforming surrounding prose', () => {
|
|
750
|
+
const entityPlugin: MarkdownInlinePlugin = {
|
|
751
|
+
pattern: /\b(DOC-\d+)\b/g,
|
|
752
|
+
render: (match, key) => (
|
|
753
|
+
<a key={key} href={`/entities/${match[1]}`} data-testid="entity-link">
|
|
754
|
+
{match[0]}
|
|
755
|
+
</a>
|
|
756
|
+
),
|
|
757
|
+
};
|
|
758
|
+
const MathRenderer: NonNullable<MarkdownComponents['math']> = ({value}) => (
|
|
759
|
+
<span role="math">{value}</span>
|
|
760
|
+
);
|
|
761
|
+
render(
|
|
762
|
+
<Markdown
|
|
763
|
+
components={{math: MathRenderer}}
|
|
764
|
+
inlinePlugins={[entityPlugin]}>
|
|
765
|
+
{'DOC-1 and $DOC-2 + x$ and `DOC-3`'}
|
|
766
|
+
</Markdown>,
|
|
767
|
+
);
|
|
768
|
+
expect(screen.getAllByTestId('entity-link')).toHaveLength(1);
|
|
769
|
+
expect(screen.getByTestId('entity-link')).toHaveTextContent('DOC-1');
|
|
770
|
+
expect(screen.getByRole('math')).toHaveTextContent('DOC-2 + x');
|
|
771
|
+
expect(screen.getByText('DOC-3').tagName).toBe('CODE');
|
|
772
|
+
});
|
|
773
|
+
|
|
670
774
|
it('supports multiple plugins', () => {
|
|
671
775
|
const {container} = render(
|
|
672
776
|
<Markdown inlinePlugins={[createTicketPlugin(), createXRefPlugin()]}>
|