@astryxdesign/core 0.6.2-canary.bfee8a4 → 0.6.2-canary.d4cc232
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/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/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.map +1 -1
- package/dist/Markdown/Markdown.js +63 -59
- package/dist/Markdown/ast.d.ts +124 -0
- package/dist/Markdown/ast.d.ts.map +1 -0
- package/dist/Markdown/ast.js +38 -0
- package/dist/Markdown/parser.d.ts +14 -8
- package/dist/Markdown/parser.d.ts.map +1 -1
- package/dist/Markdown/parser.js +352 -97
- package/dist/Outline/parseOutlineFromMarkdown.d.ts.map +1 -1
- package/dist/Outline/parseOutlineFromMarkdown.js +7 -7
- 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 +14 -8
- 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 +8 -0
- package/dist/utils/interactionOverlay.stylex.d.ts +8 -0
- package/dist/utils/interactionOverlay.stylex.d.ts.map +1 -1
- package/dist/utils/interactionOverlay.stylex.js +9 -0
- package/package.json +3 -3
- 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/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.spec.md +159 -43
- package/src/Markdown/Markdown.test.tsx +11 -0
- package/src/Markdown/Markdown.tsx +106 -96
- package/src/Markdown/ast.test.ts +169 -0
- package/src/Markdown/ast.ts +231 -0
- package/src/Markdown/incremental.test.ts +12 -0
- package/src/Markdown/parser.ts +427 -118
- package/src/Outline/Outline.spec.md +1 -1
- package/src/Outline/modules/parseOutlineFromMarkdown.spec.md +142 -0
- package/src/Outline/parseOutlineFromMarkdown.ts +8 -13
- 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/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 +11 -0
- package/src/Slider/Slider.test.tsx +128 -0
- package/src/Slider/Slider.tsx +25 -7
- 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/utils/interactionOverlay.stylex.ts +20 -0
|
@@ -48,22 +48,21 @@ import type {CitationSource} from '../Citation/Citation';
|
|
|
48
48
|
import {useLinkComponent} from '../Link/useLinkComponent';
|
|
49
49
|
import type {LinkComponentType} from '../Link/types';
|
|
50
50
|
import {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
51
|
+
parseInlineAst,
|
|
52
|
+
parseMarkdownAst,
|
|
53
|
+
parseMarkdownAstIncremental,
|
|
54
54
|
createIncrementalState,
|
|
55
55
|
trimStreamingArtifacts,
|
|
56
|
-
inlineText,
|
|
57
56
|
slugify,
|
|
58
57
|
uniqueSlug,
|
|
59
58
|
} from './parser';
|
|
59
|
+
import type {IncrementalState, MathParseOptions, ParseOptions} from './parser';
|
|
60
|
+
import {markdownAstText} from './ast';
|
|
60
61
|
import type {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
ParseOptions,
|
|
66
|
-
} from './parser';
|
|
62
|
+
MarkdownAstBlockContent,
|
|
63
|
+
MarkdownAstPhrasingContent,
|
|
64
|
+
MarkdownAstTable,
|
|
65
|
+
} from './ast';
|
|
67
66
|
import {themeProps} from '../utils/themeProps';
|
|
68
67
|
import {useTranslator, type TranslatorFn} from '../i18n';
|
|
69
68
|
|
|
@@ -487,25 +486,27 @@ interface StreamingCursor {
|
|
|
487
486
|
* Count the total text characters in inline nodes without rendering.
|
|
488
487
|
* Used to advance the cursor past a block that will be faded as a whole unit.
|
|
489
488
|
*/
|
|
490
|
-
function countInlineTextLength(
|
|
489
|
+
function countInlineTextLength(
|
|
490
|
+
nodes: ReadonlyArray<MarkdownAstPhrasingContent>,
|
|
491
|
+
): number {
|
|
491
492
|
let len = 0;
|
|
492
493
|
for (const node of nodes) {
|
|
493
494
|
switch (node.type) {
|
|
494
495
|
case 'text':
|
|
495
|
-
len += node.
|
|
496
|
+
len += node.value.length;
|
|
496
497
|
break;
|
|
497
|
-
case '
|
|
498
|
-
len += node.
|
|
498
|
+
case 'inlineCode':
|
|
499
|
+
len += node.value.length;
|
|
499
500
|
break;
|
|
500
|
-
case '
|
|
501
|
+
case 'inlineMath':
|
|
501
502
|
len += node.value.length;
|
|
502
503
|
break;
|
|
503
504
|
case 'image':
|
|
504
505
|
len += node.alt.length;
|
|
505
506
|
break;
|
|
506
|
-
case '
|
|
507
|
-
case '
|
|
508
|
-
case '
|
|
507
|
+
case 'strong':
|
|
508
|
+
case 'emphasis':
|
|
509
|
+
case 'delete':
|
|
509
510
|
case 'link':
|
|
510
511
|
len += countInlineTextLength(node.children);
|
|
511
512
|
break;
|
|
@@ -523,7 +524,9 @@ function countInlineTextLength(nodes: InlineNodeWithMath[]): number {
|
|
|
523
524
|
/**
|
|
524
525
|
* Count total text characters in a block node tree.
|
|
525
526
|
*/
|
|
526
|
-
function countBlockTextLength(
|
|
527
|
+
function countBlockTextLength(
|
|
528
|
+
nodes: ReadonlyArray<MarkdownAstBlockContent>,
|
|
529
|
+
): number {
|
|
527
530
|
let len = 0;
|
|
528
531
|
for (const node of nodes) {
|
|
529
532
|
switch (node.type) {
|
|
@@ -531,8 +534,8 @@ function countBlockTextLength(nodes: BlockNodeWithMath[]): number {
|
|
|
531
534
|
case 'paragraph':
|
|
532
535
|
len += countInlineTextLength(node.children);
|
|
533
536
|
break;
|
|
534
|
-
case '
|
|
535
|
-
len += node.
|
|
537
|
+
case 'code':
|
|
538
|
+
len += node.value.length;
|
|
536
539
|
break;
|
|
537
540
|
case 'math':
|
|
538
541
|
len += node.value.length;
|
|
@@ -541,21 +544,18 @@ function countBlockTextLength(nodes: BlockNodeWithMath[]): number {
|
|
|
541
544
|
len += countBlockTextLength(node.children);
|
|
542
545
|
break;
|
|
543
546
|
case 'list':
|
|
544
|
-
for (const item of node.
|
|
547
|
+
for (const item of node.children) {
|
|
545
548
|
len += countBlockTextLength(item.children);
|
|
546
549
|
}
|
|
547
550
|
break;
|
|
548
551
|
case 'table':
|
|
549
|
-
for (const
|
|
550
|
-
|
|
551
|
-
}
|
|
552
|
-
for (const row of node.rows) {
|
|
553
|
-
for (const cell of row) {
|
|
552
|
+
for (const row of node.children) {
|
|
553
|
+
for (const cell of row.children) {
|
|
554
554
|
len += countInlineTextLength(cell.children);
|
|
555
555
|
}
|
|
556
556
|
}
|
|
557
557
|
break;
|
|
558
|
-
case '
|
|
558
|
+
case 'thematicBreak':
|
|
559
559
|
break;
|
|
560
560
|
case 'image':
|
|
561
561
|
len += node.alt.length;
|
|
@@ -768,7 +768,7 @@ function getCitationNumber(ctx: CitationContext, sourceId: string): number {
|
|
|
768
768
|
}
|
|
769
769
|
|
|
770
770
|
function renderInline(
|
|
771
|
-
node:
|
|
771
|
+
node: MarkdownAstPhrasingContent,
|
|
772
772
|
index: number,
|
|
773
773
|
onLinkClick: MarkdownProps['onLinkClick'] | undefined,
|
|
774
774
|
cursor: StreamingCursor,
|
|
@@ -780,7 +780,7 @@ function renderInline(
|
|
|
780
780
|
switch (node.type) {
|
|
781
781
|
case 'text': {
|
|
782
782
|
if (inlinePlugins && inlinePlugins.length > 0) {
|
|
783
|
-
const segments = applyInlinePlugins(node.
|
|
783
|
+
const segments = applyInlinePlugins(node.value, inlinePlugins);
|
|
784
784
|
// If no plugin matched, fall through to the normal path
|
|
785
785
|
// O(1) guard: applyInlinePlugins returns a single text segment when
|
|
786
786
|
// nothing matched — skip the plugin path entirely in that case.
|
|
@@ -803,9 +803,9 @@ function renderInline(
|
|
|
803
803
|
return <Fragment key={index}>{result}</Fragment>;
|
|
804
804
|
}
|
|
805
805
|
}
|
|
806
|
-
return wrapTextWithFade(node.
|
|
806
|
+
return wrapTextWithFade(node.value, cursor, index);
|
|
807
807
|
}
|
|
808
|
-
case '
|
|
808
|
+
case 'strong':
|
|
809
809
|
return (
|
|
810
810
|
<strong key={index} {...stylex.props(styles.bold)}>
|
|
811
811
|
{node.children.map((c, i) =>
|
|
@@ -822,7 +822,7 @@ function renderInline(
|
|
|
822
822
|
)}
|
|
823
823
|
</strong>
|
|
824
824
|
);
|
|
825
|
-
case '
|
|
825
|
+
case 'emphasis':
|
|
826
826
|
return (
|
|
827
827
|
<em key={index}>
|
|
828
828
|
{node.children.map((c, i) =>
|
|
@@ -839,7 +839,7 @@ function renderInline(
|
|
|
839
839
|
)}
|
|
840
840
|
</em>
|
|
841
841
|
);
|
|
842
|
-
case '
|
|
842
|
+
case 'delete':
|
|
843
843
|
return (
|
|
844
844
|
<del key={index} {...stylex.props(styles.strikethrough)}>
|
|
845
845
|
{node.children.map((c, i) =>
|
|
@@ -856,18 +856,18 @@ function renderInline(
|
|
|
856
856
|
)}
|
|
857
857
|
</del>
|
|
858
858
|
);
|
|
859
|
-
case '
|
|
859
|
+
case 'inlineCode': {
|
|
860
860
|
// Track code content length for cursor but don't split inside code
|
|
861
|
-
cursor.offset += node.
|
|
861
|
+
cursor.offset += node.value.length;
|
|
862
862
|
const InlineCodeComp = components?.inlineCode;
|
|
863
863
|
const codeEl = InlineCodeComp ? (
|
|
864
|
-
<InlineCodeComp key={index}>{node.
|
|
864
|
+
<InlineCodeComp key={index}>{node.value}</InlineCodeComp>
|
|
865
865
|
) : (
|
|
866
|
-
<Code key={index}>{node.
|
|
866
|
+
<Code key={index}>{node.value}</Code>
|
|
867
867
|
);
|
|
868
868
|
return codeEl;
|
|
869
869
|
}
|
|
870
|
-
case '
|
|
870
|
+
case 'inlineMath': {
|
|
871
871
|
const MathComp = components?.math;
|
|
872
872
|
if (MathComp == null) {
|
|
873
873
|
return wrapTextWithFade(`$${node.value}$`, cursor, index);
|
|
@@ -876,7 +876,7 @@ function renderInline(
|
|
|
876
876
|
return <MathComp key={index} value={node.value} display="inline" />;
|
|
877
877
|
}
|
|
878
878
|
case 'link': {
|
|
879
|
-
const safeHref = sanitizeUrl(node.
|
|
879
|
+
const safeHref = sanitizeUrl(node.url);
|
|
880
880
|
if (safeHref == null) {
|
|
881
881
|
// Unsafe URL — render as plain text
|
|
882
882
|
return (
|
|
@@ -954,7 +954,7 @@ function renderInline(
|
|
|
954
954
|
);
|
|
955
955
|
}
|
|
956
956
|
case 'image': {
|
|
957
|
-
const safeSrc = sanitizeUrl(node.
|
|
957
|
+
const safeSrc = sanitizeUrl(node.url);
|
|
958
958
|
if (safeSrc == null) {
|
|
959
959
|
return <span key={index}>[{node.alt}]</span>;
|
|
960
960
|
}
|
|
@@ -1012,13 +1012,13 @@ function renderInline(
|
|
|
1012
1012
|
// ---------------------------------------------------------------------------
|
|
1013
1013
|
|
|
1014
1014
|
function getElementSpacing(
|
|
1015
|
-
node:
|
|
1015
|
+
node: MarkdownAstBlockContent,
|
|
1016
1016
|
density: 'default' | 'compact',
|
|
1017
1017
|
): StyleXStyles {
|
|
1018
1018
|
const compact = density === 'compact';
|
|
1019
1019
|
switch (node.type) {
|
|
1020
1020
|
case 'heading':
|
|
1021
|
-
return node.
|
|
1021
|
+
return node.depth <= 3
|
|
1022
1022
|
? compact
|
|
1023
1023
|
? styles.spacingHeadingMajorCompact
|
|
1024
1024
|
: styles.spacingHeadingMajorDefault
|
|
@@ -1029,7 +1029,7 @@ function getElementSpacing(
|
|
|
1029
1029
|
return compact
|
|
1030
1030
|
? styles.spacingParagraphCompact
|
|
1031
1031
|
: styles.spacingParagraphDefault;
|
|
1032
|
-
case '
|
|
1032
|
+
case 'code':
|
|
1033
1033
|
case 'math':
|
|
1034
1034
|
return compact
|
|
1035
1035
|
? styles.spacingCodeblockCompact
|
|
@@ -1042,7 +1042,7 @@ function getElementSpacing(
|
|
|
1042
1042
|
return compact ? styles.spacingListCompact : styles.spacingListDefault;
|
|
1043
1043
|
case 'table':
|
|
1044
1044
|
return compact ? styles.spacingTableCompact : styles.spacingTableDefault;
|
|
1045
|
-
case '
|
|
1045
|
+
case 'thematicBreak':
|
|
1046
1046
|
return compact ? styles.spacingHrCompact : styles.spacingHrDefault;
|
|
1047
1047
|
case 'image':
|
|
1048
1048
|
return compact ? styles.spacingImageCompact : styles.spacingImageDefault;
|
|
@@ -1057,15 +1057,17 @@ function getElementSpacing(
|
|
|
1057
1057
|
* Compute per-column min-widths from table AST content.
|
|
1058
1058
|
* Buckets: ≤6 chars → 60px, 7–15 → 80px, >15 → 120px.
|
|
1059
1059
|
*/
|
|
1060
|
-
function computeTableColumnMinWidths(node: {
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1060
|
+
function computeTableColumnMinWidths(node: MarkdownAstTable): number[] {
|
|
1061
|
+
const [header, ...rows] = node.children;
|
|
1062
|
+
if (header == null) {
|
|
1063
|
+
return [];
|
|
1064
|
+
}
|
|
1065
|
+
return header.children.map((cell, colIdx) => {
|
|
1066
|
+
let maxLen = countInlineTextLength(cell.children);
|
|
1067
|
+
for (const row of rows) {
|
|
1068
|
+
const rowCell = row.children[colIdx];
|
|
1069
|
+
if (rowCell != null) {
|
|
1070
|
+
const len = countInlineTextLength(rowCell.children);
|
|
1069
1071
|
if (len > maxLen) {
|
|
1070
1072
|
maxLen = len;
|
|
1071
1073
|
}
|
|
@@ -1076,7 +1078,7 @@ function computeTableColumnMinWidths(node: {
|
|
|
1076
1078
|
}
|
|
1077
1079
|
|
|
1078
1080
|
function renderBlock(
|
|
1079
|
-
node:
|
|
1081
|
+
node: MarkdownAstBlockContent,
|
|
1080
1082
|
index: number,
|
|
1081
1083
|
blockCount: number,
|
|
1082
1084
|
density: 'default' | 'compact',
|
|
@@ -1090,7 +1092,7 @@ function renderBlock(
|
|
|
1090
1092
|
inlinePlugins: MarkdownInlinePlugin[] | undefined,
|
|
1091
1093
|
components: Partial<MarkdownComponents> | undefined,
|
|
1092
1094
|
t: TranslatorFn,
|
|
1093
|
-
headingIdMap?: ReadonlyMap<
|
|
1095
|
+
headingIdMap?: ReadonlyMap<MarkdownAstBlockContent, string>,
|
|
1094
1096
|
): SyncReactNode {
|
|
1095
1097
|
const blockAlignMargin = BLOCK_ALIGN_MARGIN[contentAlign];
|
|
1096
1098
|
const blockAlignStyle =
|
|
@@ -1103,7 +1105,7 @@ function renderBlock(
|
|
|
1103
1105
|
|
|
1104
1106
|
switch (node.type) {
|
|
1105
1107
|
case 'heading': {
|
|
1106
|
-
const level = Math.min(node.
|
|
1108
|
+
const level = Math.min(node.depth + headingLevelStart - 1, 6) as
|
|
1107
1109
|
1 | 2 | 3 | 4 | 5 | 6;
|
|
1108
1110
|
const headingChildren = node.children.map((c, i) =>
|
|
1109
1111
|
renderInline(
|
|
@@ -1203,16 +1205,16 @@ function renderBlock(
|
|
|
1203
1205
|
</div>
|
|
1204
1206
|
);
|
|
1205
1207
|
}
|
|
1206
|
-
case '
|
|
1208
|
+
case 'code': {
|
|
1207
1209
|
// Track codeblock content in cursor for accurate character counting
|
|
1208
|
-
cursor.offset += node.
|
|
1210
|
+
cursor.offset += node.value.length;
|
|
1209
1211
|
const CodeBlockComp = components?.code;
|
|
1210
1212
|
if (CodeBlockComp) {
|
|
1211
1213
|
return (
|
|
1212
1214
|
<CodeBlockComp
|
|
1213
1215
|
key={index}
|
|
1214
|
-
code={node.
|
|
1215
|
-
language={node.
|
|
1216
|
+
code={node.value}
|
|
1217
|
+
language={node.lang ?? 'plaintext'}
|
|
1216
1218
|
/>
|
|
1217
1219
|
);
|
|
1218
1220
|
}
|
|
@@ -1229,8 +1231,8 @@ function renderBlock(
|
|
|
1229
1231
|
),
|
|
1230
1232
|
)}>
|
|
1231
1233
|
<CodeBlock
|
|
1232
|
-
code={node.
|
|
1233
|
-
language={node.
|
|
1234
|
+
code={node.value}
|
|
1235
|
+
language={node.lang ?? 'plaintext'}
|
|
1234
1236
|
isCollapsible
|
|
1235
1237
|
xstyle={[
|
|
1236
1238
|
contentWidthValue != null
|
|
@@ -1316,11 +1318,12 @@ function renderBlock(
|
|
|
1316
1318
|
case 'list': {
|
|
1317
1319
|
// Detect task lists: all items have a checked state
|
|
1318
1320
|
const isTaskList =
|
|
1319
|
-
node.
|
|
1321
|
+
node.children.length > 0 &&
|
|
1322
|
+
node.children.every(item => item.checked != null);
|
|
1320
1323
|
|
|
1321
1324
|
if (isTaskList) {
|
|
1322
1325
|
// Extract labels from task items — render as rich inline content
|
|
1323
|
-
const checkedValues = node.
|
|
1326
|
+
const checkedValues = node.children
|
|
1324
1327
|
.map((item, i) => ({item, key: `task-${i}`}))
|
|
1325
1328
|
.filter(({item}) => item.checked)
|
|
1326
1329
|
.map(({key}) => key);
|
|
@@ -1343,7 +1346,7 @@ function renderBlock(
|
|
|
1343
1346
|
xstyle={styles.blockIndent}
|
|
1344
1347
|
isReadOnly
|
|
1345
1348
|
density="compact">
|
|
1346
|
-
{node.
|
|
1349
|
+
{node.children.map((item, i) => {
|
|
1347
1350
|
const firstChild = item.children[0];
|
|
1348
1351
|
const isInline =
|
|
1349
1352
|
item.children.length === 1 &&
|
|
@@ -1423,7 +1426,7 @@ function renderBlock(
|
|
|
1423
1426
|
density="compact"
|
|
1424
1427
|
start={node.ordered ? node.start : undefined}
|
|
1425
1428
|
xstyle={styles.blockIndent}>
|
|
1426
|
-
{node.
|
|
1429
|
+
{node.children.map((item, i) => {
|
|
1427
1430
|
const firstChild = item.children[0];
|
|
1428
1431
|
const isInline =
|
|
1429
1432
|
item.children.length === 1 && firstChild?.type === 'paragraph';
|
|
@@ -1483,6 +1486,7 @@ function renderBlock(
|
|
|
1483
1486
|
}
|
|
1484
1487
|
case 'table': {
|
|
1485
1488
|
const colMinWidths = computeTableColumnMinWidths(node);
|
|
1489
|
+
const [header, ...rows] = node.children;
|
|
1486
1490
|
|
|
1487
1491
|
return (
|
|
1488
1492
|
<div
|
|
@@ -1512,14 +1516,14 @@ function renderBlock(
|
|
|
1512
1516
|
<Table dividers="rows" textOverflow="wrap">
|
|
1513
1517
|
<TableHeader>
|
|
1514
1518
|
<TableRow>
|
|
1515
|
-
{
|
|
1519
|
+
{header?.children.map((h, i) => (
|
|
1516
1520
|
<TableHeaderCell
|
|
1517
1521
|
// eslint-disable-next-line @eslint-react/no-array-index-key -- markdown table columns are positional by definition
|
|
1518
1522
|
key={i}
|
|
1519
1523
|
xstyle={[
|
|
1520
1524
|
dynamicStyles.cellMinWidth(`${colMinWidths[i]}px`),
|
|
1521
|
-
node.
|
|
1522
|
-
node.
|
|
1525
|
+
node.align[i] === 'center' && cellAlignStyles.center,
|
|
1526
|
+
node.align[i] === 'right' && cellAlignStyles.end,
|
|
1523
1527
|
]}>
|
|
1524
1528
|
{h.children.map((c, j) =>
|
|
1525
1529
|
renderInline(
|
|
@@ -1538,14 +1542,14 @@ function renderBlock(
|
|
|
1538
1542
|
</TableRow>
|
|
1539
1543
|
</TableHeader>
|
|
1540
1544
|
<TableBody>
|
|
1541
|
-
{
|
|
1542
|
-
const cells = row.map((cell, j) => (
|
|
1545
|
+
{rows.map((row, i) => {
|
|
1546
|
+
const cells = row.children.map((cell, j) => (
|
|
1543
1547
|
<TableCell
|
|
1544
1548
|
// eslint-disable-next-line @eslint-react/no-array-index-key -- markdown table cells are positional by row and column
|
|
1545
1549
|
key={j}
|
|
1546
1550
|
xstyle={[
|
|
1547
|
-
node.
|
|
1548
|
-
node.
|
|
1551
|
+
node.align[j] === 'center' && cellAlignStyles.center,
|
|
1552
|
+
node.align[j] === 'right' && cellAlignStyles.end,
|
|
1549
1553
|
]}>
|
|
1550
1554
|
{cell.children.map((c, k) =>
|
|
1551
1555
|
renderInline(
|
|
@@ -1574,7 +1578,7 @@ function renderBlock(
|
|
|
1574
1578
|
</div>
|
|
1575
1579
|
);
|
|
1576
1580
|
}
|
|
1577
|
-
case '
|
|
1581
|
+
case 'thematicBreak': {
|
|
1578
1582
|
const HrComp = components?.hr;
|
|
1579
1583
|
if (HrComp) {
|
|
1580
1584
|
return <HrComp key={index} />;
|
|
@@ -1595,7 +1599,7 @@ function renderBlock(
|
|
|
1595
1599
|
);
|
|
1596
1600
|
}
|
|
1597
1601
|
case 'image': {
|
|
1598
|
-
const safeSrc = sanitizeUrl(node.
|
|
1602
|
+
const safeSrc = sanitizeUrl(node.url);
|
|
1599
1603
|
if (safeSrc == null) {
|
|
1600
1604
|
return (
|
|
1601
1605
|
<div
|
|
@@ -1719,21 +1723,25 @@ export function Markdown({
|
|
|
1719
1723
|
}
|
|
1720
1724
|
const options = hasMathRenderer ? mathParseOptions : legacyParseOptions;
|
|
1721
1725
|
const input = trimStreamingArtifacts(smoothedText, options);
|
|
1722
|
-
return
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1726
|
+
return (
|
|
1727
|
+
hasMathRenderer
|
|
1728
|
+
? parseMarkdownAstIncremental(
|
|
1729
|
+
input,
|
|
1730
|
+
incrementalStateRef.current,
|
|
1731
|
+
mathParseOptions,
|
|
1732
|
+
)
|
|
1733
|
+
: parseMarkdownAstIncremental(
|
|
1734
|
+
input,
|
|
1735
|
+
incrementalStateRef.current,
|
|
1736
|
+
legacyParseOptions,
|
|
1737
|
+
)
|
|
1738
|
+
).children;
|
|
1733
1739
|
}
|
|
1734
|
-
return
|
|
1735
|
-
|
|
1736
|
-
|
|
1740
|
+
return (
|
|
1741
|
+
hasMathRenderer
|
|
1742
|
+
? parseMarkdownAst(children, mathParseOptions)
|
|
1743
|
+
: parseMarkdownAst(children, legacyParseOptions)
|
|
1744
|
+
).children;
|
|
1737
1745
|
}, [
|
|
1738
1746
|
display,
|
|
1739
1747
|
smoothedText,
|
|
@@ -1754,11 +1762,11 @@ export function Markdown({
|
|
|
1754
1762
|
if (display === 'inline' || blocks.length === 0) {
|
|
1755
1763
|
return undefined;
|
|
1756
1764
|
}
|
|
1757
|
-
const map = new Map<
|
|
1765
|
+
const map = new Map<MarkdownAstBlockContent, string>();
|
|
1758
1766
|
const counts = new Map<string, number>();
|
|
1759
1767
|
for (const block of blocks) {
|
|
1760
1768
|
if (block.type === 'heading') {
|
|
1761
|
-
const label =
|
|
1769
|
+
const label = markdownAstText(block.children).trim();
|
|
1762
1770
|
map.set(block, uniqueSlug(slugify(label), counts));
|
|
1763
1771
|
}
|
|
1764
1772
|
}
|
|
@@ -1774,8 +1782,8 @@ export function Markdown({
|
|
|
1774
1782
|
? trimStreamingArtifacts(smoothedText, options)
|
|
1775
1783
|
: children;
|
|
1776
1784
|
return hasMathRenderer
|
|
1777
|
-
?
|
|
1778
|
-
:
|
|
1785
|
+
? parseInlineAst(input, mathParseOptions)
|
|
1786
|
+
: parseInlineAst(input, legacyParseOptions);
|
|
1779
1787
|
}, [
|
|
1780
1788
|
display,
|
|
1781
1789
|
smoothedText,
|
|
@@ -1800,8 +1808,10 @@ export function Markdown({
|
|
|
1800
1808
|
return Math.min(Math.ceil(duration / tickMs), 12);
|
|
1801
1809
|
}, [token]);
|
|
1802
1810
|
|
|
1803
|
-
const prevBlocksRef = useRef<
|
|
1804
|
-
const prevInlineNodesRef = useRef<
|
|
1811
|
+
const prevBlocksRef = useRef<ReadonlyArray<MarkdownAstBlockContent>>([]);
|
|
1812
|
+
const prevInlineNodesRef = useRef<ReadonlyArray<MarkdownAstPhrasingContent>>(
|
|
1813
|
+
[],
|
|
1814
|
+
);
|
|
1805
1815
|
const boundariesRef = useRef<number[]>([]);
|
|
1806
1816
|
const smoothedLen = smoothedText.length;
|
|
1807
1817
|
const boundaries = useMemo(() => {
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
import {describe, expect, it} from 'vitest';
|
|
4
|
+
import {
|
|
5
|
+
createIncrementalState,
|
|
6
|
+
parseMarkdown,
|
|
7
|
+
parseMarkdownAst,
|
|
8
|
+
} from './parser';
|
|
9
|
+
|
|
10
|
+
describe('canonical Markdown AST', () => {
|
|
11
|
+
it('uses MDAST-aligned names and fields for every built-in structure', () => {
|
|
12
|
+
const ast = parseMarkdownAst(
|
|
13
|
+
'# Title\n\n**bold** and *emphasis* with `code` and [link](/docs).\n\n- item\n\n| A | B |\n| - | - |\n| 1 | 2 |\n\n```ts\nconst value = 1;\n```\n\n---',
|
|
14
|
+
);
|
|
15
|
+
|
|
16
|
+
expect(ast).toMatchObject({
|
|
17
|
+
type: 'root',
|
|
18
|
+
children: [
|
|
19
|
+
{
|
|
20
|
+
type: 'heading',
|
|
21
|
+
depth: 1,
|
|
22
|
+
children: [{type: 'text', value: 'Title'}],
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
type: 'paragraph',
|
|
26
|
+
children: [
|
|
27
|
+
{type: 'strong'},
|
|
28
|
+
{type: 'text'},
|
|
29
|
+
{type: 'emphasis'},
|
|
30
|
+
{type: 'text'},
|
|
31
|
+
{type: 'inlineCode', value: 'code'},
|
|
32
|
+
{type: 'text'},
|
|
33
|
+
{type: 'link', url: '/docs'},
|
|
34
|
+
{type: 'text', value: '.'},
|
|
35
|
+
],
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
type: 'list',
|
|
39
|
+
ordered: false,
|
|
40
|
+
children: [{type: 'listItem'}],
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
type: 'table',
|
|
44
|
+
align: [null, null],
|
|
45
|
+
children: [
|
|
46
|
+
{
|
|
47
|
+
type: 'tableRow',
|
|
48
|
+
children: [{type: 'tableCell'}, {type: 'tableCell'}],
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
type: 'tableRow',
|
|
52
|
+
children: [{type: 'tableCell'}, {type: 'tableCell'}],
|
|
53
|
+
},
|
|
54
|
+
],
|
|
55
|
+
},
|
|
56
|
+
{type: 'code', lang: 'ts', value: 'const value = 1;'},
|
|
57
|
+
{type: 'thematicBreak'},
|
|
58
|
+
],
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('represents remaining built-ins without legacy aliases', () => {
|
|
63
|
+
const ast = parseMarkdownAst(
|
|
64
|
+
'> quote\n\n\n\nParagraph  [src] $x$ \nnext\n\n$$\ny\n$$',
|
|
65
|
+
{math: true, sourceIds: new Set(['src'])},
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
expect(ast.children).toMatchObject([
|
|
69
|
+
{
|
|
70
|
+
type: 'blockquote',
|
|
71
|
+
children: [
|
|
72
|
+
{type: 'paragraph', children: [{type: 'text', value: 'quote'}]},
|
|
73
|
+
],
|
|
74
|
+
},
|
|
75
|
+
{type: 'image', url: '/block.png', alt: 'Block'},
|
|
76
|
+
{
|
|
77
|
+
type: 'paragraph',
|
|
78
|
+
children: [
|
|
79
|
+
{type: 'text', value: 'Paragraph '},
|
|
80
|
+
{type: 'image', url: '/inline.png', alt: 'Inline'},
|
|
81
|
+
{type: 'text', value: ' '},
|
|
82
|
+
{type: 'citation', sourceId: 'src'},
|
|
83
|
+
{type: 'text', value: ' '},
|
|
84
|
+
{type: 'inlineMath', value: 'x'},
|
|
85
|
+
{type: 'break'},
|
|
86
|
+
{type: 'text', value: 'next'},
|
|
87
|
+
],
|
|
88
|
+
},
|
|
89
|
+
{type: 'math', value: 'y'},
|
|
90
|
+
]);
|
|
91
|
+
expect(parseMarkdownAst('```\nplain\n```').children[0]).toEqual({
|
|
92
|
+
type: 'code',
|
|
93
|
+
lang: null,
|
|
94
|
+
value: 'plain',
|
|
95
|
+
});
|
|
96
|
+
expect(parseMarkdown('```\nplain\n```')[0]).toEqual({
|
|
97
|
+
type: 'codeblock',
|
|
98
|
+
language: 'plaintext',
|
|
99
|
+
content: 'plain',
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('keeps source provenance canonical while projecting the released range', () => {
|
|
104
|
+
const source = '# Heading\n\nParagraph';
|
|
105
|
+
const ast = parseMarkdownAst(source, {sourceRanges: true});
|
|
106
|
+
const legacy = parseMarkdown(source, {sourceRanges: true});
|
|
107
|
+
|
|
108
|
+
expect(ast.children[0].position).toEqual({
|
|
109
|
+
start: {offset: 0},
|
|
110
|
+
end: {offset: 9},
|
|
111
|
+
});
|
|
112
|
+
expect(legacy[0]).toEqual({
|
|
113
|
+
type: 'heading',
|
|
114
|
+
level: 1,
|
|
115
|
+
children: [{type: 'text', content: 'Heading'}],
|
|
116
|
+
range: {start: 0, end: 9},
|
|
117
|
+
});
|
|
118
|
+
expect('position' in legacy[0]).toBe(false);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('preserves optional legacy keys and insertion order exactly', () => {
|
|
122
|
+
const [image, list] = parseMarkdown('\n\n- item');
|
|
123
|
+
|
|
124
|
+
expect(Object.keys(image)).toEqual(['type', 'alt', 'src']);
|
|
125
|
+
expect(Object.keys(list)).toEqual([
|
|
126
|
+
'type',
|
|
127
|
+
'ordered',
|
|
128
|
+
'start',
|
|
129
|
+
'delimiter',
|
|
130
|
+
'loose',
|
|
131
|
+
'items',
|
|
132
|
+
]);
|
|
133
|
+
if (list.type !== 'list') {
|
|
134
|
+
throw new Error('Expected list');
|
|
135
|
+
}
|
|
136
|
+
expect(Object.keys(list.items[0])).toEqual(['checked', 'children']);
|
|
137
|
+
expect(list).toStrictEqual({
|
|
138
|
+
type: 'list',
|
|
139
|
+
ordered: false,
|
|
140
|
+
start: undefined,
|
|
141
|
+
delimiter: undefined,
|
|
142
|
+
loose: undefined,
|
|
143
|
+
items: [
|
|
144
|
+
{
|
|
145
|
+
checked: undefined,
|
|
146
|
+
children: [
|
|
147
|
+
{
|
|
148
|
+
type: 'paragraph',
|
|
149
|
+
children: [{type: 'text', content: 'item'}],
|
|
150
|
+
},
|
|
151
|
+
],
|
|
152
|
+
},
|
|
153
|
+
],
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it('does not expose canonical fields through released parser results', () => {
|
|
158
|
+
const state = createIncrementalState();
|
|
159
|
+
const blocks = parseMarkdown('# Heading\n\nParagraph');
|
|
160
|
+
|
|
161
|
+
expect(blocks[0]).toEqual({
|
|
162
|
+
type: 'heading',
|
|
163
|
+
level: 1,
|
|
164
|
+
children: [{type: 'text', content: 'Heading'}],
|
|
165
|
+
});
|
|
166
|
+
expect('depth' in blocks[0]).toBe(false);
|
|
167
|
+
expect(state.settledBlocks).toEqual([]);
|
|
168
|
+
});
|
|
169
|
+
});
|