@surrealdb/ui 1.2.12 → 1.2.14

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 (99) 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 +25 -19
  6. package/CLAUDE.md +111 -0
  7. package/README.md +15 -10
  8. package/dist/icons.d.ts +4 -0
  9. package/dist/icons.js +91 -89
  10. package/dist/icons.js.map +1 -1
  11. package/dist/ui.css +1 -1
  12. package/dist/ui.d.ts +259 -1
  13. package/dist/ui.js +9807 -12265
  14. package/dist/ui.js.map +1 -1
  15. package/doc/code-style.md +264 -0
  16. package/doc/release.md +102 -0
  17. package/doc/stacker-plan.md +219 -0
  18. package/package.json +120 -116
  19. package/release-notes.md +0 -0
  20. package/res/_mixins.scss +15 -11
  21. package/tests/_setup/e2e-helpers.tsx +85 -85
  22. package/tests/_setup/portable-stories.ts +11 -10
  23. package/tests/e2e/MarkdownEditor/content-blocks.test.tsx +140 -139
  24. package/tests/e2e/MarkdownEditor/edits.test.tsx +101 -100
  25. package/tests/e2e/MarkdownEditor/heading-fold.test.tsx +35 -34
  26. package/tests/e2e/MarkdownEditor/hybrid-widgets.test.tsx +178 -177
  27. package/tests/e2e/MarkdownEditor/image-on-image.test.tsx +50 -49
  28. package/tests/e2e/MarkdownEditor/jsx-block-content.test.tsx +195 -194
  29. package/tests/e2e/MarkdownEditor/jsx-highlight.test.tsx +47 -46
  30. package/tests/e2e/MarkdownEditor/jsx-inline-badges.test.tsx +35 -34
  31. package/tests/e2e/MarkdownEditor/jsx-inline-click.test.tsx +39 -38
  32. package/tests/e2e/MarkdownEditor/jsx-selection.test.tsx +28 -27
  33. package/tests/e2e/MarkdownEditor/link-placeholder.test.tsx +49 -48
  34. package/tests/e2e/MarkdownEditor/media-align.test.tsx +40 -39
  35. package/tests/e2e/MarkdownEditor/media-edit.test.tsx +49 -48
  36. package/tests/e2e/MarkdownEditor/media-sizing.test.tsx +108 -107
  37. package/tests/e2e/MarkdownEditor/modes.test.tsx +79 -78
  38. package/tests/e2e/MarkdownEditor/onimage-parity.test.tsx +29 -28
  39. package/tests/e2e/MarkdownEditor/regressions.test.tsx +194 -193
  40. package/tests/e2e/MarkdownEditor/slash-commands.test.tsx +81 -80
  41. package/tests/e2e/MarkdownEditor/table-click.test.tsx +36 -35
  42. package/tests/e2e/MarkdownEditor/table-controls.test.tsx +44 -43
  43. package/tests/e2e/MarkdownEditor/table-format.test.tsx +28 -27
  44. package/tests/e2e/MarkdownEditor/undo-redo.test.tsx +22 -21
  45. package/tests/e2e/MarkdownViewer/parity.test.tsx +144 -143
  46. package/tests/e2e/MarkdownViewer/render.test.tsx +20 -19
  47. package/tests/e2e/Stacker/stacker.test.tsx +304 -0
  48. package/tests/unit/Editor/helpers.test.ts +26 -26
  49. package/tests/unit/MarkdownConformance/error-isolation.test.tsx +26 -25
  50. package/tests/unit/MarkdownConformance/indentation.test.tsx +20 -19
  51. package/tests/unit/MarkdownConformance/inline-code-fuzz.test.tsx +255 -0
  52. package/tests/unit/MarkdownConformance/jsx-attributes.test.tsx +39 -38
  53. package/tests/unit/MarkdownConformance/jsx-fuzz.test.tsx +122 -122
  54. package/tests/unit/MarkdownConformance/links.test.tsx +78 -54
  55. package/tests/unit/MarkdownConformance/render-helpers.tsx +60 -60
  56. package/tests/unit/MarkdownEditor/code-info.test.ts +58 -58
  57. package/tests/unit/MarkdownEditor/decorations.test.ts +423 -421
  58. package/tests/unit/MarkdownEditor/editor-ready.test.ts +22 -22
  59. package/tests/unit/MarkdownEditor/html-descriptors.test.ts +70 -70
  60. package/tests/unit/MarkdownEditor/indented-fence.test.ts +80 -80
  61. package/tests/unit/MarkdownEditor/jsx-attr-scan.test.ts +87 -87
  62. package/tests/unit/MarkdownEditor/jsx-block-widget.test.ts +51 -51
  63. package/tests/unit/MarkdownEditor/jsx-tag-grammar.test.ts +63 -63
  64. package/tests/unit/MarkdownEditor/list-indent.test.ts +67 -67
  65. package/tests/unit/MarkdownEditor/slash-commands.test.ts +150 -150
  66. package/tests/unit/MarkdownEditor/table-format.test.ts +67 -67
  67. package/tests/unit/MarkdownEditor/table.test.ts +96 -96
  68. package/tests/unit/MarkdownEditor/triggers.test.ts +167 -167
  69. package/tests/unit/MarkdownEditor/widget-store.test.ts +90 -90
  70. package/tests/unit/MarkdownViewer/callout.test.tsx +42 -42
  71. package/tests/unit/MarkdownViewer/code-highlight.test.tsx +41 -41
  72. package/tests/unit/MarkdownViewer/code-title.test.tsx +45 -45
  73. package/tests/unit/MarkdownViewer/features.test.tsx +129 -129
  74. package/tests/unit/MarkdownViewer/headings.test.tsx +28 -28
  75. package/tests/unit/MarkdownViewer/indented-jsx-children.test.tsx +53 -53
  76. package/tests/unit/MarkdownViewer/jsx-block-nesting.test.tsx +137 -137
  77. package/tests/unit/MarkdownViewer/jsx.test.tsx +188 -188
  78. package/tests/unit/MarkdownViewer/list-bullets.test.tsx +31 -31
  79. package/tests/unit/MarkdownViewer/list-code.test.tsx +45 -45
  80. package/tests/unit/MarkdownViewer/preserve-newlines.test.tsx +109 -109
  81. package/tests/unit/MarkdownViewer/read-time.test.ts +13 -13
  82. package/tests/unit/MarkdownViewer/renderers.test.tsx +75 -75
  83. package/tests/unit/MarkdownViewer/runnable.test.tsx +51 -51
  84. package/tests/unit/MarkdownViewer/ssr.test.tsx +56 -55
  85. package/tests/unit/MarkdownViewer/syntax-highlighting.test.tsx +43 -42
  86. package/tests/unit/MarkdownViewer/tabs-fenced-code.test.tsx +120 -120
  87. package/tests/unit/MarkdownViewer/tabs.test.tsx +83 -82
  88. package/tests/unit/Stacker/drag.test.ts +21 -0
  89. package/tests/unit/Stacker/helpers.test.ts +105 -0
  90. package/tests/unit/Stacker/reducer.test.ts +107 -0
  91. package/tools/duplicate-vector-mappings.json +141 -141
  92. package/tools/glass-export-overrides.json +6 -6
  93. package/tools/icon-parser.ts +41 -40
  94. package/tools/integration-gradient-exports.json +55 -55
  95. package/tools/picto-compare.ts +32 -32
  96. package/tools/picto-merge-urls.ts +19 -19
  97. package/tools/picto-sync.ts +543 -543
  98. package/tools/picto-verify-gradients.ts +50 -50
  99. 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
  }