@surrealdb/ui 1.2.2 → 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/.picto-fix-png/Cursor.png +0 -0
- package/.picto-fix-png/DocumentGradient.png +0 -0
- package/.picto-fix-png/FileGradient.png +0 -0
- package/.picto-fix-png/HealthBubbleGradient.png +0 -0
- package/.picto-fix-png/LearnGradient.png +0 -0
- package/.picto-fix-png/MediaFileGradient.png +0 -0
- package/README.md +16 -0
- package/REVIEW.md +2 -2
- package/dist/assets/00f479574770406b.webp +0 -0
- package/dist/assets/3e3150054c482d19.webp +0 -0
- package/dist/assets/4ab8aa6d05f81b2d.webp +0 -0
- package/dist/assets/9af7119697945bfe.webp +0 -0
- package/dist/assets/b8c39a75a6dfb50a.webp +0 -0
- package/dist/assets/b9123e22ce5345b5.webp +0 -0
- package/dist/assets/bb014a59e501b200.webp +0 -0
- package/dist/assets/bffb52641c4c3861.webp +0 -0
- package/dist/assets/ca65cd02fa429a01.webp +0 -0
- package/dist/assets/d95b9e2207cd1bbb.webp +0 -0
- package/dist/assets/e58704a124b17f9c.webp +0 -0
- package/dist/assets/e731247869f3839b.webp +0 -0
- package/dist/assets/f4cdd6dcd9f393b8.webp +0 -0
- package/dist/assets/fd4bfe9daca141f1.webp +0 -0
- package/dist/ui.css +1 -1
- package/dist/ui.d.ts +285 -110
- package/dist/ui.js +7036 -6280
- 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
- package/tools/duplicate-vector-mappings.json +79 -0
- package/tools/glass-export-overrides.json +8 -0
- package/tools/integration-gradient-exports.json +14 -1
- package/tools/picto-sync.ts +2 -1
- package/dist/assets/b5cb190d5047855e.webp +0 -0
|
@@ -0,0 +1,88 @@
|
|
|
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(/"/g, '"')
|
|
82
|
+
.replace(/'/g, "'")
|
|
83
|
+
.replace(/</g, "<")
|
|
84
|
+
.replace(/>/g, ">")
|
|
85
|
+
.replace(/&/g, "&")
|
|
86
|
+
.replace(/\s+/g, " ")
|
|
87
|
+
.trim();
|
|
88
|
+
}
|
|
@@ -7,10 +7,8 @@ import {
|
|
|
7
7
|
markdownModeField,
|
|
8
8
|
setMarkdownMode,
|
|
9
9
|
} from "@src/lib/markdown";
|
|
10
|
-
import { Callout } from "@src/lib/markdown/editor/callout";
|
|
11
10
|
import { markdownDecorations } from "@src/lib/markdown/editor/decorations";
|
|
12
11
|
import { setMarkdownEditorFocused } from "@src/lib/markdown/editor/decorations/editor-focus";
|
|
13
|
-
import { createMarkdownLanguage } from "@src/lib/markdown/editor/language";
|
|
14
12
|
import { CalloutHeaderWidget } from "@src/lib/markdown/editor/widgets/callout-header-widget";
|
|
15
13
|
import { CheckboxWidget } from "@src/lib/markdown/editor/widgets/checkbox-widget";
|
|
16
14
|
import {
|
|
@@ -22,6 +20,8 @@ import { HeadingFoldWidget } from "@src/lib/markdown/editor/widgets/heading-fold
|
|
|
22
20
|
import { ListBulletWidget } from "@src/lib/markdown/editor/widgets/list-bullet-widget";
|
|
23
21
|
import { createWidgetStore } from "@src/lib/markdown/editor/widgets/store";
|
|
24
22
|
import { TableWidget } from "@src/lib/markdown/editor/widgets/table-widget";
|
|
23
|
+
import { Callout } from "@src/lib/markdown/grammar/callout";
|
|
24
|
+
import { createMarkdownLanguage } from "@src/lib/markdown/grammar/language";
|
|
25
25
|
import type { CodeRenderer } from "@src/lib/markdown/types";
|
|
26
26
|
import { describe, expect, it } from "vitest";
|
|
27
27
|
import { md } from "../../_setup/markdown-classes";
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { markdownSourceFromString, parseMarkdownTree } from "@src/lib/markdown";
|
|
2
|
+
import { readCodeText } from "@src/lib/markdown/tree/code-node";
|
|
3
|
+
import { describe, expect, it } from "vitest";
|
|
4
|
+
|
|
5
|
+
/** Reconstructed body text of the (single) FencedCode node. */
|
|
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;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Top-level node names of a parsed document. */
|
|
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;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** The source text of the (single) FencedCode node, or null if none. */
|
|
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;
|
|
42
|
+
}
|
|
43
|
+
|
|
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
|
+
});
|
|
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
|
+
});
|
|
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
|
+
});
|
|
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
|
+
});
|
|
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
|
+
});
|
|
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
|
+
});
|
|
103
|
+
});
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { EditorSelection, EditorState } from "@codemirror/state";
|
|
2
|
+
import { type Decoration, type DecorationSet, EditorView } from "@codemirror/view";
|
|
3
|
+
import { markdownModeField, setMarkdownMode } from "@src/lib/markdown";
|
|
4
|
+
import { markdownDecorations } from "@src/lib/markdown/editor/decorations";
|
|
5
|
+
import { setMarkdownEditorFocused } from "@src/lib/markdown/editor/decorations/editor-focus";
|
|
6
|
+
import { JsxBlockWidget } from "@src/lib/markdown/editor/widgets/jsx-widget";
|
|
7
|
+
import { createWidgetStore } from "@src/lib/markdown/editor/widgets/store";
|
|
8
|
+
import { Callout } from "@src/lib/markdown/grammar/callout";
|
|
9
|
+
import { createMarkdownLanguage } from "@src/lib/markdown/grammar/language";
|
|
10
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
11
|
+
|
|
12
|
+
const views: EditorView[] = [];
|
|
13
|
+
|
|
14
|
+
afterEach(() => {
|
|
15
|
+
for (const v of views.splice(0)) v.destroy();
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
/** Build a focused, preview-mode editor with the caret at `cursor`. */
|
|
19
|
+
function preview(doc: string, cursor: number): EditorView {
|
|
20
|
+
const state = EditorState.create({
|
|
21
|
+
doc,
|
|
22
|
+
extensions: [
|
|
23
|
+
createMarkdownLanguage({ extensions: [Callout] }),
|
|
24
|
+
markdownModeField,
|
|
25
|
+
markdownDecorations({ widgetStore: createWidgetStore() }),
|
|
26
|
+
],
|
|
27
|
+
});
|
|
28
|
+
const parent = document.createElement("div");
|
|
29
|
+
document.body.appendChild(parent);
|
|
30
|
+
const view = new EditorView({ state, parent });
|
|
31
|
+
views.push(view);
|
|
32
|
+
view.dispatch({ effects: setMarkdownMode.of("preview") });
|
|
33
|
+
view.focus();
|
|
34
|
+
view.dispatch({ effects: setMarkdownEditorFocused.of(true) });
|
|
35
|
+
view.dispatch({ selection: EditorSelection.cursor(cursor) });
|
|
36
|
+
return view;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function jsxBlockRanges(view: EditorView): Array<{ from: number; to: number }> {
|
|
40
|
+
const out: Array<{ from: number; to: number }> = [];
|
|
41
|
+
for (const set of (view as unknown as { docView: { decorations: DecorationSet[] } }).docView
|
|
42
|
+
.decorations) {
|
|
43
|
+
const cursor = set.iter();
|
|
44
|
+
while (cursor.value) {
|
|
45
|
+
const widget = (cursor.value.spec as { widget?: Decoration }).widget;
|
|
46
|
+
if (widget instanceof JsxBlockWidget) {
|
|
47
|
+
out.push({ from: cursor.from, to: cursor.to });
|
|
48
|
+
}
|
|
49
|
+
cursor.next();
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return out;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const TABS = `<Tabs syncKey="demo">
|
|
56
|
+
<TabItem label="A">
|
|
57
|
+
Alpha para one.
|
|
58
|
+
|
|
59
|
+
Alpha para two.
|
|
60
|
+
</TabItem>
|
|
61
|
+
<TabItem label="B">
|
|
62
|
+
Beta para one.
|
|
63
|
+
</TabItem>
|
|
64
|
+
</Tabs>`;
|
|
65
|
+
|
|
66
|
+
// Some prose before the block so the caret can sit fully outside it.
|
|
67
|
+
const TABS_WITH_PROSE = `Intro line.\n\n${TABS}`;
|
|
68
|
+
|
|
69
|
+
describe("MarkdownEditor / JSX block widget", () => {
|
|
70
|
+
it("renders a multi-paragraph Tabs block as a single widget when the caret is outside", () => {
|
|
71
|
+
const view = preview(TABS_WITH_PROSE, 0); // caret in the intro line
|
|
72
|
+
const ranges = jsxBlockRanges(view);
|
|
73
|
+
expect(ranges).toHaveLength(1);
|
|
74
|
+
// Covers the whole <Tabs>…</Tabs> span, not a fragment of it.
|
|
75
|
+
const tabsStart = TABS_WITH_PROSE.indexOf("<Tabs");
|
|
76
|
+
expect(ranges[0]?.from).toBe(tabsStart);
|
|
77
|
+
expect(ranges[0]?.to).toBe(TABS_WITH_PROSE.length);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("reveals the entire block as source (no partial widgets) when the caret is inside", () => {
|
|
81
|
+
// Caret inside the first tab's content.
|
|
82
|
+
const inside = TABS_WITH_PROSE.indexOf("Alpha para one.") + 3;
|
|
83
|
+
const view = preview(TABS_WITH_PROSE, inside);
|
|
84
|
+
expect(jsxBlockRanges(view)).toHaveLength(0);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("keeps nested TabItem tags raw when the caret is on the closing tag line", () => {
|
|
88
|
+
const onClose = TABS_WITH_PROSE.indexOf("</Tabs>") + 1;
|
|
89
|
+
const view = preview(TABS_WITH_PROSE, onClose);
|
|
90
|
+
expect(jsxBlockRanges(view)).toHaveLength(0);
|
|
91
|
+
});
|
|
92
|
+
});
|
|
@@ -3,8 +3,8 @@ import {
|
|
|
3
3
|
JSX_ATTR_STRING_NODE,
|
|
4
4
|
JSX_EXPRESSION_CONTENT_NODE,
|
|
5
5
|
JSX_EXPRESSION_MARK_NODE,
|
|
6
|
-
} from "@src/lib/markdown/
|
|
7
|
-
import { createMarkdownLanguage } from "@src/lib/markdown/
|
|
6
|
+
} from "@src/lib/markdown/grammar/jsx-tag";
|
|
7
|
+
import { createMarkdownLanguage } from "@src/lib/markdown/grammar/language";
|
|
8
8
|
import { describe, expect, it } from "vitest";
|
|
9
9
|
|
|
10
10
|
function parse(text: string) {
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { EditorSelection, EditorState } from "@codemirror/state";
|
|
2
2
|
import { EditorView } from "@codemirror/view";
|
|
3
|
-
import { Callout } from "@src/lib/markdown/editor/callout";
|
|
4
|
-
import { createMarkdownLanguage } from "@src/lib/markdown/editor/language";
|
|
5
3
|
import { indentMarkdownList } from "@src/lib/markdown/editor/list-indent";
|
|
4
|
+
import { Callout } from "@src/lib/markdown/grammar/callout";
|
|
5
|
+
import { createMarkdownLanguage } from "@src/lib/markdown/grammar/language";
|
|
6
6
|
import { afterEach, describe, expect, it } from "vitest";
|
|
7
7
|
|
|
8
8
|
let view: EditorView;
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { EditorSelection, EditorState } from "@codemirror/state";
|
|
2
2
|
import { EditorView } from "@codemirror/view";
|
|
3
|
-
import { Callout } from "@src/lib/markdown/editor/callout";
|
|
4
|
-
import { createMarkdownLanguage } from "@src/lib/markdown/editor/language";
|
|
5
3
|
import { DEFAULT_SLASH_COMMANDS } from "@src/lib/markdown/editor/slash/commands";
|
|
6
4
|
import { slashDecorationsField } from "@src/lib/markdown/editor/slash/decorations";
|
|
7
5
|
import { getSlashSession, slashSessionField } from "@src/lib/markdown/editor/slash/field";
|
|
6
|
+
import { Callout } from "@src/lib/markdown/grammar/callout";
|
|
7
|
+
import { createMarkdownLanguage } from "@src/lib/markdown/grammar/language";
|
|
8
8
|
import { describe, expect, it } from "vitest";
|
|
9
9
|
|
|
10
10
|
function createView(doc: string, cursor = doc.length): EditorView {
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { EditorSelection, EditorState } from "@codemirror/state";
|
|
2
2
|
import { EditorView, runScopeHandlers } from "@codemirror/view";
|
|
3
|
-
import { Callout } from "@src/lib/markdown/editor/callout";
|
|
4
|
-
import { createMarkdownLanguage } from "@src/lib/markdown/editor/language";
|
|
5
3
|
import { markdownPairKeymap, markdownSnippetHandler } from "@src/lib/markdown/editor/snippets";
|
|
4
|
+
import { Callout } from "@src/lib/markdown/grammar/callout";
|
|
5
|
+
import { createMarkdownLanguage } from "@src/lib/markdown/grammar/language";
|
|
6
6
|
import { describe, expect, it } from "vitest";
|
|
7
7
|
|
|
8
8
|
function createView(doc: string, cursor?: number | { anchor: number; head: number }): EditorView {
|
|
@@ -0,0 +1,60 @@
|
|
|
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
|
+
describe("MarkdownViewer / callouts", () => {
|
|
39
|
+
it("renders the title and body without a leading <br> between them", () => {
|
|
40
|
+
const html = shell(
|
|
41
|
+
<MarkdownViewer
|
|
42
|
+
content={"> [!NOTE]\n> Sandbox data is **not persistent**: it is reset later."}
|
|
43
|
+
/>,
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
expect(html).toContain("Note");
|
|
47
|
+
expect(html).toContain("Sandbox data is");
|
|
48
|
+
expect(html).toContain("not persistent");
|
|
49
|
+
|
|
50
|
+
// The body paragraph must not open with a stray line break.
|
|
51
|
+
expect(html).not.toMatch(/<p[^>]*><br\s*\/?>/);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("supports the title and body on adjacent lines", () => {
|
|
55
|
+
const html = shell(<MarkdownViewer content={"> [!WARNING]\n> Be careful here."} />);
|
|
56
|
+
expect(html).toContain("Warning");
|
|
57
|
+
expect(html).toContain("Be careful here.");
|
|
58
|
+
expect(html).not.toMatch(/<p[^>]*><br\s*\/?>/);
|
|
59
|
+
});
|
|
60
|
+
});
|
|
@@ -0,0 +1,61 @@
|
|
|
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
|
+
// Dark-scheme syntax colors (see src/syntax/theme.ts).
|
|
39
|
+
const STRING_COLOR = "#00ff6e";
|
|
40
|
+
const PROPERTY_COLOR = "#e06c75";
|
|
41
|
+
|
|
42
|
+
describe("MarkdownViewer / code syntax highlighting", () => {
|
|
43
|
+
it("highlights object keys as property names (not bare identifiers)", () => {
|
|
44
|
+
const code = ["{", ' "monthly_sales": "DEFINE TABLE monthly_sales TYPE NORMAL"', "}"].join(
|
|
45
|
+
"\n",
|
|
46
|
+
);
|
|
47
|
+
const html = shell(<MarkdownViewer content={`\`\`\`json\n${code}\n\`\`\``} />);
|
|
48
|
+
expect(html).toContain(PROPERTY_COLOR);
|
|
49
|
+
expect(html).toContain(STRING_COLOR);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("keeps a multi-line string token highlighted as one string across lines", () => {
|
|
53
|
+
const code = ["const q = `DEFINE TABLE wishlist", "SCHEMAFULL PERMISSIONS NONE`;"].join(
|
|
54
|
+
"\n",
|
|
55
|
+
);
|
|
56
|
+
const html = shell(<MarkdownViewer content={`\`\`\`js\n${code}\n\`\`\``} />);
|
|
57
|
+
// Continuation line content must be inside a string-colored span — proof
|
|
58
|
+
// the whole block was parsed once rather than line by line.
|
|
59
|
+
expect(html).toMatch(new RegExp(`color:${STRING_COLOR}[^>]*>SCHEMAFULL PERMISSIONS NONE`));
|
|
60
|
+
});
|
|
61
|
+
});
|
|
@@ -100,11 +100,65 @@ describe("MarkdownViewer / features", () => {
|
|
|
100
100
|
expect(html).toContain("<code");
|
|
101
101
|
});
|
|
102
102
|
|
|
103
|
+
it("wraps a GFM table in a horizontal scroll container", () => {
|
|
104
|
+
const html = shell(
|
|
105
|
+
<MarkdownViewer
|
|
106
|
+
content={`| A | B |
|
|
107
|
+
| - | - |
|
|
108
|
+
| 1 | 2 |`}
|
|
109
|
+
/>,
|
|
110
|
+
);
|
|
111
|
+
expect(html).toContain("mantine-TableScrollContainer-scrollContainer");
|
|
112
|
+
expect(html).toContain("<table");
|
|
113
|
+
expect(html).toContain("1");
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it("wraps an HTML table in a horizontal scroll container", () => {
|
|
117
|
+
const html = shell(
|
|
118
|
+
<MarkdownViewer
|
|
119
|
+
content={`<table>
|
|
120
|
+
<tbody><tr><td>cell</td></tr></tbody>
|
|
121
|
+
</table>`}
|
|
122
|
+
/>,
|
|
123
|
+
);
|
|
124
|
+
expect(html).toContain("mantine-TableScrollContainer-scrollContainer");
|
|
125
|
+
expect(html).toContain("<table");
|
|
126
|
+
expect(html).toContain("cell");
|
|
127
|
+
});
|
|
128
|
+
|
|
103
129
|
it("renders default Button component", () => {
|
|
104
130
|
const html = shell(
|
|
105
|
-
<MarkdownViewer content={'<Button label="Go"
|
|
131
|
+
<MarkdownViewer content={'<Button label="Go" href="https://surrealdb.com" />'} />,
|
|
106
132
|
);
|
|
107
133
|
expect(html).toContain("Go");
|
|
108
134
|
expect(html).toContain('href="https://surrealdb.com"');
|
|
109
135
|
});
|
|
136
|
+
|
|
137
|
+
it("resolves image sources through onImage (e.g. CDN code → URL)", () => {
|
|
138
|
+
const html = shell(
|
|
139
|
+
<MarkdownViewer
|
|
140
|
+
content={""}
|
|
141
|
+
onResolveMedia={(node) => ({
|
|
142
|
+
...node,
|
|
143
|
+
src: `https://cdn.surrealdb.com/${node.src}.auto`,
|
|
144
|
+
})}
|
|
145
|
+
/>,
|
|
146
|
+
);
|
|
147
|
+
expect(html).toContain("cdn.surrealdb.com/cdn-code-123.auto");
|
|
148
|
+
expect(html).toContain("Example");
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it("renders a video player when onImage resolves a source to a video container", () => {
|
|
152
|
+
const html = shell(
|
|
153
|
+
<MarkdownViewer
|
|
154
|
+
content={""}
|
|
155
|
+
onResolveMedia={(node) => ({
|
|
156
|
+
...node,
|
|
157
|
+
src: `https://cdn.surrealdb.com/${node.src}.mp4`,
|
|
158
|
+
})}
|
|
159
|
+
/>,
|
|
160
|
+
);
|
|
161
|
+
expect(html).toContain("cdn.surrealdb.com/cdn-video-123.mp4");
|
|
162
|
+
expect(html).toContain("<video");
|
|
163
|
+
});
|
|
110
164
|
});
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { MantineProvider, v8CssVariablesResolver } from "@mantine/core";
|
|
2
|
+
import type { MarkdownComponents } from "@src/lib/markdown";
|
|
3
|
+
import { parseMarkdownTree } from "@src/lib/markdown";
|
|
4
|
+
import { MarkdownViewer } from "@src/primitives/MarkdownViewer";
|
|
5
|
+
import { MANTINE_THEME } from "@src/theme/mantine";
|
|
6
|
+
import type { ReactElement, 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
|
+
function nodeNames(text: string): string[] {
|
|
41
|
+
const tree = parseMarkdownTree(text);
|
|
42
|
+
const names: string[] = [];
|
|
43
|
+
const cursor = tree.cursor();
|
|
44
|
+
do {
|
|
45
|
+
names.push(cursor.name);
|
|
46
|
+
} while (cursor.next());
|
|
47
|
+
return names;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function Boxes({ children }: { children?: ReactNode }): ReactElement {
|
|
51
|
+
return <div data-boxes>{children}</div>;
|
|
52
|
+
}
|
|
53
|
+
function IconBox({ title }: { title?: string }): ReactElement {
|
|
54
|
+
return <div data-iconbox>{title}</div>;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const components: MarkdownComponents = {
|
|
58
|
+
Boxes: { component: Boxes, block: true },
|
|
59
|
+
IconBox: { component: IconBox, block: true },
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
// Children indented with tabs, exactly as authored in docs' index.mdx.
|
|
63
|
+
const CONTENT = `<Boxes>
|
|
64
|
+
\t<IconBox
|
|
65
|
+
\t\ttitle="Querying"
|
|
66
|
+
\t\tdescription="Learn to query your data."
|
|
67
|
+
\t\thref="/docs/learn/querying"
|
|
68
|
+
\t\ticon="@ui/pictoSpeed"
|
|
69
|
+
\t/>
|
|
70
|
+
\t<IconBox
|
|
71
|
+
\t\ttitle="Schema management"
|
|
72
|
+
\t\tdescription="Learn how to shape data."
|
|
73
|
+
\t\thref="/docs/learn/schema-management"
|
|
74
|
+
\t\ticon="@ui/pictoPath"
|
|
75
|
+
\t/>
|
|
76
|
+
</Boxes>`;
|
|
77
|
+
|
|
78
|
+
describe("MarkdownViewer / no indented code blocks", () => {
|
|
79
|
+
it("does not parse tab/4-space indented lines as code blocks", () => {
|
|
80
|
+
expect(nodeNames("\t<IconBox />")).not.toContain("CodeBlock");
|
|
81
|
+
expect(nodeNames(" plain indented text")).not.toContain("CodeBlock");
|
|
82
|
+
// Fenced code blocks still work.
|
|
83
|
+
expect(nodeNames("```\ncode\n```")).toContain("FencedCode");
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("renders tab-indented IconBox children inside a Boxes block", () => {
|
|
87
|
+
const html = shell(
|
|
88
|
+
<MarkdownViewer
|
|
89
|
+
content={CONTENT}
|
|
90
|
+
components={components}
|
|
91
|
+
/>,
|
|
92
|
+
);
|
|
93
|
+
expect(html).toContain("data-boxes");
|
|
94
|
+
expect(html).toContain("data-iconbox");
|
|
95
|
+
expect(html).toContain("Querying");
|
|
96
|
+
expect(html).toContain("Schema management");
|
|
97
|
+
});
|
|
98
|
+
});
|