@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.
Files changed (103) hide show
  1. package/dist/CheckboxInput/CheckboxInput.d.ts.map +1 -1
  2. package/dist/CheckboxInput/CheckboxInput.js +12 -2
  3. package/dist/Collapsible/Collapsible.d.ts.map +1 -1
  4. package/dist/Collapsible/Collapsible.js +6 -1
  5. package/dist/Kbd/Kbd.d.ts +5 -3
  6. package/dist/Kbd/Kbd.d.ts.map +1 -1
  7. package/dist/Kbd/Kbd.js +36 -42
  8. package/dist/Link/Link.d.ts.map +1 -1
  9. package/dist/Link/Link.js +6 -2
  10. package/dist/Markdown/Markdown.d.ts.map +1 -1
  11. package/dist/Markdown/Markdown.js +63 -59
  12. package/dist/Markdown/ast.d.ts +124 -0
  13. package/dist/Markdown/ast.d.ts.map +1 -0
  14. package/dist/Markdown/ast.js +38 -0
  15. package/dist/Markdown/parser.d.ts +14 -8
  16. package/dist/Markdown/parser.d.ts.map +1 -1
  17. package/dist/Markdown/parser.js +352 -97
  18. package/dist/Outline/parseOutlineFromMarkdown.d.ts.map +1 -1
  19. package/dist/Outline/parseOutlineFromMarkdown.js +7 -7
  20. package/dist/PowerSearch/PowerSearchEditPopover.d.ts.map +1 -1
  21. package/dist/PowerSearch/PowerSearchEditPopover.js +46 -30
  22. package/dist/RadioList/RadioListItem.d.ts.map +1 -1
  23. package/dist/RadioList/RadioListItem.js +13 -1
  24. package/dist/SegmentedControl/SegmentedControlItem.d.ts.map +1 -1
  25. package/dist/SegmentedControl/SegmentedControlItem.js +5 -5
  26. package/dist/SideNav/SideNav.d.ts +2 -1
  27. package/dist/SideNav/SideNav.d.ts.map +1 -1
  28. package/dist/SideNav/SideNav.js +9 -3
  29. package/dist/Slider/Slider.d.ts.map +1 -1
  30. package/dist/Slider/Slider.js +14 -8
  31. package/dist/Switch/Switch.d.ts.map +1 -1
  32. package/dist/Switch/Switch.js +11 -0
  33. package/dist/TabList/Tab.d.ts +1 -1
  34. package/dist/TabList/Tab.d.ts.map +1 -1
  35. package/dist/TabList/Tab.js +20 -7
  36. package/dist/ToggleButton/ToggleButton.d.ts +2 -1
  37. package/dist/ToggleButton/ToggleButton.d.ts.map +1 -1
  38. package/dist/ToggleButton/ToggleButton.js +7 -1
  39. package/dist/Typeahead/BaseTypeahead.d.ts.map +1 -1
  40. package/dist/Typeahead/BaseTypeahead.js +15 -6
  41. package/dist/astryx.css +8 -0
  42. package/dist/utils/interactionOverlay.stylex.d.ts +8 -0
  43. package/dist/utils/interactionOverlay.stylex.d.ts.map +1 -1
  44. package/dist/utils/interactionOverlay.stylex.js +9 -0
  45. package/package.json +3 -3
  46. package/src/Button/__tests__/Button.a11y.chromium.spec.ts +17 -1
  47. package/src/Button/__tests__/Button.a11y.known-failures.ts +0 -29
  48. package/src/Button/__tests__/Button.a11y.renders.tsx +9 -2
  49. package/src/Button/__tests__/Button.a11y.states.ts +9 -0
  50. package/src/CheckboxInput/CheckboxInput.doc.mjs +11 -0
  51. package/src/CheckboxInput/CheckboxInput.test.tsx +34 -0
  52. package/src/CheckboxInput/CheckboxInput.tsx +21 -1
  53. package/src/ClickableCard/ClickableCard.test.tsx +102 -5
  54. package/src/Collapsible/Collapsible.doc.mjs +11 -0
  55. package/src/Collapsible/Collapsible.test.tsx +21 -0
  56. package/src/Collapsible/Collapsible.tsx +5 -0
  57. package/src/Kbd/Kbd.doc.mjs +3 -3
  58. package/src/Kbd/Kbd.test.tsx +57 -1
  59. package/src/Kbd/Kbd.tsx +57 -37
  60. package/src/Link/Link.doc.mjs +11 -0
  61. package/src/Link/Link.test.tsx +24 -0
  62. package/src/Link/Link.tsx +5 -0
  63. package/src/Markdown/Markdown.spec.md +159 -43
  64. package/src/Markdown/Markdown.test.tsx +11 -0
  65. package/src/Markdown/Markdown.tsx +106 -96
  66. package/src/Markdown/ast.test.ts +169 -0
  67. package/src/Markdown/ast.ts +231 -0
  68. package/src/Markdown/incremental.test.ts +12 -0
  69. package/src/Markdown/parser.ts +427 -118
  70. package/src/Outline/Outline.spec.md +1 -1
  71. package/src/Outline/modules/parseOutlineFromMarkdown.spec.md +142 -0
  72. package/src/Outline/parseOutlineFromMarkdown.ts +8 -13
  73. package/src/PowerSearch/PowerSearchEditPopover.test.tsx +150 -1
  74. package/src/PowerSearch/PowerSearchEditPopover.tsx +51 -28
  75. package/src/RadioList/RadioList.doc.mjs +11 -0
  76. package/src/RadioList/RadioList.test.tsx +32 -0
  77. package/src/RadioList/RadioListItem.tsx +25 -1
  78. package/src/SegmentedControl/SegmentedControl.doc.mjs +2 -2
  79. package/src/SegmentedControl/SegmentedControl.test.tsx +31 -0
  80. package/src/SegmentedControl/SegmentedControlItem.tsx +6 -9
  81. package/src/SideNav/SideNav.doc.mjs +1 -1
  82. package/src/SideNav/SideNav.test.tsx +10 -0
  83. package/src/SideNav/SideNav.tsx +14 -2
  84. package/src/Slider/Slider.doc.mjs +11 -0
  85. package/src/Slider/Slider.test.tsx +128 -0
  86. package/src/Slider/Slider.tsx +25 -7
  87. package/src/Switch/Switch.doc.mjs +11 -0
  88. package/src/Switch/Switch.test.tsx +16 -0
  89. package/src/Switch/Switch.tsx +28 -0
  90. package/src/TabList/Tab.tsx +35 -7
  91. package/src/TabList/TabList.doc.mjs +11 -0
  92. package/src/TabList/TabList.test.tsx +66 -0
  93. package/src/TabList/__tests__/Tabs.a11y.known-failures.ts +1 -34
  94. package/src/ToggleButton/ToggleButton.test.tsx +133 -0
  95. package/src/ToggleButton/ToggleButton.tsx +9 -2
  96. package/src/ToggleButton/__tests__/ToggleButton.a11y.chromium.spec.ts +209 -0
  97. package/src/Tokenizer/Tokenizer.spec.md +142 -75
  98. package/src/Typeahead/BaseTypeahead.spec.md +4 -3
  99. package/src/Typeahead/BaseTypeahead.tsx +15 -6
  100. package/src/Typeahead/Typeahead.test.tsx +53 -0
  101. package/src/__tests__/PressedState.a11y.chromium.spec.ts +813 -0
  102. package/src/__tests__/pressState.ts +93 -0
  103. 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
- parseInline,
52
- parseMarkdown,
53
- parseMarkdownIncremental,
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
- BlockNodeWithMath,
62
- InlineNodeWithMath,
63
- IncrementalState,
64
- MathParseOptions,
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(nodes: InlineNodeWithMath[]): number {
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.content.length;
496
+ len += node.value.length;
496
497
  break;
497
- case 'code':
498
- len += node.content.length;
498
+ case 'inlineCode':
499
+ len += node.value.length;
499
500
  break;
500
- case 'math':
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 'bold':
507
- case 'italic':
508
- case 'strikethrough':
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(nodes: BlockNodeWithMath[]): number {
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 'codeblock':
535
- len += node.content.length;
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.items) {
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 h of node.headers) {
550
- len += countInlineTextLength(h.children);
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 'hr':
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: InlineNodeWithMath,
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.content, inlinePlugins);
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.content, cursor, index);
806
+ return wrapTextWithFade(node.value, cursor, index);
807
807
  }
808
- case 'bold':
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 'italic':
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 'strikethrough':
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 'code': {
859
+ case 'inlineCode': {
860
860
  // Track code content length for cursor but don't split inside code
861
- cursor.offset += node.content.length;
861
+ cursor.offset += node.value.length;
862
862
  const InlineCodeComp = components?.inlineCode;
863
863
  const codeEl = InlineCodeComp ? (
864
- <InlineCodeComp key={index}>{node.content}</InlineCodeComp>
864
+ <InlineCodeComp key={index}>{node.value}</InlineCodeComp>
865
865
  ) : (
866
- <Code key={index}>{node.content}</Code>
866
+ <Code key={index}>{node.value}</Code>
867
867
  );
868
868
  return codeEl;
869
869
  }
870
- case 'math': {
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.href);
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.src);
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: BlockNodeWithMath,
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.level <= 3
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 'codeblock':
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 'hr':
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
- headers: {children: InlineNodeWithMath[]}[];
1062
- rows: {children: InlineNodeWithMath[]}[][];
1063
- }): number[] {
1064
- return node.headers.map((h, colIdx) => {
1065
- let maxLen = countInlineTextLength(h.children);
1066
- for (const row of node.rows) {
1067
- if (row[colIdx]) {
1068
- const len = countInlineTextLength(row[colIdx].children);
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: BlockNodeWithMath,
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<BlockNodeWithMath, string>,
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.level + headingLevelStart - 1, 6) as
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 'codeblock': {
1208
+ case 'code': {
1207
1209
  // Track codeblock content in cursor for accurate character counting
1208
- cursor.offset += node.content.length;
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.content}
1215
- language={node.language}
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.content}
1233
- language={node.language}
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.items.length > 0 && node.items.every(item => item.checked != null);
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.items
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.items.map((item, i) => {
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.items.map((item, i) => {
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
- {node.headers.map((h, i) => (
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.alignments[i] === 'center' && cellAlignStyles.center,
1522
- node.alignments[i] === 'right' && cellAlignStyles.end,
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
- {node.rows.map((row, i) => {
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.alignments[j] === 'center' && cellAlignStyles.center,
1548
- node.alignments[j] === 'right' && cellAlignStyles.end,
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 'hr': {
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.src);
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 hasMathRenderer
1723
- ? parseMarkdownIncremental(
1724
- input,
1725
- incrementalStateRef.current as IncrementalState<true>,
1726
- mathParseOptions,
1727
- )
1728
- : parseMarkdownIncremental(
1729
- input,
1730
- incrementalStateRef.current as IncrementalState<false>,
1731
- legacyParseOptions,
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 hasMathRenderer
1735
- ? parseMarkdown(children, mathParseOptions)
1736
- : parseMarkdown(children, legacyParseOptions);
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<BlockNodeWithMath, string>();
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 = inlineText(block.children).trim();
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
- ? parseInline(input, mathParseOptions)
1778
- : parseInline(input, legacyParseOptions);
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<BlockNodeWithMath[]>([]);
1804
- const prevInlineNodesRef = useRef<InlineNodeWithMath[]>([]);
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![Block](/block.png)\n\nParagraph ![Inline](/inline.png) [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('![Alt](/image.png)\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
+ });