@surrealdb/ui 1.2.11 → 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 (101) 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/assets/245b1ccaa4927818.svg +3 -0
  8. package/dist/assets/333e1fe6e67ca4a2.svg +10 -0
  9. package/dist/assets/4b0e10c655814886.svg +21 -0
  10. package/dist/assets/9932da008ea5dd32.svg +3 -0
  11. package/dist/assets/a372168d5789daa2.svg +4 -0
  12. package/dist/assets/b140e44ee4ae7e35.svg +5 -0
  13. package/dist/assets/e028075817d928e4.svg +3 -0
  14. package/dist/assets/ee1065ad5a8f7f14.svg +6 -0
  15. package/dist/ui.css +1 -1
  16. package/dist/ui.d.ts +579 -1
  17. package/dist/ui.js +11553 -8988
  18. package/dist/ui.js.map +1 -1
  19. package/doc/code-style.md +264 -0
  20. package/doc/stacker-plan.md +219 -0
  21. package/package.json +119 -115
  22. package/res/_mixins.scss +15 -11
  23. package/tests/_setup/e2e-helpers.tsx +85 -85
  24. package/tests/_setup/portable-stories.ts +11 -10
  25. package/tests/e2e/MarkdownEditor/content-blocks.test.tsx +140 -139
  26. package/tests/e2e/MarkdownEditor/edits.test.tsx +101 -100
  27. package/tests/e2e/MarkdownEditor/heading-fold.test.tsx +35 -34
  28. package/tests/e2e/MarkdownEditor/hybrid-widgets.test.tsx +178 -177
  29. package/tests/e2e/MarkdownEditor/image-on-image.test.tsx +50 -49
  30. package/tests/e2e/MarkdownEditor/jsx-block-content.test.tsx +195 -194
  31. package/tests/e2e/MarkdownEditor/jsx-highlight.test.tsx +47 -46
  32. package/tests/e2e/MarkdownEditor/jsx-inline-badges.test.tsx +35 -34
  33. package/tests/e2e/MarkdownEditor/jsx-inline-click.test.tsx +39 -38
  34. package/tests/e2e/MarkdownEditor/jsx-selection.test.tsx +28 -27
  35. package/tests/e2e/MarkdownEditor/link-placeholder.test.tsx +49 -48
  36. package/tests/e2e/MarkdownEditor/media-align.test.tsx +40 -39
  37. package/tests/e2e/MarkdownEditor/media-edit.test.tsx +49 -48
  38. package/tests/e2e/MarkdownEditor/media-sizing.test.tsx +108 -107
  39. package/tests/e2e/MarkdownEditor/modes.test.tsx +79 -78
  40. package/tests/e2e/MarkdownEditor/onimage-parity.test.tsx +29 -28
  41. package/tests/e2e/MarkdownEditor/regressions.test.tsx +194 -193
  42. package/tests/e2e/MarkdownEditor/slash-commands.test.tsx +81 -80
  43. package/tests/e2e/MarkdownEditor/table-click.test.tsx +36 -35
  44. package/tests/e2e/MarkdownEditor/table-controls.test.tsx +44 -43
  45. package/tests/e2e/MarkdownEditor/table-format.test.tsx +28 -27
  46. package/tests/e2e/MarkdownEditor/undo-redo.test.tsx +22 -21
  47. package/tests/e2e/MarkdownViewer/parity.test.tsx +144 -143
  48. package/tests/e2e/MarkdownViewer/render.test.tsx +20 -19
  49. package/tests/e2e/Stacker/stacker.test.tsx +304 -0
  50. package/tests/unit/Editor/helpers.test.ts +26 -26
  51. package/tests/unit/MarkdownConformance/error-isolation.test.tsx +26 -25
  52. package/tests/unit/MarkdownConformance/indentation.test.tsx +20 -19
  53. package/tests/unit/MarkdownConformance/inline-code-fuzz.test.tsx +255 -0
  54. package/tests/unit/MarkdownConformance/jsx-attributes.test.tsx +39 -38
  55. package/tests/unit/MarkdownConformance/jsx-fuzz.test.tsx +122 -122
  56. package/tests/unit/MarkdownConformance/links.test.tsx +78 -54
  57. package/tests/unit/MarkdownConformance/render-helpers.tsx +60 -60
  58. package/tests/unit/MarkdownEditor/code-info.test.ts +58 -58
  59. package/tests/unit/MarkdownEditor/decorations.test.ts +423 -421
  60. package/tests/unit/MarkdownEditor/editor-ready.test.ts +22 -22
  61. package/tests/unit/MarkdownEditor/html-descriptors.test.ts +70 -70
  62. package/tests/unit/MarkdownEditor/indented-fence.test.ts +80 -80
  63. package/tests/unit/MarkdownEditor/jsx-attr-scan.test.ts +87 -87
  64. package/tests/unit/MarkdownEditor/jsx-block-widget.test.ts +51 -51
  65. package/tests/unit/MarkdownEditor/jsx-tag-grammar.test.ts +63 -63
  66. package/tests/unit/MarkdownEditor/list-indent.test.ts +67 -67
  67. package/tests/unit/MarkdownEditor/slash-commands.test.ts +150 -150
  68. package/tests/unit/MarkdownEditor/table-format.test.ts +67 -67
  69. package/tests/unit/MarkdownEditor/table.test.ts +96 -96
  70. package/tests/unit/MarkdownEditor/triggers.test.ts +167 -167
  71. package/tests/unit/MarkdownEditor/widget-store.test.ts +90 -90
  72. package/tests/unit/MarkdownViewer/callout.test.tsx +42 -42
  73. package/tests/unit/MarkdownViewer/code-highlight.test.tsx +41 -41
  74. package/tests/unit/MarkdownViewer/code-title.test.tsx +45 -45
  75. package/tests/unit/MarkdownViewer/features.test.tsx +129 -129
  76. package/tests/unit/MarkdownViewer/headings.test.tsx +28 -28
  77. package/tests/unit/MarkdownViewer/indented-jsx-children.test.tsx +53 -53
  78. package/tests/unit/MarkdownViewer/jsx-block-nesting.test.tsx +137 -137
  79. package/tests/unit/MarkdownViewer/jsx.test.tsx +188 -188
  80. package/tests/unit/MarkdownViewer/list-bullets.test.tsx +31 -31
  81. package/tests/unit/MarkdownViewer/list-code.test.tsx +45 -45
  82. package/tests/unit/MarkdownViewer/preserve-newlines.test.tsx +109 -109
  83. package/tests/unit/MarkdownViewer/read-time.test.ts +13 -13
  84. package/tests/unit/MarkdownViewer/renderers.test.tsx +75 -75
  85. package/tests/unit/MarkdownViewer/runnable.test.tsx +51 -51
  86. package/tests/unit/MarkdownViewer/ssr.test.tsx +56 -55
  87. package/tests/unit/MarkdownViewer/syntax-highlighting.test.tsx +43 -42
  88. package/tests/unit/MarkdownViewer/tabs-fenced-code.test.tsx +120 -120
  89. package/tests/unit/MarkdownViewer/tabs.test.tsx +83 -82
  90. package/tests/unit/Stacker/drag.test.ts +21 -0
  91. package/tests/unit/Stacker/helpers.test.ts +105 -0
  92. package/tests/unit/Stacker/reducer.test.ts +107 -0
  93. package/tools/duplicate-vector-mappings.json +141 -141
  94. package/tools/glass-export-overrides.json +6 -6
  95. package/tools/icon-parser.ts +41 -40
  96. package/tools/integration-gradient-exports.json +55 -55
  97. package/tools/picto-compare.ts +32 -32
  98. package/tools/picto-merge-urls.ts +19 -19
  99. package/tools/picto-sync.ts +543 -543
  100. package/tools/picto-verify-gradients.ts +50 -50
  101. package/tools/vector-export-nodes.json +975 -975
@@ -0,0 +1,255 @@
1
+ import { EditorState } from "@codemirror/state";
2
+ import { EditorView } from "@codemirror/view";
3
+ import { MantineProvider, v8CssVariablesResolver } from "@mantine/core";
4
+ import { markdownModeField } from "@src/lib/markdown";
5
+ import { mergeMarkdownComponents } from "@src/lib/markdown/components/defaults";
6
+ import { markdownDecorations } from "@src/lib/markdown/editor/decorations";
7
+ import { setMarkdownEditorFocused } from "@src/lib/markdown/editor/decorations/editor-focus";
8
+ import { setMarkdownMode } from "@src/lib/markdown/editor/mode";
9
+ import { createWidgetStore } from "@src/lib/markdown/editor/widgets/store";
10
+ import { Callout } from "@src/lib/markdown/grammar/callout";
11
+ import { createMarkdownLanguage } from "@src/lib/markdown/grammar/language";
12
+ import { MarkdownViewer } from "@src/primitives/MarkdownViewer";
13
+ import { MANTINE_THEME } from "@src/theme/mantine";
14
+ import { renderToString } from "react-dom/server";
15
+ import { beforeAll, describe, expect, it } from "vitest";
16
+
17
+ beforeAll(() => {
18
+ if (typeof window !== "undefined" && typeof window.matchMedia !== "function") {
19
+ Object.defineProperty(window, "matchMedia", {
20
+ writable: true,
21
+ value: (query: string) => ({
22
+ matches: false,
23
+ media: query,
24
+ onchange: null,
25
+ addListener: () => {},
26
+ removeListener: () => {},
27
+ addEventListener: () => {},
28
+ removeEventListener: () => {},
29
+ dispatchEvent: () => false,
30
+ }),
31
+ });
32
+ }
33
+ });
34
+
35
+ /** Deterministic LCG so failures reproduce (no Math.random). */
36
+ function makeRng(seed: number): () => number {
37
+ let s = seed >>> 0;
38
+ return () => {
39
+ s = (Math.imul(s, 1664525) + 1013904223) >>> 0;
40
+ return s / 0x100000000;
41
+ };
42
+ }
43
+
44
+ const COMPONENTS = mergeMarkdownComponents({});
45
+
46
+ function renderViewerHtml(doc: string): string {
47
+ return renderToString(
48
+ <MantineProvider
49
+ theme={MANTINE_THEME}
50
+ cssVariablesResolver={v8CssVariablesResolver}
51
+ forceColorScheme="dark"
52
+ >
53
+ <MarkdownViewer
54
+ content={doc}
55
+ components={COMPONENTS}
56
+ />
57
+ </MantineProvider>,
58
+ );
59
+ }
60
+
61
+ /** Build an editor over the doc and sweep the caret so decoration paths run. */
62
+ function buildEditor(doc: string): void {
63
+ const state = EditorState.create({
64
+ doc,
65
+ extensions: [
66
+ createMarkdownLanguage({ extensions: [Callout] }),
67
+ markdownModeField,
68
+ markdownDecorations({ widgetStore: createWidgetStore() }),
69
+ ],
70
+ });
71
+ const parent = document.createElement("div");
72
+ document.body.appendChild(parent);
73
+ const view = new EditorView({ state, parent });
74
+ view.dispatch({ effects: setMarkdownMode.of("preview") });
75
+ view.dispatch({ effects: setMarkdownEditorFocused.of(true) });
76
+ for (let pos = 0; pos <= doc.length; pos += Math.max(1, Math.floor(doc.length / 6))) {
77
+ view.dispatch({ selection: { anchor: pos } });
78
+ }
79
+ view.destroy();
80
+ parent.remove();
81
+ }
82
+
83
+ function decodeEntities(text: string): string {
84
+ return text
85
+ .replace(/&lt;/g, "<")
86
+ .replace(/&gt;/g, ">")
87
+ .replace(/&quot;/g, '"')
88
+ .replace(/&#x27;/g, "'")
89
+ .replace(/&#39;/g, "'")
90
+ .replace(/&amp;/g, "&");
91
+ }
92
+
93
+ /** Decoded text content of every inline `<code>` element in the HTML. */
94
+ function codeContents(html: string): string[] {
95
+ const out: string[] = [];
96
+ const re = /<code\b[^>]*>([\s\S]*?)<\/code>/g;
97
+ let m: RegExpExecArray | null;
98
+ // biome-ignore lint/suspicious/noAssignInExpressions: standard regex iteration
99
+ while ((m = re.exec(html)) !== null) {
100
+ out.push(decodeEntities(m[1] ?? ""));
101
+ }
102
+ return out;
103
+ }
104
+
105
+ /** Visible text: strip styles, all tags, then decode entities. */
106
+ function visibleText(html: string): string {
107
+ return decodeEntities(
108
+ html.replace(/<style[^>]*>[\s\S]*?<\/style>/g, "").replace(/<[^>]+>/g, ""),
109
+ );
110
+ }
111
+
112
+ // Inner contents for inline code spans — all rich in the characters that
113
+ // previously derailed the paragraph scanner (`<`, `>`, `{`, `}`, `=`).
114
+ const CODE_INNERS = [
115
+ "<target>",
116
+ "<T>",
117
+ "<a>",
118
+ "<div>",
119
+ "<Tabs>",
120
+ "</Tabs>",
121
+ "<Tabs><TabItem/></Tabs>",
122
+ ".{..+collect}",
123
+ ".{..+shortest=<target>}",
124
+ "a < b > c",
125
+ "x <= y >= z",
126
+ "map<string, int>",
127
+ "Array<Record<string, T>>",
128
+ "<not closed",
129
+ "unclosed>",
130
+ "<<multi>>",
131
+ "{...spread}",
132
+ '<a href="x">',
133
+ "<!-- comment -->",
134
+ "generic<Foo<Bar>>",
135
+ "i++ < len && j-- > 0",
136
+ ];
137
+
138
+ // Plain words with no markdown-significant characters.
139
+ const WORDS = ["returns", "the", "unique", "node", "set", "path", "and", "every", "shortest"];
140
+
141
+ // Real inline HTML tags that should still pair when they sit beside code.
142
+ const REAL_TAGS = ["<strong>bold</strong>", "<em>italic</em>", "text <b>x</b> more"];
143
+
144
+ const BLOCK_PREFIXES = ["", "", "", "> ", "# ", "- ", "1. "];
145
+
146
+ function buildParagraph(rng: () => number): { doc: string; expectedCodes: string[] } {
147
+ const tokenCount = 1 + Math.floor(rng() * 6);
148
+ const parts: string[] = [];
149
+ const expectedCodes: string[] = [];
150
+
151
+ for (let i = 0; i < tokenCount; i++) {
152
+ const roll = rng();
153
+ if (roll < 0.55) {
154
+ const inner = CODE_INNERS[Math.floor(rng() * CODE_INNERS.length)] ?? "";
155
+ // Occasionally use double-backtick fencing.
156
+ if (rng() < 0.25) {
157
+ parts.push(`\`\` ${inner} \`\``);
158
+ expectedCodes.push(inner);
159
+ } else {
160
+ parts.push(`\`${inner}\``);
161
+ expectedCodes.push(inner);
162
+ }
163
+ } else if (roll < 0.85) {
164
+ parts.push(WORDS[Math.floor(rng() * WORDS.length)] ?? "");
165
+ } else {
166
+ parts.push(REAL_TAGS[Math.floor(rng() * REAL_TAGS.length)] ?? "");
167
+ }
168
+ }
169
+
170
+ const prefix = BLOCK_PREFIXES[Math.floor(rng() * BLOCK_PREFIXES.length)] ?? "";
171
+ return { doc: prefix + parts.join(" "), expectedCodes };
172
+ }
173
+
174
+ function buildDoc(rng: () => number): { doc: string; expectedCodes: string[] } {
175
+ // Optionally prepend a fenced code block — the reported bug appeared right
176
+ // after one, so keep exercising that adjacency.
177
+ const paras: string[] = [];
178
+ const expectedCodes: string[] = [];
179
+ if (rng() < 0.4) {
180
+ paras.push("```surql\nSELECT * FROM person WHERE age > 18;\n```");
181
+ }
182
+ const count = 1 + Math.floor(rng() * 2);
183
+ for (let i = 0; i < count; i++) {
184
+ const { doc, expectedCodes: codes } = buildParagraph(rng);
185
+ paras.push(doc);
186
+ expectedCodes.push(...codes);
187
+ }
188
+ return { doc: paras.join("\n\n"), expectedCodes };
189
+ }
190
+
191
+ describe("MarkdownConformance / inline-code-with-angle-brackets fuzz", () => {
192
+ it("never leaks backticks and preserves every inline code span", () => {
193
+ const rng = makeRng(0x5eed42);
194
+ for (let i = 0; i < 400; i++) {
195
+ const { doc, expectedCodes } = buildDoc(rng);
196
+ let html = "";
197
+ try {
198
+ html = renderViewerHtml(doc);
199
+ buildEditor(doc);
200
+ } catch (err) {
201
+ throw new Error(`Fuzz #${i} threw for:\n${JSON.stringify(doc)}\n\n${String(err)}`);
202
+ }
203
+
204
+ const visible = visibleText(html);
205
+ // The backtick delimiters must never survive into visible output.
206
+ expect(
207
+ visible.includes("`"),
208
+ `Fuzz #${i} leaked a backtick.\nDOC: ${JSON.stringify(doc)}\nVISIBLE: ${JSON.stringify(visible)}`,
209
+ ).toBe(false);
210
+
211
+ // Every inline code's content must appear inside a <code> element.
212
+ const rendered = codeContents(html);
213
+ for (const expected of expectedCodes) {
214
+ const found = rendered.some((c) => c.includes(expected));
215
+ expect(
216
+ found,
217
+ `Fuzz #${i} lost inline code ${JSON.stringify(expected)}.\nDOC: ${JSON.stringify(doc)}\nCODES: ${JSON.stringify(rendered)}`,
218
+ ).toBe(true);
219
+ }
220
+ }
221
+ }, 60_000);
222
+
223
+ it("handles hand-picked adversarial inline-code snippets", () => {
224
+ const cases: Array<{ md: string; code: string }> = [
225
+ { md: "`<target>`", code: "<target>" },
226
+ { md: "`.{..+shortest=<target>}`", code: ".{..+shortest=<target>}" },
227
+ { md: "before `<div>` after", code: "<div>" },
228
+ { md: "`</Tabs>` orphan close", code: "</Tabs>" },
229
+ { md: "`Array<Record<string, T>>`", code: "Array<Record<string, T>>" },
230
+ { md: "a `x < y` and `p > q` end", code: "x < y" },
231
+ { md: "*em with `<code>` inside*", code: "<code>" },
232
+ { md: "[link `<x>` text](https://s.example)", code: "<x>" },
233
+ { md: "> quote `<target>` here", code: "<target>" },
234
+ { md: "# heading `<T>` end", code: "<T>" },
235
+ { md: "- item `<li>` text", code: "<li>" },
236
+ { md: '`<a href="x">` unbalanced open', code: '<a href="x">' },
237
+ { md: "`<<nested>>` chevrons", code: "<<nested>>" },
238
+ { md: "| head | `<td>` |\n| - | - |\n| a | `<b>` |", code: "<td>" },
239
+ ];
240
+
241
+ for (const { md, code } of cases) {
242
+ const html = renderViewerHtml(md);
243
+ const visible = visibleText(html);
244
+ expect(visible.includes("`"), `backtick leaked for ${JSON.stringify(md)}`).toBe(false);
245
+ const found = codeContents(html).some((c) => c.includes(code));
246
+ expect(found, `lost ${JSON.stringify(code)} for ${JSON.stringify(md)}`).toBe(true);
247
+ }
248
+ });
249
+
250
+ it("still renders a real inline tag adjacent to bracketed inline code", () => {
251
+ const html = renderViewerHtml("`<target>` then <strong>go</strong> now");
252
+ expect(html).toContain("<strong>go</strong>");
253
+ expect(codeContents(html).some((c) => c.includes("<target>"))).toBe(true);
254
+ });
255
+ });
@@ -1,56 +1,57 @@
1
1
  import type { MarkdownComponents } from "@src/lib/markdown";
2
2
  import { afterEach, describe, expect, it, vi } from "vitest";
3
+
3
4
  import { renderMarkdown, visibleText } from "./render-helpers";
4
5
 
5
6
  /** Echo component that serialises the props it receives for assertion. */
6
7
  function Echo(props: Record<string, unknown>): string {
7
- const { children, ...rest } = props;
8
- return `props=${JSON.stringify(rest)}`;
8
+ const { children: _, ...rest } = props;
9
+ return `props=${JSON.stringify(rest)}`;
9
10
  }
10
11
 
11
12
  const components: MarkdownComponents = { Echo };
12
13
 
13
14
  afterEach(() => {
14
- vi.restoreAllMocks();
15
+ vi.restoreAllMocks();
15
16
  });
16
17
 
17
18
  describe("Conformance / JSX attribute JSON", () => {
18
- it("parses a JSON object value", () => {
19
- const html = renderMarkdown('<Echo value={{ "a": 1, "b": [2, 3] }} />', { components });
20
- expect(visibleText(html)).toContain('"value":{"a":1,"b":[2,3]}');
21
- });
22
-
23
- it("parses JSON number, boolean, and array values", () => {
24
- const html = renderMarkdown("<Echo n={42} flag={true} list={[1,2]} />", { components });
25
- const text = visibleText(html);
26
- expect(text).toContain('"n":42');
27
- expect(text).toContain('"flag":true');
28
- expect(text).toContain('"list":[1,2]');
29
- });
30
-
31
- it("parses a quoted string value", () => {
32
- const html = renderMarkdown('<Echo label="hello" />', { components });
33
- expect(visibleText(html)).toContain('"label":"hello"');
34
- });
35
-
36
- it("treats a bare attribute as a boolean flag", () => {
37
- const html = renderMarkdown("<Echo disabled />", { components });
38
- // Bare booleans resolve to `undefined` props (not serialised) by design.
39
- expect(visibleText(html)).toContain("props=");
40
- });
41
-
42
- it("drops an invalid-JSON value and warns in development", () => {
43
- const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
44
- const html = renderMarkdown("<Echo value={{ a: 1 }} />", { components });
45
- // Single-unquoted-key object is not valid JSON → the prop is dropped.
46
- expect(visibleText(html)).not.toContain('"value"');
47
- expect(warn).toHaveBeenCalled();
48
- });
19
+ it("parses a JSON object value", () => {
20
+ const html = renderMarkdown('<Echo value={{ "a": 1, "b": [2, 3] }} />', { components });
21
+ expect(visibleText(html)).toContain('"value":{"a":1,"b":[2,3]}');
22
+ });
23
+
24
+ it("parses JSON number, boolean, and array values", () => {
25
+ const html = renderMarkdown("<Echo n={42} flag={true} list={[1,2]} />", { components });
26
+ const text = visibleText(html);
27
+ expect(text).toContain('"n":42');
28
+ expect(text).toContain('"flag":true');
29
+ expect(text).toContain('"list":[1,2]');
30
+ });
31
+
32
+ it("parses a quoted string value", () => {
33
+ const html = renderMarkdown('<Echo label="hello" />', { components });
34
+ expect(visibleText(html)).toContain('"label":"hello"');
35
+ });
36
+
37
+ it("treats a bare attribute as a boolean flag", () => {
38
+ const html = renderMarkdown("<Echo disabled />", { components });
39
+ // Bare booleans resolve to `undefined` props (not serialised) by design.
40
+ expect(visibleText(html)).toContain("props=");
41
+ });
42
+
43
+ it("drops an invalid-JSON value and warns in development", () => {
44
+ const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
45
+ const html = renderMarkdown("<Echo value={{ a: 1 }} />", { components });
46
+ // Single-unquoted-key object is not valid JSON → the prop is dropped.
47
+ expect(visibleText(html)).not.toContain('"value"');
48
+ expect(warn).toHaveBeenCalled();
49
+ });
49
50
  });
50
51
 
51
52
  describe("Conformance / inline expressions are literal", () => {
52
- it("renders a lone brace expression as text", () => {
53
- const html = renderMarkdown("Count: {count} items");
54
- expect(visibleText(html)).toContain("Count: {count} items");
55
- });
53
+ it("renders a lone brace expression as text", () => {
54
+ const html = renderMarkdown("Count: {count} items");
55
+ expect(visibleText(html)).toContain("Count: {count} items");
56
+ });
56
57
  });
@@ -16,150 +16,150 @@ import { renderToString } from "react-dom/server";
16
16
  import { beforeAll, describe, expect, it } from "vitest";
17
17
 
18
18
  beforeAll(() => {
19
- if (typeof window !== "undefined" && typeof window.matchMedia !== "function") {
20
- Object.defineProperty(window, "matchMedia", {
21
- writable: true,
22
- value: (query: string) => ({
23
- matches: false,
24
- media: query,
25
- onchange: null,
26
- addListener: () => {},
27
- removeListener: () => {},
28
- addEventListener: () => {},
29
- removeEventListener: () => {},
30
- dispatchEvent: () => false,
31
- }),
32
- });
33
- }
19
+ if (typeof window !== "undefined" && typeof window.matchMedia !== "function") {
20
+ Object.defineProperty(window, "matchMedia", {
21
+ writable: true,
22
+ value: (query: string) => ({
23
+ matches: false,
24
+ media: query,
25
+ onchange: null,
26
+ addListener: () => {},
27
+ removeListener: () => {},
28
+ addEventListener: () => {},
29
+ removeEventListener: () => {},
30
+ dispatchEvent: () => false,
31
+ }),
32
+ });
33
+ }
34
34
  });
35
35
 
36
36
  /** Deterministic LCG so failures reproduce (no Math.random). */
37
37
  function makeRng(seed: number): () => number {
38
- let s = seed >>> 0;
39
- return () => {
40
- s = (Math.imul(s, 1664525) + 1013904223) >>> 0;
41
- return s / 0x100000000;
42
- };
38
+ let s = seed >>> 0;
39
+ return () => {
40
+ s = (Math.imul(s, 1664525) + 1013904223) >>> 0;
41
+ return s / 0x100000000;
42
+ };
43
43
  }
44
44
 
45
45
  // Fragments chosen to stress tag pairing, attributes, nesting, blank lines,
46
46
  // indentation, and malformed/adversarial shapes.
47
47
  const FRAGMENTS = [
48
- "<Tabs>",
49
- "</Tabs>",
50
- '<Tabs syncKey="a b c">',
51
- '<TabItem label="One">',
52
- '<TabItem label="Two" icon="star">',
53
- "</TabItem>",
54
- "<Since />",
55
- '<Since v="1.0.0" />',
56
- '<Button label="Go" href="https://x.example" />',
57
- "<div>",
58
- "</div>",
59
- "<details>",
60
- "<summary>Toggle</summary>",
61
- "</details>",
62
- "<strong>bold</strong>",
63
- "<em>em",
64
- "</em>",
65
- 'text before <TabItem label="Inline"> and after',
66
- '<Echo value={{ "k": [1, 2, {"n": 3}] }} />',
67
- "<Echo value={ broken json } />",
68
- '<Echo value="unterminated ',
69
- '<Tabs><TabItem label="Nested">deep</TabItem></Tabs>',
70
- "\tindented content",
71
- " four space indent",
72
- "para line one",
73
- "",
74
- "",
75
- "# heading",
76
- "> quote",
77
- "- list item",
78
- "`code`",
79
- "<https://autolink.example>",
80
- "<not a tag",
81
- "a > b < c",
82
- "</Orphan>",
83
- "<Weird a={{{}}} b='x' c=\"y\" />",
84
- "| a | b |\n| - | - |\n| 1 | 2 |",
48
+ "<Tabs>",
49
+ "</Tabs>",
50
+ '<Tabs syncKey="a b c">',
51
+ '<TabItem label="One">',
52
+ '<TabItem label="Two" icon="star">',
53
+ "</TabItem>",
54
+ "<Since />",
55
+ '<Since v="1.0.0" />',
56
+ '<Button label="Go" href="https://x.example" />',
57
+ "<div>",
58
+ "</div>",
59
+ "<details>",
60
+ "<summary>Toggle</summary>",
61
+ "</details>",
62
+ "<strong>bold</strong>",
63
+ "<em>em",
64
+ "</em>",
65
+ 'text before <TabItem label="Inline"> and after',
66
+ '<Echo value={{ "k": [1, 2, {"n": 3}] }} />',
67
+ "<Echo value={ broken json } />",
68
+ '<Echo value="unterminated ',
69
+ '<Tabs><TabItem label="Nested">deep</TabItem></Tabs>',
70
+ "\tindented content",
71
+ " four space indent",
72
+ "para line one",
73
+ "",
74
+ "",
75
+ "# heading",
76
+ "> quote",
77
+ "- list item",
78
+ "`code`",
79
+ "<https://autolink.example>",
80
+ "<not a tag",
81
+ "a > b < c",
82
+ "</Orphan>",
83
+ "<Weird a={{{}}} b='x' c=\"y\" />",
84
+ "| a | b |\n| - | - |\n| 1 | 2 |",
85
85
  ];
86
86
 
87
87
  const COMPONENTS = mergeMarkdownComponents({
88
- Echo: (props: Record<string, unknown>) => JSON.stringify(props.value ?? null),
88
+ Echo: (props: Record<string, unknown>) => JSON.stringify(props.value ?? null),
89
89
  });
90
90
 
91
91
  function buildDoc(rng: () => number): string {
92
- const lines = 1 + Math.floor(rng() * 12);
93
- const parts: string[] = [];
94
- for (let i = 0; i < lines; i++) {
95
- parts.push(FRAGMENTS[Math.floor(rng() * FRAGMENTS.length)] ?? "");
96
- }
97
- return parts.join("\n");
92
+ const lines = 1 + Math.floor(rng() * 12);
93
+ const parts: string[] = [];
94
+ for (let i = 0; i < lines; i++) {
95
+ parts.push(FRAGMENTS[Math.floor(rng() * FRAGMENTS.length)] ?? "");
96
+ }
97
+ return parts.join("\n");
98
98
  }
99
99
 
100
100
  function renderViewer(doc: string): void {
101
- renderToString(
102
- <MantineProvider
103
- theme={MANTINE_THEME}
104
- cssVariablesResolver={v8CssVariablesResolver}
105
- forceColorScheme="dark"
106
- >
107
- <MarkdownViewer
108
- content={doc}
109
- components={COMPONENTS}
110
- />
111
- </MantineProvider>,
112
- );
101
+ renderToString(
102
+ <MantineProvider
103
+ theme={MANTINE_THEME}
104
+ cssVariablesResolver={v8CssVariablesResolver}
105
+ forceColorScheme="dark"
106
+ >
107
+ <MarkdownViewer
108
+ content={doc}
109
+ components={COMPONENTS}
110
+ />
111
+ </MantineProvider>,
112
+ );
113
113
  }
114
114
 
115
115
  function buildEditor(doc: string): void {
116
- const state = EditorState.create({
117
- doc,
118
- extensions: [
119
- createMarkdownLanguage({ extensions: [Callout] }),
120
- markdownModeField,
121
- markdownDecorations({ widgetStore: createWidgetStore() }),
122
- ],
123
- });
124
- const parent = document.createElement("div");
125
- document.body.appendChild(parent);
126
- const view = new EditorView({ state, parent });
127
- view.dispatch({ effects: setMarkdownMode.of("preview") });
128
- view.dispatch({ effects: setMarkdownEditorFocused.of(true) });
129
- // Sweep the caret through the doc so active/reveal paths are exercised.
130
- for (let pos = 0; pos <= doc.length; pos += Math.max(1, Math.floor(doc.length / 6))) {
131
- view.dispatch({ selection: { anchor: pos } });
132
- }
133
- view.destroy();
134
- parent.remove();
116
+ const state = EditorState.create({
117
+ doc,
118
+ extensions: [
119
+ createMarkdownLanguage({ extensions: [Callout] }),
120
+ markdownModeField,
121
+ markdownDecorations({ widgetStore: createWidgetStore() }),
122
+ ],
123
+ });
124
+ const parent = document.createElement("div");
125
+ document.body.appendChild(parent);
126
+ const view = new EditorView({ state, parent });
127
+ view.dispatch({ effects: setMarkdownMode.of("preview") });
128
+ view.dispatch({ effects: setMarkdownEditorFocused.of(true) });
129
+ // Sweep the caret through the doc so active/reveal paths are exercised.
130
+ for (let pos = 0; pos <= doc.length; pos += Math.max(1, Math.floor(doc.length / 6))) {
131
+ view.dispatch({ selection: { anchor: pos } });
132
+ }
133
+ view.destroy();
134
+ parent.remove();
135
135
  }
136
136
 
137
137
  describe("MarkdownConformance / JSX fuzz", () => {
138
- // Each case runs a full Mantine SSR render plus an EditorView build with a
139
- // caret sweep, so keep the count modest and grant a generous timeout for CI.
140
- it("never throws and keeps descriptor offsets sane across random JSX/HTML docs", () => {
141
- const rng = makeRng(0xc0ffee);
142
- for (let i = 0; i < 200; i++) {
143
- const doc = buildDoc(rng);
144
- try {
145
- const tree = parseMarkdownTree(doc);
146
- const src = markdownSourceFromString(doc);
147
- const descs = collectBlockHtmlDescriptors(tree, src, COMPONENTS);
148
- for (const d of descs) {
149
- expect(d.openFrom).toBeGreaterThanOrEqual(0);
150
- expect(d.openTo).toBeGreaterThan(d.openFrom);
151
- if (d.closeTo !== null) {
152
- expect(d.closeTo).toBeGreaterThanOrEqual(d.openTo);
153
- expect(d.closeTo).toBeLessThanOrEqual(doc.length);
154
- }
155
- }
156
- renderViewer(doc);
157
- buildEditor(doc);
158
- } catch (err) {
159
- throw new Error(
160
- `Fuzz case #${i} threw for input:\n${JSON.stringify(doc)}\n\n${String(err)}`,
161
- );
162
- }
163
- }
164
- }, 30_000);
138
+ // Each case runs a full Mantine SSR render plus an EditorView build with a
139
+ // caret sweep, so keep the count modest and grant a generous timeout for CI.
140
+ it("never throws and keeps descriptor offsets sane across random JSX/HTML docs", () => {
141
+ const rng = makeRng(0xc0ffee);
142
+ for (let i = 0; i < 200; i++) {
143
+ const doc = buildDoc(rng);
144
+ try {
145
+ const tree = parseMarkdownTree(doc);
146
+ const src = markdownSourceFromString(doc);
147
+ const descs = collectBlockHtmlDescriptors(tree, src, COMPONENTS);
148
+ for (const d of descs) {
149
+ expect(d.openFrom).toBeGreaterThanOrEqual(0);
150
+ expect(d.openTo).toBeGreaterThan(d.openFrom);
151
+ if (d.closeTo !== null) {
152
+ expect(d.closeTo).toBeGreaterThanOrEqual(d.openTo);
153
+ expect(d.closeTo).toBeLessThanOrEqual(doc.length);
154
+ }
155
+ }
156
+ renderViewer(doc);
157
+ buildEditor(doc);
158
+ } catch (err) {
159
+ throw new Error(
160
+ `Fuzz case #${i} threw for input:\n${JSON.stringify(doc)}\n\n${String(err)}`,
161
+ );
162
+ }
163
+ }
164
+ }, 30_000);
165
165
  });