@surrealdb/ui 1.2.12 → 1.2.13

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 (93) hide show
  1. package/.claude/launch.json +17 -0
  2. package/.oxfmtrc.json +27 -0
  3. package/.oxlintrc.json +23 -0
  4. package/.zed/settings.json +34 -34
  5. package/AGENTS.md +21 -19
  6. package/README.md +15 -10
  7. package/dist/ui.css +1 -1
  8. package/dist/ui.d.ts +234 -1
  9. package/dist/ui.js +8217 -7329
  10. package/dist/ui.js.map +1 -1
  11. package/doc/code-style.md +264 -0
  12. package/doc/stacker-plan.md +219 -0
  13. package/package.json +119 -116
  14. package/res/_mixins.scss +15 -11
  15. package/tests/_setup/e2e-helpers.tsx +85 -85
  16. package/tests/_setup/portable-stories.ts +11 -10
  17. package/tests/e2e/MarkdownEditor/content-blocks.test.tsx +140 -139
  18. package/tests/e2e/MarkdownEditor/edits.test.tsx +101 -100
  19. package/tests/e2e/MarkdownEditor/heading-fold.test.tsx +35 -34
  20. package/tests/e2e/MarkdownEditor/hybrid-widgets.test.tsx +178 -177
  21. package/tests/e2e/MarkdownEditor/image-on-image.test.tsx +50 -49
  22. package/tests/e2e/MarkdownEditor/jsx-block-content.test.tsx +195 -194
  23. package/tests/e2e/MarkdownEditor/jsx-highlight.test.tsx +47 -46
  24. package/tests/e2e/MarkdownEditor/jsx-inline-badges.test.tsx +35 -34
  25. package/tests/e2e/MarkdownEditor/jsx-inline-click.test.tsx +39 -38
  26. package/tests/e2e/MarkdownEditor/jsx-selection.test.tsx +28 -27
  27. package/tests/e2e/MarkdownEditor/link-placeholder.test.tsx +49 -48
  28. package/tests/e2e/MarkdownEditor/media-align.test.tsx +40 -39
  29. package/tests/e2e/MarkdownEditor/media-edit.test.tsx +49 -48
  30. package/tests/e2e/MarkdownEditor/media-sizing.test.tsx +108 -107
  31. package/tests/e2e/MarkdownEditor/modes.test.tsx +79 -78
  32. package/tests/e2e/MarkdownEditor/onimage-parity.test.tsx +29 -28
  33. package/tests/e2e/MarkdownEditor/regressions.test.tsx +194 -193
  34. package/tests/e2e/MarkdownEditor/slash-commands.test.tsx +81 -80
  35. package/tests/e2e/MarkdownEditor/table-click.test.tsx +36 -35
  36. package/tests/e2e/MarkdownEditor/table-controls.test.tsx +44 -43
  37. package/tests/e2e/MarkdownEditor/table-format.test.tsx +28 -27
  38. package/tests/e2e/MarkdownEditor/undo-redo.test.tsx +22 -21
  39. package/tests/e2e/MarkdownViewer/parity.test.tsx +144 -143
  40. package/tests/e2e/MarkdownViewer/render.test.tsx +20 -19
  41. package/tests/e2e/Stacker/stacker.test.tsx +304 -0
  42. package/tests/unit/Editor/helpers.test.ts +26 -26
  43. package/tests/unit/MarkdownConformance/error-isolation.test.tsx +26 -25
  44. package/tests/unit/MarkdownConformance/indentation.test.tsx +20 -19
  45. package/tests/unit/MarkdownConformance/inline-code-fuzz.test.tsx +255 -0
  46. package/tests/unit/MarkdownConformance/jsx-attributes.test.tsx +39 -38
  47. package/tests/unit/MarkdownConformance/jsx-fuzz.test.tsx +122 -122
  48. package/tests/unit/MarkdownConformance/links.test.tsx +78 -54
  49. package/tests/unit/MarkdownConformance/render-helpers.tsx +60 -60
  50. package/tests/unit/MarkdownEditor/code-info.test.ts +58 -58
  51. package/tests/unit/MarkdownEditor/decorations.test.ts +423 -421
  52. package/tests/unit/MarkdownEditor/editor-ready.test.ts +22 -22
  53. package/tests/unit/MarkdownEditor/html-descriptors.test.ts +70 -70
  54. package/tests/unit/MarkdownEditor/indented-fence.test.ts +80 -80
  55. package/tests/unit/MarkdownEditor/jsx-attr-scan.test.ts +87 -87
  56. package/tests/unit/MarkdownEditor/jsx-block-widget.test.ts +51 -51
  57. package/tests/unit/MarkdownEditor/jsx-tag-grammar.test.ts +63 -63
  58. package/tests/unit/MarkdownEditor/list-indent.test.ts +67 -67
  59. package/tests/unit/MarkdownEditor/slash-commands.test.ts +150 -150
  60. package/tests/unit/MarkdownEditor/table-format.test.ts +67 -67
  61. package/tests/unit/MarkdownEditor/table.test.ts +96 -96
  62. package/tests/unit/MarkdownEditor/triggers.test.ts +167 -167
  63. package/tests/unit/MarkdownEditor/widget-store.test.ts +90 -90
  64. package/tests/unit/MarkdownViewer/callout.test.tsx +42 -42
  65. package/tests/unit/MarkdownViewer/code-highlight.test.tsx +41 -41
  66. package/tests/unit/MarkdownViewer/code-title.test.tsx +45 -45
  67. package/tests/unit/MarkdownViewer/features.test.tsx +129 -129
  68. package/tests/unit/MarkdownViewer/headings.test.tsx +28 -28
  69. package/tests/unit/MarkdownViewer/indented-jsx-children.test.tsx +53 -53
  70. package/tests/unit/MarkdownViewer/jsx-block-nesting.test.tsx +137 -137
  71. package/tests/unit/MarkdownViewer/jsx.test.tsx +188 -188
  72. package/tests/unit/MarkdownViewer/list-bullets.test.tsx +31 -31
  73. package/tests/unit/MarkdownViewer/list-code.test.tsx +45 -45
  74. package/tests/unit/MarkdownViewer/preserve-newlines.test.tsx +109 -109
  75. package/tests/unit/MarkdownViewer/read-time.test.ts +13 -13
  76. package/tests/unit/MarkdownViewer/renderers.test.tsx +75 -75
  77. package/tests/unit/MarkdownViewer/runnable.test.tsx +51 -51
  78. package/tests/unit/MarkdownViewer/ssr.test.tsx +56 -55
  79. package/tests/unit/MarkdownViewer/syntax-highlighting.test.tsx +43 -42
  80. package/tests/unit/MarkdownViewer/tabs-fenced-code.test.tsx +120 -120
  81. package/tests/unit/MarkdownViewer/tabs.test.tsx +83 -82
  82. package/tests/unit/Stacker/drag.test.ts +21 -0
  83. package/tests/unit/Stacker/helpers.test.ts +105 -0
  84. package/tests/unit/Stacker/reducer.test.ts +107 -0
  85. package/tools/duplicate-vector-mappings.json +141 -141
  86. package/tools/glass-export-overrides.json +6 -6
  87. package/tools/icon-parser.ts +41 -40
  88. package/tools/integration-gradient-exports.json +55 -55
  89. package/tools/picto-compare.ts +32 -32
  90. package/tools/picto-merge-urls.ts +19 -19
  91. package/tools/picto-sync.ts +543 -543
  92. package/tools/picto-verify-gradients.ts +50 -50
  93. package/tools/vector-export-nodes.json +975 -975
@@ -4,33 +4,33 @@ import { getMarkdownEditorReady, markdownEditorReady } from "@src/lib/markdown/e
4
4
  import { afterEach, describe, expect, it, vi } from "vitest";
5
5
 
6
6
  describe("markdownEditorReady", () => {
7
- afterEach(() => {
8
- vi.useRealTimers();
9
- });
7
+ afterEach(() => {
8
+ vi.useRealTimers();
9
+ });
10
10
 
11
- it("sets data-markdown-ready after the first animation frame", async () => {
12
- vi.useFakeTimers({ toFake: ["requestAnimationFrame"] });
11
+ it("sets data-markdown-ready after the first animation frame", async () => {
12
+ vi.useFakeTimers({ toFake: ["requestAnimationFrame"] });
13
13
 
14
- const parent = document.createElement("div");
15
- document.body.appendChild(parent);
14
+ const parent = document.createElement("div");
15
+ document.body.appendChild(parent);
16
16
 
17
- const view = new EditorView({
18
- state: EditorState.create({
19
- doc: "# Title",
20
- extensions: [markdownEditorReady()],
21
- }),
22
- parent,
23
- });
17
+ const view = new EditorView({
18
+ state: EditorState.create({
19
+ doc: "# Title",
20
+ extensions: [markdownEditorReady()],
21
+ }),
22
+ parent,
23
+ });
24
24
 
25
- expect(getMarkdownEditorReady(view.state)).toBe(false);
26
- expect(view.dom.getAttribute("data-markdown-ready")).toBeNull();
25
+ expect(getMarkdownEditorReady(view.state)).toBe(false);
26
+ expect(view.dom.getAttribute("data-markdown-ready")).toBeNull();
27
27
 
28
- await vi.runAllTimersAsync();
28
+ await vi.runAllTimersAsync();
29
29
 
30
- expect(getMarkdownEditorReady(view.state)).toBe(true);
31
- expect(view.dom.getAttribute("data-markdown-ready")).toBe("true");
30
+ expect(getMarkdownEditorReady(view.state)).toBe(true);
31
+ expect(view.dom.getAttribute("data-markdown-ready")).toBe("true");
32
32
 
33
- view.destroy();
34
- parent.remove();
35
- });
33
+ view.destroy();
34
+ parent.remove();
35
+ });
36
36
  });
@@ -1,12 +1,12 @@
1
1
  import {
2
- collectHtmlBlockTokens,
3
- collectParagraphJsxDescriptors,
4
- findDescriptorForOpenBlock,
5
- isBlockHtmlNodeDescriptor,
6
- outermostHtmlDescriptors,
7
- pairDocumentHtmlBlocks,
8
- pairHtmlTokens,
9
- pairInlineHtmlTags,
2
+ collectHtmlBlockTokens,
3
+ collectParagraphJsxDescriptors,
4
+ findDescriptorForOpenBlock,
5
+ isBlockHtmlNodeDescriptor,
6
+ outermostHtmlDescriptors,
7
+ pairDocumentHtmlBlocks,
8
+ pairHtmlTokens,
9
+ pairInlineHtmlTags,
10
10
  } from "@src/lib/markdown/tree/html";
11
11
  import { parseMarkdownTree } from "@src/lib/markdown/tree/parse";
12
12
  import { markdownSourceFromString } from "@src/lib/markdown/tree/source";
@@ -18,77 +18,77 @@ const TABLE_MARKDOWN = `<table>
18
18
  </table>`;
19
19
 
20
20
  describe("HTML descriptors", () => {
21
- it("findDescriptorForOpenBlock picks the outermost table, not nested th/td", () => {
22
- const tree = parseMarkdownTree(TABLE_MARKDOWN);
23
- const node = tree.topNode.firstChild;
24
- expect(node?.name).toBe("HTMLBlock");
25
- if (!node) return;
21
+ it("findDescriptorForOpenBlock picks the outermost table, not nested th/td", () => {
22
+ const tree = parseMarkdownTree(TABLE_MARKDOWN);
23
+ const node = tree.topNode.firstChild;
24
+ expect(node?.name).toBe("HTMLBlock");
25
+ if (!node) return;
26
26
 
27
- const source = markdownSourceFromString(TABLE_MARKDOWN);
28
- const cross = pairDocumentHtmlBlocks([node], source);
29
- const picked = findDescriptorForOpenBlock(node, cross);
27
+ const source = markdownSourceFromString(TABLE_MARKDOWN);
28
+ const cross = pairDocumentHtmlBlocks([node], source);
29
+ const picked = findDescriptorForOpenBlock(node, cross);
30
30
 
31
- expect(picked?.tagName).toBe("table");
32
- expect(picked?.closeTo).not.toBeNull();
33
- });
31
+ expect(picked?.tagName).toBe("table");
32
+ expect(picked?.closeTo).not.toBeNull();
33
+ });
34
34
 
35
- it("outermostHtmlDescriptors keeps only the table for a single HTML block", () => {
36
- const tree = parseMarkdownTree(TABLE_MARKDOWN);
37
- const node = tree.topNode.firstChild;
38
- expect(node).toBeDefined();
39
- if (!node) return;
35
+ it("outermostHtmlDescriptors keeps only the table for a single HTML block", () => {
36
+ const tree = parseMarkdownTree(TABLE_MARKDOWN);
37
+ const node = tree.topNode.firstChild;
38
+ expect(node).toBeDefined();
39
+ if (!node) return;
40
40
 
41
- const source = markdownSourceFromString(TABLE_MARKDOWN);
42
- const tokens = collectHtmlBlockTokens(node, source);
43
- const outer = outermostHtmlDescriptors(pairHtmlTokens(tokens));
41
+ const source = markdownSourceFromString(TABLE_MARKDOWN);
42
+ const tokens = collectHtmlBlockTokens(node, source);
43
+ const outer = outermostHtmlDescriptors(pairHtmlTokens(tokens));
44
44
 
45
- expect(outer).toHaveLength(1);
46
- expect(outer[0]?.tagName).toBe("table");
47
- });
45
+ expect(outer).toHaveLength(1);
46
+ expect(outer[0]?.tagName).toBe("table");
47
+ });
48
48
 
49
- it("treats inline <Since /> as non-block inside a paragraph", () => {
50
- const text = 'Text <Since v="1" /> end.';
51
- const source = markdownSourceFromString(text);
52
- const tree = parseMarkdownTree(text);
53
- const para = tree.topNode.firstChild;
54
- expect(para?.name).toBe("Paragraph");
55
- if (!para) return;
49
+ it("treats inline <Since /> as non-block inside a paragraph", () => {
50
+ const text = 'Text <Since v="1" /> end.';
51
+ const source = markdownSourceFromString(text);
52
+ const tree = parseMarkdownTree(text);
53
+ const para = tree.topNode.firstChild;
54
+ expect(para?.name).toBe("Paragraph");
55
+ if (!para) return;
56
56
 
57
- const descriptors = pairInlineHtmlTags(para, source);
58
- const since = descriptors.find((d) => d.isJsx && d.rawTagName === "Since");
59
- expect(since).toBeDefined();
60
- // Unregistered components default to inline.
61
- expect(since && isBlockHtmlNodeDescriptor(since, undefined)).toBe(false);
62
- // Explicitly registering as block flips the resolved block-ness.
63
- expect(
64
- since &&
65
- isBlockHtmlNodeDescriptor(since, {
66
- Since: { component: () => null, block: true },
67
- }),
68
- ).toBe(true);
69
- });
57
+ const descriptors = pairInlineHtmlTags(para, source);
58
+ const since = descriptors.find((d) => d.isJsx && d.rawTagName === "Since");
59
+ expect(since).toBeDefined();
60
+ // Unregistered components default to inline.
61
+ expect(since && isBlockHtmlNodeDescriptor(since, undefined)).toBe(false);
62
+ // Explicitly registering as block flips the resolved block-ness.
63
+ expect(
64
+ since &&
65
+ isBlockHtmlNodeDescriptor(since, {
66
+ Since: { component: () => null, block: true },
67
+ }),
68
+ ).toBe(true);
69
+ });
70
70
 
71
- it("pairs multiple self-closing tags in one paragraph", () => {
72
- const text = '<Label label="Required" /><Label label="Optional" />';
73
- const source = markdownSourceFromString(text);
74
- const tree = parseMarkdownTree(text);
75
- const para = tree.topNode.firstChild;
76
- expect(para?.name).toBe("Paragraph");
77
- if (!para) return;
71
+ it("pairs multiple self-closing tags in one paragraph", () => {
72
+ const text = '<Label label="Required" /><Label label="Optional" />';
73
+ const source = markdownSourceFromString(text);
74
+ const tree = parseMarkdownTree(text);
75
+ const para = tree.topNode.firstChild;
76
+ expect(para?.name).toBe("Paragraph");
77
+ if (!para) return;
78
78
 
79
- expect(pairInlineHtmlTags(para, source)).toHaveLength(2);
80
- });
79
+ expect(pairInlineHtmlTags(para, source)).toHaveLength(2);
80
+ });
81
81
 
82
- it("collectParagraphJsxDescriptors scans plain paragraphs without HTMLTag nodes", () => {
83
- const text = 'Before <Card label="x" /> after.';
84
- const source = markdownSourceFromString(text);
85
- const tree = parseMarkdownTree(text);
86
- const para = tree.topNode.firstChild;
87
- expect(para?.name).toBe("Paragraph");
88
- if (!para) return;
82
+ it("collectParagraphJsxDescriptors scans plain paragraphs without HTMLTag nodes", () => {
83
+ const text = 'Before <Card label="x" /> after.';
84
+ const source = markdownSourceFromString(text);
85
+ const tree = parseMarkdownTree(text);
86
+ const para = tree.topNode.firstChild;
87
+ expect(para?.name).toBe("Paragraph");
88
+ if (!para) return;
89
89
 
90
- const descriptors = collectParagraphJsxDescriptors(para, source);
91
- expect(descriptors).toHaveLength(1);
92
- expect(descriptors[0]?.rawTagName).toBe("Card");
93
- });
90
+ const descriptors = collectParagraphJsxDescriptors(para, source);
91
+ expect(descriptors).toHaveLength(1);
92
+ expect(descriptors[0]?.rawTagName).toBe("Card");
93
+ });
94
94
  });
@@ -4,100 +4,100 @@ import { describe, expect, it } from "vitest";
4
4
 
5
5
  /** Reconstructed body text of the (single) FencedCode node. */
6
6
  function fencedCodeBody(text: string): string | null {
7
- const tree = parseMarkdownTree(text);
8
- const src = markdownSourceFromString(text);
9
- let body: string | null = null;
10
- tree.iterate({
11
- enter: (n) => {
12
- if (n.name === "FencedCode" && body === null) body = readCodeText(n.node, src);
13
- },
14
- });
15
- return body;
7
+ const tree = parseMarkdownTree(text);
8
+ const src = markdownSourceFromString(text);
9
+ let body: string | null = null;
10
+ tree.iterate({
11
+ enter: (n) => {
12
+ if (n.name === "FencedCode" && body === null) body = readCodeText(n.node, src);
13
+ },
14
+ });
15
+ return body;
16
16
  }
17
17
 
18
18
  /** Top-level node names of a parsed document. */
19
19
  function topLevelNodes(text: string): { name: string; text: string }[] {
20
- const tree = parseMarkdownTree(text);
21
- const src = markdownSourceFromString(text);
22
- const out: { name: string; text: string }[] = [];
23
- let ch = tree.topNode.firstChild;
24
- while (ch) {
25
- out.push({ name: ch.name, text: src.slice(ch.from, ch.to) });
26
- ch = ch.nextSibling;
27
- }
28
- return out;
20
+ const tree = parseMarkdownTree(text);
21
+ const src = markdownSourceFromString(text);
22
+ const out: { name: string; text: string }[] = [];
23
+ let ch = tree.topNode.firstChild;
24
+ while (ch) {
25
+ out.push({ name: ch.name, text: src.slice(ch.from, ch.to) });
26
+ ch = ch.nextSibling;
27
+ }
28
+ return out;
29
29
  }
30
30
 
31
31
  /** The source text of the (single) FencedCode node, or null if none. */
32
32
  function fencedCodeText(text: string): string | null {
33
- const tree = parseMarkdownTree(text);
34
- const src = markdownSourceFromString(text);
35
- let found: string | null = null;
36
- tree.iterate({
37
- enter: (n) => {
38
- if (n.name === "FencedCode" && found === null) found = src.slice(n.from, n.to);
39
- },
40
- });
41
- return found;
33
+ const tree = parseMarkdownTree(text);
34
+ const src = markdownSourceFromString(text);
35
+ let found: string | null = null;
36
+ tree.iterate({
37
+ enter: (n) => {
38
+ if (n.name === "FencedCode" && found === null) found = src.slice(n.from, n.to);
39
+ },
40
+ });
41
+ return found;
42
42
  }
43
43
 
44
44
  describe("MarkdownEditor / indented fenced code closing", () => {
45
- it("closes a tab-indented fence and does not swallow following content", () => {
46
- const text = "\t```ts\n\tcode\n\t```\ntext";
47
- const fence = fencedCodeText(text);
48
- expect(fence).not.toBeNull();
49
- expect(fence).not.toContain("text");
50
- // The trailing paragraph survives as its own node.
51
- expect(
52
- topLevelNodes(text).some((n) => n.name === "Paragraph" && n.text.includes("text")),
53
- ).toBe(true);
54
- });
45
+ it("closes a tab-indented fence and does not swallow following content", () => {
46
+ const text = "\t```ts\n\tcode\n\t```\ntext";
47
+ const fence = fencedCodeText(text);
48
+ expect(fence).not.toBeNull();
49
+ expect(fence).not.toContain("text");
50
+ // The trailing paragraph survives as its own node.
51
+ expect(
52
+ topLevelNodes(text).some((n) => n.name === "Paragraph" && n.text.includes("text")),
53
+ ).toBe(true);
54
+ });
55
55
 
56
- it("closes a 4-space-indented fence and does not swallow following content", () => {
57
- const text = " ```ts\n code\n ```\ntext";
58
- const fence = fencedCodeText(text);
59
- expect(fence).not.toBeNull();
60
- expect(fence).not.toContain("text");
61
- });
56
+ it("closes a 4-space-indented fence and does not swallow following content", () => {
57
+ const text = " ```ts\n code\n ```\ntext";
58
+ const fence = fencedCodeText(text);
59
+ expect(fence).not.toBeNull();
60
+ expect(fence).not.toContain("text");
61
+ });
62
62
 
63
- it("does not let an indented fence swallow the closing </Tabs> tag", () => {
64
- const text = [
65
- "<Tabs>",
66
- '\t<TabItem value="esm" label="ESM">',
67
- "\t```ts",
68
- "\tconst db = 1;",
69
- "\t```",
70
- "\t</TabItem>",
71
- "</Tabs>",
72
- ].join("\n");
73
- // </Tabs> and </TabItem> must be their own HTMLBlock nodes, not buried
74
- // inside a runaway FencedCode.
75
- const fence = fencedCodeText(text);
76
- expect(fence).not.toContain("</TabItem>");
77
- expect(fence).not.toContain("</Tabs>");
78
- const names = topLevelNodes(text).map((n) => n.name);
79
- expect(names.filter((n) => n === "HTMLBlock").length).toBeGreaterThanOrEqual(4);
80
- });
63
+ it("does not let an indented fence swallow the closing </Tabs> tag", () => {
64
+ const text = [
65
+ "<Tabs>",
66
+ '\t<TabItem value="esm" label="ESM">',
67
+ "\t```ts",
68
+ "\tconst db = 1;",
69
+ "\t```",
70
+ "\t</TabItem>",
71
+ "</Tabs>",
72
+ ].join("\n");
73
+ // </Tabs> and </TabItem> must be their own HTMLBlock nodes, not buried
74
+ // inside a runaway FencedCode.
75
+ const fence = fencedCodeText(text);
76
+ expect(fence).not.toContain("</TabItem>");
77
+ expect(fence).not.toContain("</Tabs>");
78
+ const names = topLevelNodes(text).map((n) => n.name);
79
+ expect(names.filter((n) => n === "HTMLBlock").length).toBeGreaterThanOrEqual(4);
80
+ });
81
81
 
82
- it("still parses a flush fence normally (regression)", () => {
83
- const text = "```ts\ncode\n```\ntext";
84
- expect(fencedCodeText(text)).toBe("```ts\ncode\n```");
85
- });
82
+ it("still parses a flush fence normally (regression)", () => {
83
+ const text = "```ts\ncode\n```\ntext";
84
+ expect(fencedCodeText(text)).toBe("```ts\ncode\n```");
85
+ });
86
86
 
87
- it("still parses a fence nested in a list item (regression)", () => {
88
- const text = ["- item", " ```js", " code;", " ```", "", "after"].join("\n");
89
- const fence = fencedCodeText(text);
90
- expect(fence).not.toBeNull();
91
- expect(fence).not.toContain("after");
92
- });
87
+ it("still parses a fence nested in a list item (regression)", () => {
88
+ const text = ["- item", " ```js", " code;", " ```", "", "after"].join("\n");
89
+ const fence = fencedCodeText(text);
90
+ expect(fence).not.toBeNull();
91
+ expect(fence).not.toContain("after");
92
+ });
93
93
 
94
- // Matches MDX/micromark: exactly the opening fence's indentation is removed
95
- // from each content line; deeper indentation is preserved.
96
- it("strips the opening fence indent from content, preserving deeper indent", () => {
97
- const text =
98
- "\t```ts\n\timport { Surreal } from 'surrealdb';\n\t\tconst indented = 1;\n\t```";
99
- expect(fencedCodeBody(text)).toBe(
100
- "import { Surreal } from 'surrealdb';\n\tconst indented = 1;",
101
- );
102
- });
94
+ // Matches MDX/micromark: exactly the opening fence's indentation is removed
95
+ // from each content line; deeper indentation is preserved.
96
+ it("strips the opening fence indent from content, preserving deeper indent", () => {
97
+ const text =
98
+ "\t```ts\n\timport { Surreal } from 'surrealdb';\n\t\tconst indented = 1;\n\t```";
99
+ expect(fencedCodeBody(text)).toBe(
100
+ "import { Surreal } from 'surrealdb';\n\tconst indented = 1;",
101
+ );
102
+ });
103
103
  });
@@ -1,115 +1,115 @@
1
1
  import {
2
- buildJsxTagHighlights,
3
- findJsonAttributeClose,
4
- parseAttributes,
5
- parseJsonAttribute,
6
- resolveAttributeValue,
2
+ buildJsxTagHighlights,
3
+ findJsonAttributeClose,
4
+ parseAttributes,
5
+ parseJsonAttribute,
6
+ resolveAttributeValue,
7
7
  } from "@src/lib/markdown/grammar/html-attrs";
8
8
  import { describe, expect, it } from "vitest";
9
9
 
10
10
  describe("findJsonAttributeClose", () => {
11
- it("finds the closing brace for nested JSON objects and strings", () => {
12
- const json = '{"version": "2.0.0", "items": [{"id": 1}]}';
13
- const full = `{${json}}`;
14
- const bounds = findJsonAttributeClose(`${full} trailing`, 0);
15
- expect(bounds).toEqual({
16
- contentFrom: 1,
17
- contentTo: full.length - 1,
18
- closeBrace: full.length - 1,
19
- });
20
- });
21
-
22
- it("handles braces inside JSON strings", () => {
23
- const json = '{"note": "}"}';
24
- const full = `{${json}}`;
25
- const bounds = findJsonAttributeClose(full, 0);
26
- expect(bounds?.closeBrace).toBe(full.length - 1);
27
- });
11
+ it("finds the closing brace for nested JSON objects and strings", () => {
12
+ const json = '{"version": "2.0.0", "items": [{"id": 1}]}';
13
+ const full = `{${json}}`;
14
+ const bounds = findJsonAttributeClose(`${full} trailing`, 0);
15
+ expect(bounds).toEqual({
16
+ contentFrom: 1,
17
+ contentTo: full.length - 1,
18
+ closeBrace: full.length - 1,
19
+ });
20
+ });
21
+
22
+ it("handles braces inside JSON strings", () => {
23
+ const json = '{"note": "}"}';
24
+ const full = `{${json}}`;
25
+ const bounds = findJsonAttributeClose(full, 0);
26
+ expect(bounds?.closeBrace).toBe(full.length - 1);
27
+ });
28
28
  });
29
29
 
30
30
  describe("parseJsonAttribute", () => {
31
- it("parses JSON literals without evaluation", () => {
32
- expect(parseJsonAttribute("42")).toBe(42);
33
- expect(parseJsonAttribute('"hello"')).toBe("hello");
34
- expect(parseJsonAttribute('{"enabled": true}')).toEqual({ enabled: true });
35
- });
36
-
37
- it("returns undefined for invalid JSON", () => {
38
- expect(parseJsonAttribute("{ foo: 1 }")).toBeUndefined();
39
- expect(parseJsonAttribute("count")).toBeUndefined();
40
- });
31
+ it("parses JSON literals without evaluation", () => {
32
+ expect(parseJsonAttribute("42")).toBe(42);
33
+ expect(parseJsonAttribute('"hello"')).toBe("hello");
34
+ expect(parseJsonAttribute('{"enabled": true}')).toEqual({ enabled: true });
35
+ });
36
+
37
+ it("returns undefined for invalid JSON", () => {
38
+ expect(parseJsonAttribute("{ foo: 1 }")).toBeUndefined();
39
+ expect(parseJsonAttribute("count")).toBeUndefined();
40
+ });
41
41
  });
42
42
 
43
43
  describe("resolveAttributeValue", () => {
44
- it("resolves JSON attribute values", () => {
45
- expect(resolveAttributeValue({ type: "json", value: "100" })).toBe(100);
46
- expect(resolveAttributeValue({ type: "json", value: '"2.0.0"' })).toBe("2.0.0");
47
- });
44
+ it("resolves JSON attribute values", () => {
45
+ expect(resolveAttributeValue({ type: "json", value: "100" })).toBe(100);
46
+ expect(resolveAttributeValue({ type: "json", value: '"2.0.0"' })).toBe("2.0.0");
47
+ });
48
48
  });
49
49
 
50
50
  describe("buildJsxTagHighlights", () => {
51
- const tag = '<Since v={"2.0.0"} t="true" b="false" />';
52
-
53
- it("covers the attribute section without gaps for the nested HTML parser", () => {
54
- const highlights = buildJsxTagHighlights(tag);
55
- const attrStart = tag.indexOf(" v=");
56
- const attrEnd = tag.lastIndexOf(">");
57
-
58
- const gaps = leftoverGaps(attrStart, attrEnd, highlights);
59
- expect(gaps).toEqual([]);
60
- });
61
-
62
- it("labels attribute names and quoted literals after a JSON value", () => {
63
- const highlights = buildJsxTagHighlights(tag);
64
- const names = textAtKinds(tag, highlights, "name");
65
- const strings = textAtKinds(tag, highlights, "string");
66
-
67
- expect(names).toEqual(["v", "t", "b"]);
68
- expect(strings).toEqual(['"true"', '"false"']);
69
- });
70
-
71
- it("matches parseAttributes JSON boundaries", () => {
72
- const { endIndex } = readTagNameForTest(tag);
73
- const attrString = tag.slice(endIndex, tag.lastIndexOf(">"));
74
- const attrs = parseAttributes(attrString);
75
- const expr = buildJsxTagHighlights(tag).find((h) => h.kind === "expression-content");
76
-
77
- expect(attrs.v).toEqual({ type: "json", value: '"2.0.0"' });
78
- expect(expr).toBeDefined();
79
- if (!expr) return;
80
- expect(tag.slice(expr.from, expr.to)).toBe('"2.0.0"');
81
- expect(attrs.t).toBe("true");
82
- expect(attrs.b).toBe("false");
83
- });
51
+ const tag = '<Since v={"2.0.0"} t="true" b="false" />';
52
+
53
+ it("covers the attribute section without gaps for the nested HTML parser", () => {
54
+ const highlights = buildJsxTagHighlights(tag);
55
+ const attrStart = tag.indexOf(" v=");
56
+ const attrEnd = tag.lastIndexOf(">");
57
+
58
+ const gaps = leftoverGaps(attrStart, attrEnd, highlights);
59
+ expect(gaps).toEqual([]);
60
+ });
61
+
62
+ it("labels attribute names and quoted literals after a JSON value", () => {
63
+ const highlights = buildJsxTagHighlights(tag);
64
+ const names = textAtKinds(tag, highlights, "name");
65
+ const strings = textAtKinds(tag, highlights, "string");
66
+
67
+ expect(names).toEqual(["v", "t", "b"]);
68
+ expect(strings).toEqual(['"true"', '"false"']);
69
+ });
70
+
71
+ it("matches parseAttributes JSON boundaries", () => {
72
+ const { endIndex } = readTagNameForTest(tag);
73
+ const attrString = tag.slice(endIndex, tag.lastIndexOf(">"));
74
+ const attrs = parseAttributes(attrString);
75
+ const expr = buildJsxTagHighlights(tag).find((h) => h.kind === "expression-content");
76
+
77
+ expect(attrs.v).toEqual({ type: "json", value: '"2.0.0"' });
78
+ expect(expr).toBeDefined();
79
+ if (!expr) return;
80
+ expect(tag.slice(expr.from, expr.to)).toBe('"2.0.0"');
81
+ expect(attrs.t).toBe("true");
82
+ expect(attrs.b).toBe("false");
83
+ });
84
84
  });
85
85
 
86
86
  function readTagNameForTest(text: string): { endIndex: number } {
87
- let i = 1;
88
- while (i < text.length && !/[\s/>]/.test(text[i] ?? "")) i++;
89
- return { endIndex: i };
87
+ let i = 1;
88
+ while (i < text.length && !/[\s/>]/.test(text[i] ?? "")) i++;
89
+ return { endIndex: i };
90
90
  }
91
91
 
92
92
  function leftoverGaps(
93
- from: number,
94
- to: number,
95
- spans: { from: number; to: number }[],
93
+ from: number,
94
+ to: number,
95
+ spans: { from: number; to: number }[],
96
96
  ): { from: number; to: number }[] {
97
- const gaps: { from: number; to: number }[] = [];
98
- let pos = from;
97
+ const gaps: { from: number; to: number }[] = [];
98
+ let pos = from;
99
99
 
100
- for (const span of spans) {
101
- if (span.from > pos) gaps.push({ from: pos, to: span.from });
102
- pos = Math.max(pos, span.to);
103
- }
100
+ for (const span of spans) {
101
+ if (span.from > pos) gaps.push({ from: pos, to: span.from });
102
+ pos = Math.max(pos, span.to);
103
+ }
104
104
 
105
- if (pos < to) gaps.push({ from: pos, to });
106
- return gaps;
105
+ if (pos < to) gaps.push({ from: pos, to });
106
+ return gaps;
107
107
  }
108
108
 
109
109
  function textAtKinds(
110
- text: string,
111
- highlights: ReturnType<typeof buildJsxTagHighlights>,
112
- kind: "name" | "string",
110
+ text: string,
111
+ highlights: ReturnType<typeof buildJsxTagHighlights>,
112
+ kind: "name" | "string",
113
113
  ): string[] {
114
- return highlights.filter((h) => h.kind === kind).map((h) => text.slice(h.from, h.to));
114
+ return highlights.filter((h) => h.kind === kind).map((h) => text.slice(h.from, h.to));
115
115
  }