@surrealdb/ui 1.2.15 → 1.2.17

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 (148) hide show
  1. package/dist/assets/1299e4695ad0bd61.svg +5 -0
  2. package/dist/assets/408117e904653548.svg +48 -0
  3. package/dist/assets/b516b020352f6369.svg +1 -0
  4. package/dist/assets/dfde8de566c3c2b9.svg +11 -0
  5. package/dist/assets/e9eae7bc863f211b.svg +1 -0
  6. package/dist/assets/f74ae3707a5d418f.svg +1 -0
  7. package/dist/ui.d.ts +12 -0
  8. package/dist/ui.js +678 -649
  9. package/dist/ui.js.map +1 -1
  10. package/package.json +8 -2
  11. package/.claude/launch.json +0 -17
  12. package/.oxfmtrc.json +0 -27
  13. package/.oxlintrc.json +0 -23
  14. package/.picto-fix-png/Cursor.png +0 -0
  15. package/.picto-fix-png/DocumentGradient.png +0 -0
  16. package/.picto-fix-png/FileGradient.png +0 -0
  17. package/.picto-fix-png/HealthBubbleGradient.png +0 -0
  18. package/.picto-fix-png/LearnGradient.png +0 -0
  19. package/.picto-fix-png/MediaFileGradient.png +0 -0
  20. package/.zed/settings.json +0 -36
  21. package/AGENTS.md +0 -111
  22. package/CLAUDE.md +0 -111
  23. package/REVIEW.md +0 -81
  24. package/doc/code-style.md +0 -264
  25. package/doc/release.md +0 -102
  26. package/doc/stacker-plan.md +0 -219
  27. package/release-notes.md +0 -0
  28. package/tests/_setup/e2e-helpers.tsx +0 -169
  29. package/tests/_setup/markdown-classes.ts +0 -3
  30. package/tests/_setup/portable-stories.ts +0 -33
  31. package/tests/e2e/MarkdownEditor/__screenshots__/content-blocks.test.tsx/MarkdownEditor---content-blocks-continues-an-ordered-list-when-Enter-is-pressed-at-the-end-of-a-numbered-line-1.png +0 -0
  32. package/tests/e2e/MarkdownEditor/__screenshots__/content-blocks.test.tsx/MarkdownEditor---content-blocks-renders-a-diagram-for-the-fenced-mermaid-block-1.png +0 -0
  33. package/tests/e2e/MarkdownEditor/__screenshots__/content-blocks.test.tsx/MarkdownEditor---content-blocks-renders-a-horizontal-rule-as-a-separator-1.png +0 -0
  34. package/tests/e2e/MarkdownEditor/__screenshots__/content-blocks.test.tsx/MarkdownEditor---content-blocks-renders-a-read-only-table-preview-when-the-table-block-is-inactive-1.png +0 -0
  35. package/tests/e2e/MarkdownEditor/__screenshots__/content-blocks.test.tsx/MarkdownEditor---content-blocks-shows-blockquote-text-from-the-sample-document-1.png +0 -0
  36. package/tests/e2e/MarkdownEditor/__screenshots__/content-blocks.test.tsx/MarkdownEditor---content-blocks-shows-fenced-TypeScript-sample-code-in-the-document-1.png +0 -0
  37. package/tests/e2e/MarkdownEditor/__screenshots__/edits.test.tsx/MarkdownEditor---edits-auto-continues-a-bullet-list-when-Enter-is-pressed-at-the-end-of-a-list-item-1.png +0 -0
  38. package/tests/e2e/MarkdownEditor/__screenshots__/edits.test.tsx/MarkdownEditor---edits-expands-triple-backtick-into-a-fenced-code-block-with-the-caret-on-the-empty-body-line-1.png +0 -0
  39. package/tests/e2e/MarkdownEditor/__screenshots__/edits.test.tsx/MarkdownEditor---edits-reflects-typed-characters-in-the-underlying-document-1.png +0 -0
  40. package/tests/e2e/MarkdownEditor/__screenshots__/heading-fold.test.tsx/MarkdownEditor---heading-folds-viewer--folds-and-unfolds-a-heading-section-via-the-margin-control-1.png +0 -0
  41. package/tests/e2e/MarkdownEditor/__screenshots__/hybrid-widgets.test.tsx/MarkdownEditor---hybrid-widgets-clicking-a-callout-header-focuses-the-editor-and-parks-the-caret-inside-the-callout--REASONING--4--1.png +0 -0
  42. package/tests/e2e/MarkdownEditor/__screenshots__/hybrid-widgets.test.tsx/MarkdownEditor---hybrid-widgets-keeps-list-bullets-consistent-after-hopping-the-caret-across-items-several-times-1.png +0 -0
  43. package/tests/e2e/MarkdownEditor/__screenshots__/hybrid-widgets.test.tsx/MarkdownEditor---hybrid-widgets-reveals-heading-marks-when-the-caret-enters-the-heading-line-and-hides-them-when-it-leaves-1.png +0 -0
  44. package/tests/e2e/MarkdownEditor/__screenshots__/hybrid-widgets.test.tsx/MarkdownEditor---hybrid-widgets-shows-the-right-callout-title-when-moving-focus-between-consecutive-callouts-1.png +0 -0
  45. package/tests/e2e/MarkdownEditor/__screenshots__/hybrid-widgets.test.tsx/MarkdownEditor---preview-widgets-clicking-a-callout-header-focuses-the-editor-with-the-caret-in-that-callout-1.png +0 -0
  46. package/tests/e2e/MarkdownEditor/__screenshots__/hybrid-widgets.test.tsx/MarkdownEditor---preview-widgets-keeps-list-bullets-consistent-after-hopping-the-caret-across-items-several-times-1.png +0 -0
  47. package/tests/e2e/MarkdownEditor/__screenshots__/hybrid-widgets.test.tsx/MarkdownEditor---preview-widgets-shows-the-right-callout-title-when-moving-focus-between-consecutive-callouts-1.png +0 -0
  48. package/tests/e2e/MarkdownEditor/__screenshots__/jsx-block-content.test.tsx/MarkdownEditor---block-JSX-components-clicking-the-edit-source-action-selects-the-component-source-range-1.png +0 -0
  49. package/tests/e2e/MarkdownEditor/__screenshots__/jsx-block-content.test.tsx/MarkdownEditor---block-JSX-components-renders-block-components-via-JsxBlockWidget-with-an-edit-source-action-1.png +0 -0
  50. package/tests/e2e/MarkdownEditor/__screenshots__/jsx-block-content.test.tsx/MarkdownEditor---inline-JSX-rendering-block-content-keeps-multiple-inline-JSX-widgets-on-the-same-line-side-by-side-1.png +0 -0
  51. package/tests/e2e/MarkdownEditor/__screenshots__/jsx-highlight.test.tsx/MarkdownEditor---JSX-attribute-highlighting-does-not-let-the-nested-HTML-parser-mis-highlight-attributes-after-an-expression-1.png +0 -0
  52. package/tests/e2e/MarkdownEditor/__screenshots__/jsx-inline-click.test.tsx/MarkdownEditor---inline-JSX-click-reveal-reveals-the-raw-source-when-an-inline-component-badge-is-clicked-1.png +0 -0
  53. package/tests/e2e/MarkdownEditor/__screenshots__/jsx-inline-click.test.tsx/MarkdownEditor---inline-JSX-click-reveal-reveals-the-raw-source-when-an-inline-component-badge-is-clicked-2.png +0 -0
  54. package/tests/e2e/MarkdownEditor/__screenshots__/jsx-inline-click.test.tsx/MarkdownEditor---inline-JSX-click-reveal-reveals-the-raw-source-when-an-inline-component-badge-is-clicked-3.png +0 -0
  55. package/tests/e2e/MarkdownEditor/__screenshots__/jsx-selection.test.tsx/MarkdownEditor---JSX-selection-reveal-shows-JSX-widget-when-inactive-and-raw-source-when-caret-is-inside-the-tag-1.png +0 -0
  56. package/tests/e2e/MarkdownEditor/__screenshots__/media-sizing.test.tsx/MarkdownEditor---media-sizing-images-and-videos-do-not-exceed-the-editor-content-width-1.png +0 -0
  57. package/tests/e2e/MarkdownEditor/__screenshots__/media-sizing.test.tsx/MarkdownEditor---media-sizing-matches-MarkdownViewer-image-width-in-the-split-playground-1.png +0 -0
  58. package/tests/e2e/MarkdownEditor/__screenshots__/modes.test.tsx/MarkdownEditor---modes-standalone-MarkdownViewer-is-not-a-CodeMirror-surface-1.png +0 -0
  59. package/tests/e2e/MarkdownEditor/__screenshots__/modes.test.tsx/MarkdownEditor---modes-toggling-the-SegmentedControl-swaps-mode-without-remounting-the-editor-1.png +0 -0
  60. package/tests/e2e/MarkdownEditor/__screenshots__/regressions.test.tsx/MarkdownEditor---regressions-repeated-checkbox-toggles-do-not-duplicate-or-drift-task-markers-1.png +0 -0
  61. package/tests/e2e/MarkdownEditor/__screenshots__/regressions.test.tsx/MarkdownEditor---regressions-scroll-position-is-preserved-when-the-document-is-edited--chat-92584463--1.png +0 -0
  62. package/tests/e2e/MarkdownEditor/__screenshots__/regressions.test.tsx/MarkdownEditor---regressions-task-checkbox-toggles-between-checked-and-unchecked-in-the-source-document-1.png +0 -0
  63. package/tests/e2e/MarkdownEditor/__screenshots__/regressions.test.tsx/MarkdownEditor---regressions-toggle-via-checkbox-preserves-task-marker-widgets-visible-from-the-previous-caret-position-1.png +0 -0
  64. package/tests/e2e/MarkdownEditor/__screenshots__/slash-commands.test.tsx/MarkdownEditor---slash-commands-dismisses-on-Escape-and-leaves-the-slash-as-literal-text-1.png +0 -0
  65. package/tests/e2e/MarkdownEditor/__screenshots__/slash-commands.test.tsx/MarkdownEditor---slash-commands-highlights-the-slash-and-shows-an-Enter-command-placeholder-until-text-is-typed-1.png +0 -0
  66. package/tests/e2e/MarkdownEditor/__screenshots__/slash-commands.test.tsx/MarkdownEditor---slash-commands-opens-a-filtered--keyboard-navigable-menu-and-inserts-boilerplate-on-Enter-1.png +0 -0
  67. package/tests/e2e/MarkdownEditor/__screenshots__/undo-redo.test.tsx/MarkdownEditor---undo-and-redo-restores-undone-document-edits-via-redo-1.png +0 -0
  68. package/tests/e2e/MarkdownEditor/content-blocks.test.tsx +0 -153
  69. package/tests/e2e/MarkdownEditor/edits.test.tsx +0 -112
  70. package/tests/e2e/MarkdownEditor/heading-fold.test.tsx +0 -45
  71. package/tests/e2e/MarkdownEditor/hybrid-widgets.test.tsx +0 -193
  72. package/tests/e2e/MarkdownEditor/image-on-image.test.tsx +0 -73
  73. package/tests/e2e/MarkdownEditor/jsx-block-content.test.tsx +0 -243
  74. package/tests/e2e/MarkdownEditor/jsx-highlight.test.tsx +0 -69
  75. package/tests/e2e/MarkdownEditor/jsx-inline-badges.test.tsx +0 -60
  76. package/tests/e2e/MarkdownEditor/jsx-inline-click.test.tsx +0 -54
  77. package/tests/e2e/MarkdownEditor/jsx-selection.test.tsx +0 -44
  78. package/tests/e2e/MarkdownEditor/link-placeholder.test.tsx +0 -68
  79. package/tests/e2e/MarkdownEditor/media-align.test.tsx +0 -58
  80. package/tests/e2e/MarkdownEditor/media-edit.test.tsx +0 -64
  81. package/tests/e2e/MarkdownEditor/media-sizing.test.tsx +0 -124
  82. package/tests/e2e/MarkdownEditor/modes.test.tsx +0 -94
  83. package/tests/e2e/MarkdownEditor/onimage-parity.test.tsx +0 -50
  84. package/tests/e2e/MarkdownEditor/regressions.test.tsx +0 -223
  85. package/tests/e2e/MarkdownEditor/slash-commands.test.tsx +0 -100
  86. package/tests/e2e/MarkdownEditor/table-click.test.tsx +0 -48
  87. package/tests/e2e/MarkdownEditor/table-controls.test.tsx +0 -57
  88. package/tests/e2e/MarkdownEditor/table-format.test.tsx +0 -42
  89. package/tests/e2e/MarkdownEditor/undo-redo.test.tsx +0 -39
  90. package/tests/e2e/MarkdownViewer/__screenshots__/parity.test.tsx/MarkdownViewer---editor-parity-matches-computed-font-size-on-first-heading-1.png +0 -0
  91. package/tests/e2e/MarkdownViewer/__screenshots__/parity.test.tsx/MarkdownViewer---editor-parity-matches-counts-for-shared-structural-classes-1.png +0 -0
  92. package/tests/e2e/MarkdownViewer/__screenshots__/parity.test.tsx/MarkdownViewer---editor-parity-matches-visible-text-between-preview-editor--blurred--and-MarkdownViewer-1.png +0 -0
  93. package/tests/e2e/MarkdownViewer/__screenshots__/render.test.tsx/MarkdownViewer---render-exercises-shared-preview-class-names-without-mounting-CodeMirror-1.png +0 -0
  94. package/tests/e2e/MarkdownViewer/parity.test.tsx +0 -191
  95. package/tests/e2e/MarkdownViewer/render.test.tsx +0 -36
  96. package/tests/e2e/Stacker/stacker.test.tsx +0 -304
  97. package/tests/unit/Editor/helpers.test.ts +0 -42
  98. package/tests/unit/MarkdownConformance/error-isolation.test.tsx +0 -44
  99. package/tests/unit/MarkdownConformance/indentation.test.tsx +0 -30
  100. package/tests/unit/MarkdownConformance/inline-code-fuzz.test.tsx +0 -255
  101. package/tests/unit/MarkdownConformance/jsx-attributes.test.tsx +0 -57
  102. package/tests/unit/MarkdownConformance/jsx-fuzz.test.tsx +0 -165
  103. package/tests/unit/MarkdownConformance/links.test.tsx +0 -95
  104. package/tests/unit/MarkdownConformance/render-helpers.tsx +0 -88
  105. package/tests/unit/MarkdownEditor/code-info.test.ts +0 -63
  106. package/tests/unit/MarkdownEditor/decorations.test.ts +0 -490
  107. package/tests/unit/MarkdownEditor/editor-ready.test.ts +0 -36
  108. package/tests/unit/MarkdownEditor/html-descriptors.test.ts +0 -94
  109. package/tests/unit/MarkdownEditor/indented-fence.test.ts +0 -103
  110. package/tests/unit/MarkdownEditor/jsx-attr-scan.test.ts +0 -115
  111. package/tests/unit/MarkdownEditor/jsx-block-widget.test.ts +0 -92
  112. package/tests/unit/MarkdownEditor/jsx-tag-grammar.test.ts +0 -88
  113. package/tests/unit/MarkdownEditor/list-indent.test.ts +0 -95
  114. package/tests/unit/MarkdownEditor/slash-commands.test.ts +0 -213
  115. package/tests/unit/MarkdownEditor/table-format.test.ts +0 -83
  116. package/tests/unit/MarkdownEditor/table.test.ts +0 -119
  117. package/tests/unit/MarkdownEditor/triggers.test.ts +0 -244
  118. package/tests/unit/MarkdownEditor/widget-store.test.ts +0 -105
  119. package/tests/unit/MarkdownViewer/callout.test.tsx +0 -60
  120. package/tests/unit/MarkdownViewer/code-highlight.test.tsx +0 -61
  121. package/tests/unit/MarkdownViewer/code-title.test.tsx +0 -62
  122. package/tests/unit/MarkdownViewer/features.test.tsx +0 -164
  123. package/tests/unit/MarkdownViewer/headings.test.tsx +0 -40
  124. package/tests/unit/MarkdownViewer/indented-jsx-children.test.tsx +0 -98
  125. package/tests/unit/MarkdownViewer/jsx-block-nesting.test.tsx +0 -234
  126. package/tests/unit/MarkdownViewer/jsx.test.tsx +0 -211
  127. package/tests/unit/MarkdownViewer/list-bullets.test.tsx +0 -49
  128. package/tests/unit/MarkdownViewer/list-code.test.tsx +0 -65
  129. package/tests/unit/MarkdownViewer/preserve-newlines.test.tsx +0 -143
  130. package/tests/unit/MarkdownViewer/read-time.test.ts +0 -21
  131. package/tests/unit/MarkdownViewer/renderers.test.tsx +0 -95
  132. package/tests/unit/MarkdownViewer/runnable.test.tsx +0 -69
  133. package/tests/unit/MarkdownViewer/ssr.test.tsx +0 -94
  134. package/tests/unit/MarkdownViewer/syntax-highlighting.test.tsx +0 -74
  135. package/tests/unit/MarkdownViewer/tabs-fenced-code.test.tsx +0 -196
  136. package/tests/unit/MarkdownViewer/tabs.test.tsx +0 -116
  137. package/tests/unit/Stacker/drag.test.ts +0 -21
  138. package/tests/unit/Stacker/helpers.test.ts +0 -105
  139. package/tests/unit/Stacker/reducer.test.ts +0 -107
  140. package/tools/duplicate-vector-mappings.json +0 -143
  141. package/tools/glass-export-overrides.json +0 -8
  142. package/tools/icon-parser.ts +0 -78
  143. package/tools/integration-gradient-exports.json +0 -57
  144. package/tools/picto-compare.ts +0 -64
  145. package/tools/picto-merge-urls.ts +0 -43
  146. package/tools/picto-sync.ts +0 -630
  147. package/tools/picto-verify-gradients.ts +0 -86
  148. package/tools/vector-export-nodes.json +0 -977
@@ -1,255 +0,0 @@
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,57 +0,0 @@
1
- import type { MarkdownComponents } from "@src/lib/markdown";
2
- import { afterEach, describe, expect, it, vi } from "vitest";
3
-
4
- import { renderMarkdown, visibleText } from "./render-helpers";
5
-
6
- /** Echo component that serialises the props it receives for assertion. */
7
- function Echo(props: Record<string, unknown>): string {
8
- const { children: _, ...rest } = props;
9
- return `props=${JSON.stringify(rest)}`;
10
- }
11
-
12
- const components: MarkdownComponents = { Echo };
13
-
14
- afterEach(() => {
15
- vi.restoreAllMocks();
16
- });
17
-
18
- describe("Conformance / JSX attribute JSON", () => {
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
- });
50
- });
51
-
52
- describe("Conformance / inline expressions are literal", () => {
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
- });
57
- });
@@ -1,165 +0,0 @@
1
- import { EditorState } from "@codemirror/state";
2
- import { EditorView } from "@codemirror/view";
3
- import { MantineProvider, v8CssVariablesResolver } from "@mantine/core";
4
- import { markdownModeField, markdownSourceFromString, parseMarkdownTree } 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 { collectBlockHtmlDescriptors } from "@src/lib/markdown/tree/html";
13
- import { MarkdownViewer } from "@src/primitives/MarkdownViewer";
14
- import { MANTINE_THEME } from "@src/theme/mantine";
15
- import { renderToString } from "react-dom/server";
16
- import { beforeAll, describe, expect, it } from "vitest";
17
-
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
- }
34
- });
35
-
36
- /** Deterministic LCG so failures reproduce (no Math.random). */
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
- };
43
- }
44
-
45
- // Fragments chosen to stress tag pairing, attributes, nesting, blank lines,
46
- // indentation, and malformed/adversarial shapes.
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 |",
85
- ];
86
-
87
- const COMPONENTS = mergeMarkdownComponents({
88
- Echo: (props: Record<string, unknown>) => JSON.stringify(props.value ?? null),
89
- });
90
-
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");
98
- }
99
-
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
- );
113
- }
114
-
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();
135
- }
136
-
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);
165
- });
@@ -1,95 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
-
3
- import { renderMarkdown, visibleText } from "./render-helpers";
4
-
5
- describe("Conformance / autolinks", () => {
6
- it("keeps an absolute URL href unchanged", () => {
7
- const html = renderMarkdown("Go https://example.com now");
8
- expect(html).toContain('href="https://example.com"');
9
- });
10
-
11
- it("adds https:// to a www. autolink", () => {
12
- const html = renderMarkdown("Visit www.example.com today");
13
- expect(html).toContain('href="https://www.example.com"');
14
- });
15
-
16
- it("adds mailto: to a bare email autolink", () => {
17
- const html = renderMarkdown("Mail me@example.com please");
18
- expect(html).toContain('href="mailto:me@example.com"');
19
- });
20
-
21
- it("renders an angle-bracket URL autolink as an anchor", () => {
22
- const html = renderMarkdown("See <https://example.com> here");
23
- expect(html).toContain('href="https://example.com"');
24
- expect(visibleText(html)).toContain("https://example.com");
25
- // The angle brackets are delimiters, not visible content.
26
- expect(visibleText(html)).not.toContain("<https://example.com>");
27
- });
28
-
29
- it("renders an angle-bracket email autolink with mailto:", () => {
30
- const html = renderMarkdown("Mail <me@example.com> ok");
31
- expect(html).toContain('href="mailto:me@example.com"');
32
- });
33
-
34
- it("does not linkify a URL inside inline code", () => {
35
- const html = renderMarkdown("Use `https://example.com` verbatim");
36
- expect(html).not.toContain('href="https://example.com"');
37
- expect(html).toContain("https://example.com");
38
- });
39
- });
40
-
41
- describe("Conformance / angle brackets in inline code", () => {
42
- // Regression: `<…>` inside inline code was scanned as an HTML/JSX tag,
43
- // which pushed the whole paragraph down the descriptor path and rendered
44
- // every backtick literally while dropping the bracketed text.
45
- it("keeps inline code intact when it contains an angle-bracket placeholder", () => {
46
- const html = renderMarkdown(
47
- "`.{..+collect}` returns the set; and `.{..+shortest=<target>}` returns the shortest path.",
48
- );
49
- expect(html).toContain("<code");
50
- expect(html).toContain(".{..+collect}");
51
- expect(html).toContain(".{..+shortest=&lt;target&gt;}");
52
- // Backticks must not leak into the visible output.
53
- expect(visibleText(html)).not.toContain("`");
54
- expect(visibleText(html)).toContain(".{..+shortest=<target>}");
55
- });
56
-
57
- it("still pairs a real inline tag that sits beside inline code", () => {
58
- const html = renderMarkdown("Press `Enter` then <strong>go</strong> now");
59
- expect(html).toContain("<code");
60
- expect(html).toContain("<strong>go</strong>");
61
- });
62
- });
63
-
64
- describe("Conformance / reference-style links", () => {
65
- it("resolves a full reference link [text][id]", () => {
66
- const html = renderMarkdown("[click here][site] now\n\n[site]: https://example.com");
67
- expect(html).toContain('href="https://example.com"');
68
- expect(visibleText(html)).toContain("click here");
69
- });
70
-
71
- it("resolves a collapsed reference link [id][]", () => {
72
- const html = renderMarkdown("[site][] now\n\n[site]: https://example.com");
73
- expect(html).toContain('href="https://example.com"');
74
- expect(visibleText(html)).toContain("site");
75
- });
76
-
77
- it("resolves a shortcut reference link [id]", () => {
78
- const html = renderMarkdown("[site] now\n\n[site]: https://example.com");
79
- expect(html).toContain('href="https://example.com"');
80
- });
81
-
82
- it("matches reference labels case-insensitively", () => {
83
- const html = renderMarkdown("[Click][SITE] now\n\n[site]: https://example.com");
84
- expect(html).toContain('href="https://example.com"');
85
- });
86
-
87
- it("emits the visible text (no anchor) when the reference is undefined", () => {
88
- const html = renderMarkdown("see [missing][nope] end");
89
- expect(html).not.toContain("href=");
90
- const text = visibleText(html);
91
- expect(text).toContain("missing");
92
- // Reference brackets/label are not shown as literal text.
93
- expect(text).not.toContain("[missing]");
94
- });
95
- });
@@ -1,88 +0,0 @@
1
- import { MantineProvider, v8CssVariablesResolver } from "@mantine/core";
2
- import type { MarkdownViewerProps } from "@src/primitives/MarkdownViewer";
3
- import { MarkdownViewer } from "@src/primitives/MarkdownViewer";
4
- import { MANTINE_THEME } from "@src/theme/mantine";
5
- import { act } from "react";
6
- import { createRoot } from "react-dom/client";
7
- import { renderToString } from "react-dom/server";
8
-
9
- // jsdom lacks matchMedia, which Mantine touches during render.
10
- if (typeof window !== "undefined" && typeof window.matchMedia !== "function") {
11
- Object.defineProperty(window, "matchMedia", {
12
- writable: true,
13
- value: (query: string) => ({
14
- matches: false,
15
- media: query,
16
- onchange: null,
17
- addListener: () => {},
18
- removeListener: () => {},
19
- addEventListener: () => {},
20
- removeEventListener: () => {},
21
- dispatchEvent: () => false,
22
- }),
23
- });
24
- }
25
-
26
- /** Render a `MarkdownViewer` to an HTML string inside a Mantine provider. */
27
- export function renderMarkdown(content: string, props?: Partial<MarkdownViewerProps>): string {
28
- return renderToString(
29
- <MantineProvider
30
- theme={MANTINE_THEME}
31
- cssVariablesResolver={v8CssVariablesResolver}
32
- forceColorScheme="dark"
33
- >
34
- <MarkdownViewer
35
- content={content}
36
- {...props}
37
- />
38
- </MantineProvider>,
39
- );
40
- }
41
-
42
- /**
43
- * Client-render a `MarkdownViewer` into jsdom and return its HTML. Unlike SSR
44
- * `renderToString`, this exercises React error boundaries, which recover on the
45
- * client (the environment where a throwing embedded component would blank the UI).
46
- */
47
- export function renderMarkdownClient(
48
- content: string,
49
- props?: Partial<MarkdownViewerProps>,
50
- ): string {
51
- const container = document.createElement("div");
52
- document.body.appendChild(container);
53
- const root = createRoot(container);
54
- act(() => {
55
- root.render(
56
- <MantineProvider
57
- theme={MANTINE_THEME}
58
- cssVariablesResolver={v8CssVariablesResolver}
59
- forceColorScheme="dark"
60
- >
61
- <MarkdownViewer
62
- content={content}
63
- {...props}
64
- />
65
- </MantineProvider>,
66
- );
67
- });
68
- const html = container.innerHTML;
69
- act(() => {
70
- root.unmount();
71
- });
72
- container.remove();
73
- return html;
74
- }
75
-
76
- /** Strip all HTML tags and decode common entities, returning the visible text. */
77
- export function visibleText(html: string): string {
78
- return html
79
- .replace(/<style[^>]*>[\s\S]*?<\/style>/g, "")
80
- .replace(/<[^>]+>/g, "")
81
- .replace(/&quot;/g, '"')
82
- .replace(/&#39;/g, "'")
83
- .replace(/&lt;/g, "<")
84
- .replace(/&gt;/g, ">")
85
- .replace(/&amp;/g, "&")
86
- .replace(/\s+/g, " ")
87
- .trim();
88
- }