@surrealdb/ui 1.2.3 → 1.2.4
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.
- package/README.md +16 -0
- package/REVIEW.md +2 -2
- package/dist/ui.css +1 -1
- package/dist/ui.d.ts +257 -110
- package/dist/ui.js +7036 -6307
- package/dist/ui.js.map +1 -1
- package/package.json +1 -1
- 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
- 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
- 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
- package/tests/e2e/MarkdownEditor/image-on-image.test.tsx +72 -0
- package/tests/e2e/MarkdownEditor/jsx-inline-click.test.tsx +53 -0
- package/tests/e2e/MarkdownEditor/onimage-parity.test.tsx +49 -0
- package/tests/e2e/MarkdownEditor/regressions.test.tsx +40 -0
- package/tests/unit/MarkdownConformance/error-isolation.test.tsx +43 -0
- package/tests/unit/MarkdownConformance/indentation.test.tsx +29 -0
- package/tests/unit/MarkdownConformance/jsx-attributes.test.tsx +56 -0
- package/tests/unit/MarkdownConformance/jsx-fuzz.test.tsx +165 -0
- package/tests/unit/MarkdownConformance/links.test.tsx +71 -0
- package/tests/unit/MarkdownConformance/render-helpers.tsx +88 -0
- package/tests/unit/MarkdownEditor/decorations.test.ts +2 -2
- package/tests/unit/MarkdownEditor/indented-fence.test.ts +103 -0
- package/tests/unit/MarkdownEditor/jsx-attr-scan.test.ts +1 -1
- package/tests/unit/MarkdownEditor/jsx-block-widget.test.ts +92 -0
- package/tests/unit/MarkdownEditor/jsx-tag-grammar.test.ts +2 -2
- package/tests/unit/MarkdownEditor/list-indent.test.ts +2 -2
- package/tests/unit/MarkdownEditor/slash-commands.test.ts +2 -2
- package/tests/unit/MarkdownEditor/triggers.test.ts +2 -2
- package/tests/unit/MarkdownViewer/callout.test.tsx +60 -0
- package/tests/unit/MarkdownViewer/code-highlight.test.tsx +61 -0
- package/tests/unit/MarkdownViewer/features.test.tsx +55 -1
- package/tests/unit/MarkdownViewer/indented-jsx-children.test.tsx +98 -0
- package/tests/unit/MarkdownViewer/jsx-block-nesting.test.tsx +234 -0
- package/tests/unit/MarkdownViewer/jsx.test.tsx +2 -2
- package/tests/unit/MarkdownViewer/preserve-newlines.test.tsx +143 -0
- package/tests/unit/MarkdownViewer/read-time.test.ts +21 -0
- package/tests/unit/MarkdownViewer/renderers.test.tsx +16 -0
- package/tests/unit/MarkdownViewer/syntax-highlighting.test.tsx +73 -0
- package/tests/unit/MarkdownViewer/tabs-fenced-code.test.tsx +196 -0
- package/tests/unit/MarkdownViewer/tabs.test.tsx +115 -0
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { MantineProvider, v8CssVariablesResolver } from "@mantine/core";
|
|
2
|
+
import { MarkdownViewer } from "@src/primitives/MarkdownViewer";
|
|
3
|
+
import { MANTINE_THEME } from "@src/theme/mantine";
|
|
4
|
+
import type { ReactElement } from "react";
|
|
5
|
+
import { renderToString } from "react-dom/server";
|
|
6
|
+
import { beforeAll, describe, expect, it } from "vitest";
|
|
7
|
+
|
|
8
|
+
beforeAll(() => {
|
|
9
|
+
if (typeof window !== "undefined" && typeof window.matchMedia !== "function") {
|
|
10
|
+
Object.defineProperty(window, "matchMedia", {
|
|
11
|
+
writable: true,
|
|
12
|
+
value: (query: string) => ({
|
|
13
|
+
matches: false,
|
|
14
|
+
media: query,
|
|
15
|
+
onchange: null,
|
|
16
|
+
addListener: () => {},
|
|
17
|
+
removeListener: () => {},
|
|
18
|
+
addEventListener: () => {},
|
|
19
|
+
removeEventListener: () => {},
|
|
20
|
+
dispatchEvent: () => false,
|
|
21
|
+
}),
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
function shell(node: ReactElement): string {
|
|
27
|
+
return renderToString(
|
|
28
|
+
<MantineProvider
|
|
29
|
+
theme={MANTINE_THEME}
|
|
30
|
+
cssVariablesResolver={v8CssVariablesResolver}
|
|
31
|
+
forceColorScheme="dark"
|
|
32
|
+
>
|
|
33
|
+
{node}
|
|
34
|
+
</MantineProvider>,
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function labels(html: string): string[] {
|
|
39
|
+
return [...html.matchAll(/mantine-Tabs-tabLabel">([^<]*)</g)].map((m) => m[1] ?? "");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Tab items whose fenced code sits directly under the tag (no blank-line
|
|
43
|
+
// separators) and contains an interior blank line.
|
|
44
|
+
const FLUSH = `<Tabs groupId="package-manager">
|
|
45
|
+
<TabItem value="esm" label="ESM" default>
|
|
46
|
+
\`\`\`ts
|
|
47
|
+
import { Surreal } from 'surrealdb';
|
|
48
|
+
|
|
49
|
+
// Create a new Surreal instance
|
|
50
|
+
const db = new Surreal();
|
|
51
|
+
\`\`\`
|
|
52
|
+
</TabItem>
|
|
53
|
+
<TabItem value="cjs" label="CommonJS">
|
|
54
|
+
\`\`\`ts
|
|
55
|
+
const { Surreal } = require('surrealdb');
|
|
56
|
+
|
|
57
|
+
// Create a new Surreal instance
|
|
58
|
+
const db = new Surreal();
|
|
59
|
+
\`\`\`
|
|
60
|
+
</TabItem>
|
|
61
|
+
</Tabs>`;
|
|
62
|
+
|
|
63
|
+
// Same content, but the tab items are indented under <Tabs> with tabs.
|
|
64
|
+
const INDENTED = `<Tabs groupId="package-manager">
|
|
65
|
+
\t<TabItem value="esm" label="ESM" default>
|
|
66
|
+
\t\`\`\`ts
|
|
67
|
+
\timport { Surreal } from 'surrealdb';
|
|
68
|
+
|
|
69
|
+
\t// Create a new Surreal instance
|
|
70
|
+
\tconst db = new Surreal();
|
|
71
|
+
\t\`\`\`
|
|
72
|
+
\t</TabItem>
|
|
73
|
+
\t<TabItem value="cjs" label="CommonJS">
|
|
74
|
+
\t\`\`\`ts
|
|
75
|
+
\tconst { Surreal } = require('surrealdb');
|
|
76
|
+
|
|
77
|
+
\t// Create a new Surreal instance
|
|
78
|
+
\tconst db = new Surreal();
|
|
79
|
+
\t\`\`\`
|
|
80
|
+
\t</TabItem>
|
|
81
|
+
</Tabs>`;
|
|
82
|
+
|
|
83
|
+
describe("MarkdownViewer / Tabs with fenced code", () => {
|
|
84
|
+
for (const [name, content] of [
|
|
85
|
+
["flush", FLUSH],
|
|
86
|
+
["indented", INDENTED],
|
|
87
|
+
] as const) {
|
|
88
|
+
it(`${name}: renders both tabs, both labels, and highlighted code`, () => {
|
|
89
|
+
const html = shell(<MarkdownViewer content={content} />);
|
|
90
|
+
const text = html.replace(/<[^>]+>/g, "");
|
|
91
|
+
|
|
92
|
+
// Both tab buttons are present, in order.
|
|
93
|
+
expect(labels(html)).toEqual(["ESM", "CommonJS"]);
|
|
94
|
+
// The fenced code is rendered as a real (highlighted) code block
|
|
95
|
+
// whose tokens survive in the markup.
|
|
96
|
+
expect(html).toContain('data-language="ts"');
|
|
97
|
+
expect(html).toContain("Surreal");
|
|
98
|
+
expect(html).toContain("surrealdb");
|
|
99
|
+
// Tags and fences are not leaked as visible plain text.
|
|
100
|
+
expect(text).not.toContain("</TabItem>");
|
|
101
|
+
expect(text).not.toContain("<TabItem");
|
|
102
|
+
expect(text).not.toContain("```");
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
// A tab item whose content mixes prose paragraphs, blank lines, and a fenced
|
|
108
|
+
// code block — all indented under the tags. `bl` builds the blank separator
|
|
109
|
+
// lines, optionally carrying the structural tab (some editors keep it).
|
|
110
|
+
function mixedItem(bl: "" | "\t"): string {
|
|
111
|
+
return [
|
|
112
|
+
"<Tabs>",
|
|
113
|
+
'\t<TabItem value="esm" label="ESM">',
|
|
114
|
+
bl,
|
|
115
|
+
"\tTest",
|
|
116
|
+
bl,
|
|
117
|
+
"\t```ts",
|
|
118
|
+
"\timport { Surreal } from 'surrealdb';",
|
|
119
|
+
bl,
|
|
120
|
+
"\t// Create a new Surreal instance",
|
|
121
|
+
"\tconst db = new Surreal();",
|
|
122
|
+
"\t```",
|
|
123
|
+
bl,
|
|
124
|
+
"\ttest2",
|
|
125
|
+
bl,
|
|
126
|
+
"\t</TabItem>",
|
|
127
|
+
"</Tabs>",
|
|
128
|
+
].join("\n");
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Mirrors docs' in-memory.mdx: an indented tab item whose runaway fence used to
|
|
132
|
+
// swallow the following flush <Tabs> block whole.
|
|
133
|
+
const TWO_TAB_BLOCKS = [
|
|
134
|
+
"<Tabs>",
|
|
135
|
+
'\t<TabItem value="esm" label="ESM">',
|
|
136
|
+
"",
|
|
137
|
+
"\tTest",
|
|
138
|
+
"",
|
|
139
|
+
"\t```ts",
|
|
140
|
+
"\timport { Surreal } from 'surrealdb';",
|
|
141
|
+
"\t```",
|
|
142
|
+
"",
|
|
143
|
+
"\t</TabItem>",
|
|
144
|
+
"</Tabs>",
|
|
145
|
+
"",
|
|
146
|
+
"<Tabs>",
|
|
147
|
+
"",
|
|
148
|
+
'<TabItem label="SurrealDB 3.x">',
|
|
149
|
+
"",
|
|
150
|
+
"```bash",
|
|
151
|
+
"surreal start mem",
|
|
152
|
+
"```",
|
|
153
|
+
"",
|
|
154
|
+
"</TabItem>",
|
|
155
|
+
"</Tabs>",
|
|
156
|
+
].join("\n");
|
|
157
|
+
|
|
158
|
+
describe("MarkdownViewer / two adjacent Tabs blocks", () => {
|
|
159
|
+
it("renders both blocks independently without one swallowing the next", () => {
|
|
160
|
+
const html = shell(<MarkdownViewer content={TWO_TAB_BLOCKS} />);
|
|
161
|
+
const text = html.replace(/<[^>]+>/g, " ").replace(/\s+/g, " ");
|
|
162
|
+
|
|
163
|
+
// Both tab blocks produced their own labelled tab.
|
|
164
|
+
expect(labels(html)).toEqual(["ESM", "SurrealDB 3.x"]);
|
|
165
|
+
// Content from both blocks is present…
|
|
166
|
+
expect(html).toContain("surrealdb");
|
|
167
|
+
expect(text).toContain("surreal start mem");
|
|
168
|
+
// …and nothing leaked as raw tags or fences.
|
|
169
|
+
expect(text).not.toContain("TabItem");
|
|
170
|
+
expect(text).not.toContain("Tabs");
|
|
171
|
+
expect(text).not.toContain("```");
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
describe("MarkdownViewer / Tabs with mixed indented content", () => {
|
|
176
|
+
for (const blank of ["", "\t"] as const) {
|
|
177
|
+
const label = blank === "" ? "empty blank lines" : "tab-filled blank lines";
|
|
178
|
+
it(`renders prose + code + prose inside an indented tab item (${label})`, () => {
|
|
179
|
+
const html = shell(<MarkdownViewer content={mixedItem(blank)} />);
|
|
180
|
+
const text = html.replace(/<[^>]+>/g, " ").replace(/\s+/g, " ");
|
|
181
|
+
|
|
182
|
+
expect(labels(html)).toEqual(["ESM"]);
|
|
183
|
+
// Prose before and after the code block renders as prose.
|
|
184
|
+
expect(text).toContain("Test");
|
|
185
|
+
expect(text).toContain("test2");
|
|
186
|
+
// The code renders as a highlighted code block, not leaked prose.
|
|
187
|
+
expect(html).toContain('data-language="ts"');
|
|
188
|
+
expect(html).toContain("surrealdb");
|
|
189
|
+
// Nothing is swallowed: tags and fences never surface as text.
|
|
190
|
+
expect(text).not.toContain("TabItem");
|
|
191
|
+
expect(text).not.toContain("```");
|
|
192
|
+
// The code content must not appear as an indented (tab-prefixed) blob.
|
|
193
|
+
expect(text).not.toContain("\timport");
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
});
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { MantineProvider, v8CssVariablesResolver } from "@mantine/core";
|
|
2
|
+
import type { MarkdownComponents } from "@src/lib/markdown";
|
|
3
|
+
import viewerClasses from "@src/lib/markdown/style/viewer.module.scss";
|
|
4
|
+
import { MarkdownViewer } from "@src/primitives/MarkdownViewer";
|
|
5
|
+
import { MANTINE_THEME } from "@src/theme/mantine";
|
|
6
|
+
import { Children, isValidElement, type ReactElement, type ReactNode } from "react";
|
|
7
|
+
import { renderToString } from "react-dom/server";
|
|
8
|
+
import { beforeAll, describe, expect, it } from "vitest";
|
|
9
|
+
|
|
10
|
+
beforeAll(() => {
|
|
11
|
+
if (typeof window !== "undefined" && typeof window.matchMedia !== "function") {
|
|
12
|
+
Object.defineProperty(window, "matchMedia", {
|
|
13
|
+
writable: true,
|
|
14
|
+
value: (query: string) => ({
|
|
15
|
+
matches: false,
|
|
16
|
+
media: query,
|
|
17
|
+
onchange: null,
|
|
18
|
+
addListener: () => {},
|
|
19
|
+
removeListener: () => {},
|
|
20
|
+
addEventListener: () => {},
|
|
21
|
+
removeEventListener: () => {},
|
|
22
|
+
dispatchEvent: () => false,
|
|
23
|
+
}),
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
function shell(node: ReactElement): string {
|
|
29
|
+
return renderToString(
|
|
30
|
+
<MantineProvider
|
|
31
|
+
theme={MANTINE_THEME}
|
|
32
|
+
cssVariablesResolver={v8CssVariablesResolver}
|
|
33
|
+
forceColorScheme="dark"
|
|
34
|
+
>
|
|
35
|
+
{node}
|
|
36
|
+
</MantineProvider>,
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const TABS_MARKDOWN = `<Tabs>
|
|
41
|
+
<TabItem label="JavaScript">
|
|
42
|
+
|
|
43
|
+
\`\`\`javascript
|
|
44
|
+
const x = 42;
|
|
45
|
+
\`\`\`
|
|
46
|
+
|
|
47
|
+
</TabItem>
|
|
48
|
+
<TabItem label="Rust">
|
|
49
|
+
|
|
50
|
+
\`\`\`rust
|
|
51
|
+
fn main() {}
|
|
52
|
+
\`\`\`
|
|
53
|
+
|
|
54
|
+
</TabItem>
|
|
55
|
+
</Tabs>`;
|
|
56
|
+
|
|
57
|
+
describe("MarkdownViewer / Tabs", () => {
|
|
58
|
+
it("renders one tab button per TabItem with the correct labels", () => {
|
|
59
|
+
const html = shell(<MarkdownViewer content={TABS_MARKDOWN} />);
|
|
60
|
+
|
|
61
|
+
const tabButtons = html.match(/mantine-Tabs-tab\b/g) ?? [];
|
|
62
|
+
expect(tabButtons).toHaveLength(2);
|
|
63
|
+
|
|
64
|
+
const labels = [...html.matchAll(/mantine-Tabs-tabLabel">([^<]*)</g)].map((m) => m[1]);
|
|
65
|
+
expect(labels).toEqual(["JavaScript", "Rust"]);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("renders the active tab's content as markdown (code block highlighted)", () => {
|
|
69
|
+
const html = shell(<MarkdownViewer content={TABS_MARKDOWN} />);
|
|
70
|
+
// A real fenced code block is rendered (not raw text).
|
|
71
|
+
expect(html).toContain('data-language="javascript"');
|
|
72
|
+
// The code is highlighted into spans, so the plain text survives stripping tags.
|
|
73
|
+
const text = html.replace(/<[^>]+>/g, "");
|
|
74
|
+
expect(text).toContain("const x = 42");
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("wraps tab sub-content in the shared content-flow class for top-level rhythm", () => {
|
|
78
|
+
const contentFlow = viewerClasses.contentFlow;
|
|
79
|
+
expect(contentFlow).toBeTruthy();
|
|
80
|
+
|
|
81
|
+
const html = shell(<MarkdownViewer content={TABS_MARKDOWN} />);
|
|
82
|
+
|
|
83
|
+
// The class is shared: the viewer root composes it, and the tab body
|
|
84
|
+
// wrapper applies it — so nested content is spaced like top-level content.
|
|
85
|
+
const occurrences = html.split(contentFlow).length - 1;
|
|
86
|
+
expect(occurrences).toBeGreaterThanOrEqual(2);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("exposes bare child elements with their props for custom introspecting components", () => {
|
|
90
|
+
// Mirrors how docs' <ContentTabs> reads each child's label/content via
|
|
91
|
+
// React.Children — the children must be the bare TabItem elements.
|
|
92
|
+
function Group({ children }: { children?: ReactNode }): ReactElement {
|
|
93
|
+
const tabs = Children.toArray(children)
|
|
94
|
+
.filter(isValidElement)
|
|
95
|
+
.map((el) => (el.props as { label?: string }).label ?? "MISSING");
|
|
96
|
+
return <div data-labels={tabs.join("|")} />;
|
|
97
|
+
}
|
|
98
|
+
function Item({ children }: { children?: ReactNode }): ReactElement {
|
|
99
|
+
return <>{children}</>;
|
|
100
|
+
}
|
|
101
|
+
const components: MarkdownComponents = {
|
|
102
|
+
Group: { component: Group, block: true },
|
|
103
|
+
Item: { component: Item, block: true },
|
|
104
|
+
};
|
|
105
|
+
const html = shell(
|
|
106
|
+
<MarkdownViewer
|
|
107
|
+
content={
|
|
108
|
+
'<Group>\n<Item label="One">\n\na\n\n</Item>\n<Item label="Two">\n\nb\n\n</Item>\n</Group>'
|
|
109
|
+
}
|
|
110
|
+
components={components}
|
|
111
|
+
/>,
|
|
112
|
+
);
|
|
113
|
+
expect(html).toContain('data-labels="One|Two"');
|
|
114
|
+
});
|
|
115
|
+
});
|