@surrealdb/ui 1.1.1 → 1.2.1
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/.zed/settings.json +36 -0
- package/AGENTS.md +10 -5
- package/README.md +30 -0
- package/REVIEW.md +1 -1
- package/dist/assets/0d2c2f665b0f41ed.woff2 +0 -0
- package/dist/assets/12b57c6beacdbca0.woff2 +0 -0
- package/dist/assets/23645aad5ccc2b92.woff2 +0 -0
- package/dist/assets/8bbfa6e01a9e6a0f.woff2 +0 -0
- package/dist/assets/93fc40a807be6880.woff2 +0 -0
- package/dist/assets/9c9751ca111e97c2.woff2 +0 -0
- package/dist/assets/9ff55a8a9670220d.woff2 +0 -0
- package/dist/assets/a865edea076e0166.woff2 +0 -0
- package/dist/assets/b921df26851c5aca.woff2 +0 -0
- package/dist/assets/c6a3f4e555097159.woff2 +0 -0
- package/dist/assets/c6c31cb1350b2544.woff2 +0 -0
- package/dist/fonts.css +1 -0
- package/dist/fonts.js +2 -0
- package/dist/fonts.js.map +1 -0
- package/dist/ui.css +1 -1
- package/dist/ui.d.ts +537 -523
- package/dist/ui.js +16328 -14682
- package/dist/ui.js.map +1 -1
- package/package.json +22 -24
- package/tests/_setup/e2e-helpers.tsx +169 -0
- package/tests/_setup/markdown-classes.ts +3 -0
- package/tests/_setup/portable-stories.ts +10 -0
- 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
- package/tests/e2e/MarkdownEditor/__screenshots__/content-blocks.test.tsx/MarkdownEditor---content-blocks-renders-a-diagram-for-the-fenced-mermaid-block-1.png +0 -0
- package/tests/e2e/MarkdownEditor/__screenshots__/content-blocks.test.tsx/MarkdownEditor---content-blocks-renders-a-horizontal-rule-as-a-separator-1.png +0 -0
- 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
- package/tests/e2e/MarkdownEditor/__screenshots__/content-blocks.test.tsx/MarkdownEditor---content-blocks-shows-blockquote-text-from-the-sample-document-1.png +0 -0
- package/tests/e2e/MarkdownEditor/__screenshots__/content-blocks.test.tsx/MarkdownEditor---content-blocks-shows-fenced-TypeScript-sample-code-in-the-document-1.png +0 -0
- 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
- 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
- package/tests/e2e/MarkdownEditor/__screenshots__/edits.test.tsx/MarkdownEditor---edits-reflects-typed-characters-in-the-underlying-document-1.png +0 -0
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- package/tests/e2e/MarkdownEditor/__screenshots__/media-sizing.test.tsx/MarkdownEditor---media-sizing-matches-MarkdownViewer-image-width-in-the-split-playground-1.png +0 -0
- package/tests/e2e/MarkdownEditor/__screenshots__/modes.test.tsx/MarkdownEditor---modes-standalone-MarkdownViewer-is-not-a-CodeMirror-surface-1.png +0 -0
- package/tests/e2e/MarkdownEditor/__screenshots__/modes.test.tsx/MarkdownEditor---modes-toggling-the-SegmentedControl-swaps-mode-without-remounting-the-editor-1.png +0 -0
- package/tests/e2e/MarkdownEditor/__screenshots__/regressions.test.tsx/MarkdownEditor---regressions-repeated-checkbox-toggles-do-not-duplicate-or-drift-task-markers-1.png +0 -0
- 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
- 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
- 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
- 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
- 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
- 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
- package/tests/e2e/MarkdownEditor/__screenshots__/undo-redo.test.tsx/MarkdownEditor---undo-and-redo-restores-undone-document-edits-via-redo-1.png +0 -0
- package/tests/e2e/MarkdownEditor/content-blocks.test.tsx +152 -0
- package/tests/e2e/MarkdownEditor/edits.test.tsx +111 -0
- package/tests/e2e/MarkdownEditor/heading-fold.test.tsx +44 -0
- package/tests/e2e/MarkdownEditor/hybrid-widgets.test.tsx +192 -0
- package/tests/e2e/MarkdownEditor/jsx-block-content.test.tsx +242 -0
- package/tests/e2e/MarkdownEditor/jsx-highlight.test.tsx +68 -0
- package/tests/e2e/MarkdownEditor/jsx-inline-badges.test.tsx +59 -0
- package/tests/e2e/MarkdownEditor/jsx-selection.test.tsx +43 -0
- package/tests/e2e/MarkdownEditor/link-placeholder.test.tsx +67 -0
- package/tests/e2e/MarkdownEditor/media-align.test.tsx +57 -0
- package/tests/e2e/MarkdownEditor/media-edit.test.tsx +63 -0
- package/tests/e2e/MarkdownEditor/media-sizing.test.tsx +123 -0
- package/tests/e2e/MarkdownEditor/modes.test.tsx +93 -0
- package/tests/e2e/MarkdownEditor/regressions.test.tsx +182 -0
- package/tests/e2e/MarkdownEditor/slash-commands.test.tsx +99 -0
- package/tests/e2e/MarkdownEditor/table-click.test.tsx +47 -0
- package/tests/e2e/MarkdownEditor/table-controls.test.tsx +56 -0
- package/tests/e2e/MarkdownEditor/table-format.test.tsx +41 -0
- package/tests/e2e/MarkdownEditor/undo-redo.test.tsx +38 -0
- package/tests/e2e/MarkdownViewer/__screenshots__/parity.test.tsx/MarkdownViewer---editor-parity-matches-computed-font-size-on-first-heading-1.png +0 -0
- package/tests/e2e/MarkdownViewer/__screenshots__/parity.test.tsx/MarkdownViewer---editor-parity-matches-counts-for-shared-structural-classes-1.png +0 -0
- package/tests/e2e/MarkdownViewer/__screenshots__/parity.test.tsx/MarkdownViewer---editor-parity-matches-visible-text-between-preview-editor--blurred--and-MarkdownViewer-1.png +0 -0
- package/tests/e2e/MarkdownViewer/__screenshots__/render.test.tsx/MarkdownViewer---render-exercises-shared-preview-class-names-without-mounting-CodeMirror-1.png +0 -0
- package/tests/e2e/MarkdownViewer/parity.test.tsx +190 -0
- package/tests/e2e/MarkdownViewer/render.test.tsx +35 -0
- package/tests/unit/Editor/helpers.test.ts +42 -0
- package/tests/unit/MarkdownEditor/code-info.test.ts +63 -0
- package/tests/unit/MarkdownEditor/decorations.test.ts +488 -0
- package/tests/unit/MarkdownEditor/editor-ready.test.ts +36 -0
- package/tests/unit/MarkdownEditor/html-descriptors.test.ts +94 -0
- package/tests/unit/MarkdownEditor/jsx-attr-scan.test.ts +115 -0
- package/tests/unit/MarkdownEditor/jsx-tag-grammar.test.ts +88 -0
- package/tests/unit/MarkdownEditor/list-indent.test.ts +95 -0
- package/tests/unit/MarkdownEditor/slash-commands.test.ts +213 -0
- package/tests/unit/MarkdownEditor/table-format.test.ts +83 -0
- package/tests/unit/MarkdownEditor/table.test.ts +119 -0
- package/tests/unit/MarkdownEditor/triggers.test.ts +244 -0
- package/tests/unit/MarkdownEditor/widget-store.test.ts +105 -0
- package/tests/unit/MarkdownViewer/code-title.test.tsx +62 -0
- package/tests/unit/MarkdownViewer/features.test.tsx +110 -0
- package/tests/unit/MarkdownViewer/headings.test.tsx +40 -0
- package/tests/unit/MarkdownViewer/jsx.test.tsx +211 -0
- package/tests/unit/MarkdownViewer/list-bullets.test.tsx +49 -0
- package/tests/unit/MarkdownViewer/list-code.test.tsx +65 -0
- package/tests/unit/MarkdownViewer/renderers.test.tsx +79 -0
- package/tests/unit/MarkdownViewer/runnable.test.tsx +69 -0
- package/tests/unit/MarkdownViewer/ssr.test.tsx +93 -0
- package/dist/yoopta.css +0 -1
- /package/dist/{yoopta.d.ts → fonts.d.ts} +0 -0
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import { MantineProvider, v8CssVariablesResolver } from "@mantine/core";
|
|
2
|
+
import { extractHeadings, markdownSourceFromString, parseMarkdownTree } from "@src/lib/markdown";
|
|
3
|
+
import { MarkdownViewer } from "@src/primitives/MarkdownViewer";
|
|
4
|
+
import { MANTINE_THEME } from "@src/theme/mantine";
|
|
5
|
+
import type { ReactElement } from "react";
|
|
6
|
+
import { renderToString } from "react-dom/server";
|
|
7
|
+
import { beforeAll, describe, expect, it } from "vitest";
|
|
8
|
+
|
|
9
|
+
beforeAll(() => {
|
|
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
|
+
|
|
27
|
+
function shell(node: ReactElement): string {
|
|
28
|
+
return renderToString(
|
|
29
|
+
<MantineProvider
|
|
30
|
+
theme={MANTINE_THEME}
|
|
31
|
+
cssVariablesResolver={v8CssVariablesResolver}
|
|
32
|
+
forceColorScheme="dark"
|
|
33
|
+
>
|
|
34
|
+
{node}
|
|
35
|
+
</MantineProvider>,
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
describe("MarkdownViewer / JSX and HTML", () => {
|
|
40
|
+
it("renders a JSX component when jsxMode is render", () => {
|
|
41
|
+
const html = shell(<MarkdownViewer content={'<Since v="2.0.0" />'} />);
|
|
42
|
+
expect(html).toContain("2.0.0");
|
|
43
|
+
expect(html).not.toContain("<Since");
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("shows graceful fallback when jsxMode is graceful and component is missing", () => {
|
|
47
|
+
const html = shell(
|
|
48
|
+
<MarkdownViewer
|
|
49
|
+
content="<UnknownComponent />"
|
|
50
|
+
jsxMode="graceful"
|
|
51
|
+
/>,
|
|
52
|
+
);
|
|
53
|
+
expect(html).toContain("unsupported JSX");
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("parses JSON attribute values on JSX components", () => {
|
|
57
|
+
function ValueEcho({ value }: { value: unknown }) {
|
|
58
|
+
return <span data-value={JSON.stringify(value)} />;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const html = shell(
|
|
62
|
+
<MarkdownViewer
|
|
63
|
+
content={"<ValueEcho value={42} />"}
|
|
64
|
+
jsxMode="render"
|
|
65
|
+
components={{ ValueEcho }}
|
|
66
|
+
/>,
|
|
67
|
+
);
|
|
68
|
+
expect(html).toContain("data-value");
|
|
69
|
+
expect(html).toContain("42");
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("leaves inline brace text as plain text", () => {
|
|
73
|
+
const html = shell(<MarkdownViewer content="Count: {count}" />);
|
|
74
|
+
expect(html).toContain("{count}");
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("renders details/summary HTML blocks", () => {
|
|
78
|
+
const html = shell(
|
|
79
|
+
<MarkdownViewer
|
|
80
|
+
content={`<details>
|
|
81
|
+
<summary>Toggle</summary>
|
|
82
|
+
|
|
83
|
+
Hidden **bold** text
|
|
84
|
+
|
|
85
|
+
</details>`}
|
|
86
|
+
jsxMode="render"
|
|
87
|
+
/>,
|
|
88
|
+
);
|
|
89
|
+
expect(html).toContain("Toggle");
|
|
90
|
+
expect(html).toContain("bold");
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("extractHeadings returns h2 and h3 slugs", () => {
|
|
94
|
+
const source = "## Section\n\n### Sub\n";
|
|
95
|
+
const tree = parseMarkdownTree(source);
|
|
96
|
+
const headings = extractHeadings(tree, markdownSourceFromString(source));
|
|
97
|
+
expect(headings).toHaveLength(2);
|
|
98
|
+
expect(headings[0]?.text).toBe("Section");
|
|
99
|
+
expect(headings[1]?.text).toBe("Sub");
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("onImage rewrites image src", () => {
|
|
103
|
+
const html = shell(
|
|
104
|
+
<MarkdownViewer
|
|
105
|
+
content='<img src="@ui/pictoAI" />'
|
|
106
|
+
jsxMode="render"
|
|
107
|
+
onImage={(node) => ({ ...node, src: "https://example.com/resolved.png" })}
|
|
108
|
+
/>,
|
|
109
|
+
);
|
|
110
|
+
expect(html).toContain("https://example.com/resolved.png");
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("renders inline paired HTML anchor without duplicating link text", () => {
|
|
114
|
+
const html = shell(
|
|
115
|
+
<MarkdownViewer content={'Visit <a href="https://example.com">link text</a> here.'} />,
|
|
116
|
+
);
|
|
117
|
+
const matches = html.match(/link text/g) ?? [];
|
|
118
|
+
expect(matches).toHaveLength(1);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it("throws when jsxMode is throw and a JSX component is unsupported", () => {
|
|
122
|
+
expect(() =>
|
|
123
|
+
shell(
|
|
124
|
+
<MarkdownViewer
|
|
125
|
+
content="<Foo />"
|
|
126
|
+
jsxMode="throw"
|
|
127
|
+
/>,
|
|
128
|
+
),
|
|
129
|
+
).toThrow(/Unsupported JSX component <Foo \/>/);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it("omits unsupported JSX when jsxMode is omit", () => {
|
|
133
|
+
const html = shell(
|
|
134
|
+
<MarkdownViewer
|
|
135
|
+
content="<Unknown />"
|
|
136
|
+
jsxMode="omit"
|
|
137
|
+
/>,
|
|
138
|
+
);
|
|
139
|
+
expect(html).not.toContain("unsupported");
|
|
140
|
+
expect(html).not.toContain("Unsupported JSX");
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it("adds heading id and autolink anchor by default", () => {
|
|
144
|
+
const html = shell(<MarkdownViewer content="## Hello" />);
|
|
145
|
+
expect(html).toMatch(/id="hello"/);
|
|
146
|
+
expect(html).toContain('href="#hello"');
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it("renders the same output when tree and source are passed via tree prop", () => {
|
|
150
|
+
const content = "# Tree prop\n\nParagraph body.";
|
|
151
|
+
const tree = parseMarkdownTree(content);
|
|
152
|
+
const source = markdownSourceFromString(content);
|
|
153
|
+
const fromTree = shell(<MarkdownViewer tree={{ tree, source }} />);
|
|
154
|
+
const fromContent = shell(<MarkdownViewer content={content} />);
|
|
155
|
+
expect(fromTree).toBe(fromContent);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it("uses paragraph renderer override when provided", () => {
|
|
159
|
+
const html = shell(
|
|
160
|
+
<MarkdownViewer
|
|
161
|
+
content="Custom paragraph."
|
|
162
|
+
renderers={{
|
|
163
|
+
paragraph: () => <span data-custom-paragraph>PARAGRAPH_MARKER</span>,
|
|
164
|
+
}}
|
|
165
|
+
/>,
|
|
166
|
+
);
|
|
167
|
+
expect(html).toContain("PARAGRAPH_MARKER");
|
|
168
|
+
expect(html).not.toContain("Custom paragraph.");
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it("renders multi-line paragraph JSX tags", () => {
|
|
172
|
+
const html = shell(<MarkdownViewer content={'<Since\n v="2.0.0"\n/>'} />);
|
|
173
|
+
expect(html).toContain("2.0.0");
|
|
174
|
+
expect(html).not.toContain("<Since");
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it("renders multiple JSX components on one line", () => {
|
|
178
|
+
const html = shell(
|
|
179
|
+
<MarkdownViewer content={'<Label label="Required" /><Label label="Optional" />'} />,
|
|
180
|
+
);
|
|
181
|
+
expect(html.match(/mantine-Badge-label/g)?.length).toBe(2);
|
|
182
|
+
expect(html).toContain("Required");
|
|
183
|
+
expect(html).toContain("Optional");
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it("renders inline HTML on separate source lines with line breaks", () => {
|
|
187
|
+
const html = shell(
|
|
188
|
+
<MarkdownViewer
|
|
189
|
+
content={`<span>foo</span> <span>bar</span>
|
|
190
|
+
<span>hello</span>
|
|
191
|
+
<span>world</span>`}
|
|
192
|
+
/>,
|
|
193
|
+
);
|
|
194
|
+
expect(html).toContain("foo");
|
|
195
|
+
expect(html).toContain("bar");
|
|
196
|
+
expect(html).toContain("hello");
|
|
197
|
+
expect(html).toContain("world");
|
|
198
|
+
expect(html.match(/<br\s*\/?>/g)?.length).toBe(2);
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it("renders jsx-only paragraphs on separate lines as separate p elements", () => {
|
|
202
|
+
const html = shell(
|
|
203
|
+
<MarkdownViewer
|
|
204
|
+
content={'<Since v="1.5.0" />\n\n<Since v="2.0.0" prefix="Available since" />'}
|
|
205
|
+
/>,
|
|
206
|
+
);
|
|
207
|
+
expect(html.match(/mantine-Badge-label/g)?.length).toBe(2);
|
|
208
|
+
const paragraphs = html.match(/<p[^>]*>[\s\S]*?<\/p>/g) ?? [];
|
|
209
|
+
expect(paragraphs.length).toBeGreaterThanOrEqual(2);
|
|
210
|
+
});
|
|
211
|
+
});
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { MantineProvider, v8CssVariablesResolver } from "@mantine/core";
|
|
2
|
+
import { unorderedBulletForDepth } from "@src/lib/markdown/view/components/ListBullet";
|
|
3
|
+
import { MarkdownViewer } from "@src/primitives/MarkdownViewer";
|
|
4
|
+
import { MANTINE_THEME } from "@src/theme/mantine";
|
|
5
|
+
import type { ReactElement } from "react";
|
|
6
|
+
import { renderToString } from "react-dom/server";
|
|
7
|
+
import { describe, expect, it } from "vitest";
|
|
8
|
+
|
|
9
|
+
function shell(node: ReactElement): string {
|
|
10
|
+
return renderToString(
|
|
11
|
+
<MantineProvider
|
|
12
|
+
theme={MANTINE_THEME}
|
|
13
|
+
cssVariablesResolver={v8CssVariablesResolver}
|
|
14
|
+
forceColorScheme="dark"
|
|
15
|
+
>
|
|
16
|
+
{node}
|
|
17
|
+
</MantineProvider>,
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
describe("unorderedBulletForDepth", () => {
|
|
22
|
+
it("cycles through bullet, circle, square by depth", () => {
|
|
23
|
+
expect(unorderedBulletForDepth(0)).toBe("•");
|
|
24
|
+
expect(unorderedBulletForDepth(1)).toBe("◦");
|
|
25
|
+
expect(unorderedBulletForDepth(2)).toBe("▪");
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("repeats the sequence for deeper nesting", () => {
|
|
29
|
+
expect(unorderedBulletForDepth(3)).toBe("•");
|
|
30
|
+
expect(unorderedBulletForDepth(4)).toBe("◦");
|
|
31
|
+
expect(unorderedBulletForDepth(5)).toBe("▪");
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
describe("MarkdownViewer / nested list bullets", () => {
|
|
36
|
+
it("renders distinct glyphs per nesting level", () => {
|
|
37
|
+
const content = ["- top", " - second", " - third"].join("\n");
|
|
38
|
+
const html = shell(<MarkdownViewer content={content} />);
|
|
39
|
+
expect(html).toContain("•");
|
|
40
|
+
expect(html).toContain("◦");
|
|
41
|
+
expect(html).toContain("▪");
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("keeps ordered list markers as numbers regardless of depth", () => {
|
|
45
|
+
const content = ["1. top", " 1. nested"].join("\n");
|
|
46
|
+
const html = shell(<MarkdownViewer content={content} />);
|
|
47
|
+
expect(html).not.toContain("◦");
|
|
48
|
+
});
|
|
49
|
+
});
|
|
@@ -0,0 +1,65 @@
|
|
|
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
|
+
// Syntax highlighting splits a line into per-token spans, so assert on single
|
|
39
|
+
// identifier tokens that survive tokenisation rather than whole expressions.
|
|
40
|
+
describe("MarkdownViewer / code inside lists", () => {
|
|
41
|
+
it("renders a fenced code block nested in a list item", () => {
|
|
42
|
+
const content = ["- Item with code:", " ```js", " alphaVar;", " ```"].join("\n");
|
|
43
|
+
const html = shell(<MarkdownViewer content={content} />);
|
|
44
|
+
expect(html).toContain("alphaVar");
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("renders multiple code lines nested in a list item", () => {
|
|
48
|
+
const content = ["- Item:", " ```js", " alphaVar;", " betaVar;", " ```"].join("\n");
|
|
49
|
+
const html = shell(<MarkdownViewer content={content} />);
|
|
50
|
+
expect(html).toContain("alphaVar");
|
|
51
|
+
expect(html).toContain("betaVar");
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("renders a code block that is the first child of a list item", () => {
|
|
55
|
+
const content = ["- ```ts", " codeFirstVar;", " ```"].join("\n");
|
|
56
|
+
const html = shell(<MarkdownViewer content={content} />);
|
|
57
|
+
expect(html).toContain("codeFirstVar");
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("renders a fenced code block inside a task list item", () => {
|
|
61
|
+
const content = ["- [ ] Task with code", " ```js", " taskCodeVar;", " ```"].join("\n");
|
|
62
|
+
const html = shell(<MarkdownViewer content={content} />);
|
|
63
|
+
expect(html).toContain("taskCodeVar");
|
|
64
|
+
});
|
|
65
|
+
});
|
|
@@ -0,0 +1,79 @@
|
|
|
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 / MarkdownViewerRenderers", () => {
|
|
39
|
+
it("uses heading renderer override", () => {
|
|
40
|
+
const html = shell(
|
|
41
|
+
<MarkdownViewer
|
|
42
|
+
content="## Section title"
|
|
43
|
+
renderers={{
|
|
44
|
+
heading: () => <span data-custom-heading>HEADING_MARKER</span>,
|
|
45
|
+
}}
|
|
46
|
+
/>,
|
|
47
|
+
);
|
|
48
|
+
expect(html).toContain("HEADING_MARKER");
|
|
49
|
+
expect(html).not.toContain("Section title");
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("uses link renderer override", () => {
|
|
53
|
+
const html = shell(
|
|
54
|
+
<MarkdownViewer
|
|
55
|
+
content="[Example](https://example.com)"
|
|
56
|
+
renderers={{
|
|
57
|
+
link: () => <span data-custom-link>LINK_MARKER</span>,
|
|
58
|
+
}}
|
|
59
|
+
/>,
|
|
60
|
+
);
|
|
61
|
+
expect(html).toContain("LINK_MARKER");
|
|
62
|
+
expect(html).not.toContain("Example");
|
|
63
|
+
expect(html).not.toContain("https://example.com");
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("uses image renderer override", () => {
|
|
67
|
+
const html = shell(
|
|
68
|
+
<MarkdownViewer
|
|
69
|
+
content=''
|
|
70
|
+
renderers={{
|
|
71
|
+
image: () => <span data-custom-image>IMAGE_MARKER</span>,
|
|
72
|
+
}}
|
|
73
|
+
/>,
|
|
74
|
+
);
|
|
75
|
+
expect(html).toContain("IMAGE_MARKER");
|
|
76
|
+
expect(html).not.toContain("Alt text");
|
|
77
|
+
expect(html).not.toContain("https://example.com/image.png");
|
|
78
|
+
});
|
|
79
|
+
});
|
|
@@ -0,0 +1,69 @@
|
|
|
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 / runnable code", () => {
|
|
39
|
+
it("renders RunnableCodeBlock when fenced info includes runnable", () => {
|
|
40
|
+
const html = shell(
|
|
41
|
+
<MarkdownViewer content={'```surql runnable="SELECT 1"\nRETURN 1;\n```'} />,
|
|
42
|
+
);
|
|
43
|
+
expect(html).toContain("Run Query");
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("renders RunnableCodeBlock for a loose runnable flag with no value", () => {
|
|
47
|
+
const html = shell(<MarkdownViewer content={"```surql runnable\nRETURN 1;\n```"} />);
|
|
48
|
+
expect(html).toContain("Run Query");
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("does not render RunnableCodeBlock for plain fenced code", () => {
|
|
52
|
+
const html = shell(<MarkdownViewer content={"```surql\nRETURN 1;\n```"} />);
|
|
53
|
+
expect(html).not.toContain("Run Query");
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("preserves runnable attribute casing through the render pipeline", () => {
|
|
57
|
+
const html = shell(
|
|
58
|
+
<MarkdownViewer
|
|
59
|
+
content={'```surql runnable="SELECT 1"\nRETURN 1;\n```'}
|
|
60
|
+
codeRenderers={{}}
|
|
61
|
+
renderers={{
|
|
62
|
+
code: ({ runnable }) => <span data-runnable={runnable}>{runnable}</span>,
|
|
63
|
+
}}
|
|
64
|
+
/>,
|
|
65
|
+
);
|
|
66
|
+
expect(html).toContain("SELECT 1");
|
|
67
|
+
expect(html).not.toContain("select 1");
|
|
68
|
+
});
|
|
69
|
+
});
|
|
@@ -0,0 +1,93 @@
|
|
|
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
|
+
import { md, mdViewer } from "../../_setup/markdown-classes";
|
|
8
|
+
|
|
9
|
+
// Mantine's `useComputedColorScheme` reaches for `window.matchMedia` even
|
|
10
|
+
// during a `renderToString` pass; stub it so the SSR test can exercise the
|
|
11
|
+
// full viewer pipeline (including `MarkdownCodeLines`).
|
|
12
|
+
beforeAll(() => {
|
|
13
|
+
if (typeof window !== "undefined" && typeof window.matchMedia !== "function") {
|
|
14
|
+
Object.defineProperty(window, "matchMedia", {
|
|
15
|
+
writable: true,
|
|
16
|
+
value: (query: string) => ({
|
|
17
|
+
matches: false,
|
|
18
|
+
media: query,
|
|
19
|
+
onchange: null,
|
|
20
|
+
addListener: () => {},
|
|
21
|
+
removeListener: () => {},
|
|
22
|
+
addEventListener: () => {},
|
|
23
|
+
removeEventListener: () => {},
|
|
24
|
+
dispatchEvent: () => false,
|
|
25
|
+
}),
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
function shell(node: ReactElement): string {
|
|
31
|
+
return renderToString(
|
|
32
|
+
<MantineProvider
|
|
33
|
+
theme={MANTINE_THEME}
|
|
34
|
+
cssVariablesResolver={v8CssVariablesResolver}
|
|
35
|
+
forceColorScheme="dark"
|
|
36
|
+
>
|
|
37
|
+
{node}
|
|
38
|
+
</MantineProvider>,
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
describe("MarkdownViewer / SSR", () => {
|
|
43
|
+
it("renders to a static HTML string with semantic markdown structure", () => {
|
|
44
|
+
const html = shell(
|
|
45
|
+
<MarkdownViewer
|
|
46
|
+
content={`# Title
|
|
47
|
+
|
|
48
|
+
Body with **bold**, *italic* and [a link](https://surrealdb.com).
|
|
49
|
+
|
|
50
|
+
- One
|
|
51
|
+
- Two
|
|
52
|
+
|
|
53
|
+
1. Alpha
|
|
54
|
+
2. Beta
|
|
55
|
+
|
|
56
|
+
\`\`\`ts
|
|
57
|
+
return true;
|
|
58
|
+
\`\`\`
|
|
59
|
+
`}
|
|
60
|
+
/>,
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
// Wrapper.
|
|
64
|
+
expect(html).toContain(mdViewer.viewerRoot);
|
|
65
|
+
|
|
66
|
+
// Semantic heading.
|
|
67
|
+
expect(html).toContain(md.h1);
|
|
68
|
+
expect(html).toMatch(/id="title"/);
|
|
69
|
+
expect(html).toContain("Title");
|
|
70
|
+
|
|
71
|
+
// Paragraph and inline marks (paragraph uses bare <p> without module class).
|
|
72
|
+
expect(html).toMatch(/<p[^>]*>Body with/);
|
|
73
|
+
expect(html).toContain(md.strong);
|
|
74
|
+
expect(html).toContain("bold</strong>");
|
|
75
|
+
expect(html).toContain(md.emphasis);
|
|
76
|
+
expect(html).toContain(md.link);
|
|
77
|
+
|
|
78
|
+
// Semantic lists.
|
|
79
|
+
expect(html).toContain(md.list);
|
|
80
|
+
expect(html).toContain("<li");
|
|
81
|
+
|
|
82
|
+
// Semantic code block.
|
|
83
|
+
expect(html).toContain(md.codeBlock);
|
|
84
|
+
expect(html).toContain("<code");
|
|
85
|
+
expect(html).toContain("</code></pre>");
|
|
86
|
+
|
|
87
|
+
// Raw markdown markers must not leak through.
|
|
88
|
+
expect(html).not.toContain("**bold**");
|
|
89
|
+
expect(html).not.toContain("*italic*");
|
|
90
|
+
expect(html).not.toContain("## ");
|
|
91
|
+
expect(html).not.toMatch(/<p[^>]*>#/);
|
|
92
|
+
});
|
|
93
|
+
});
|
package/dist/yoopta.css
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
:root[data-mantine-color-scheme=dark]{--yoopta-ui-background: 250 20% 7%;--yoopta-ui-foreground: 240 7% 96%;--yoopta-ui-muted: 252 16% 14%;--yoopta-ui-muted-foreground: 255 5% 59%;--yoopta-ui-border: 252 16% 14%;--yoopta-ui-ring: 240 7% 96%;--yoopta-ui-accent: 255 17% 14%;--yoopta-ui-accent-foreground: 240 7% 96%;--yoopta-ui-primary: 255 86% 63%;--yoopta-ui-primary-foreground: 0 0% 100%;--yoopta-ui-destructive: 0 84% 60%;--yoopta-ui-destructive-foreground: 0 0% 100%;--yoopta-ui-shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, .3);--yoopta-ui-shadow: 0 4px 6px -1px rgba(0, 0, 0, .4), 0 2px 4px -2px rgba(0, 0, 0, .3);--yoopta-ui-shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, .5), 0 4px 6px -4px rgba(0, 0, 0, .3);--yoopta-ui-shadow-xl: 0 16px 48px -12px rgba(0, 0, 0, .5), 0 4px 16px -4px rgba(0, 0, 0, .3)}:root[data-mantine-color-scheme=light]{--yoopta-ui-background: 240 7% 96%;--yoopta-ui-foreground: 250 20% 7%;--yoopta-ui-muted: 245 7% 90%;--yoopta-ui-muted-foreground: 255 5% 59%;--yoopta-ui-border: 248 7% 88%;--yoopta-ui-ring: 250 20% 7%;--yoopta-ui-accent: 245 7% 90%;--yoopta-ui-accent-foreground: 250 20% 7%;--yoopta-ui-primary: 255 86% 63%;--yoopta-ui-primary-foreground: 0 0% 100%;--yoopta-ui-destructive: 0 84% 60%;--yoopta-ui-destructive-foreground: 0 0% 100%}:root{--yoopta-ui-radius-sm: var(--mantine-radius-xs);--yoopta-ui-radius: var(--mantine-radius-sm);--yoopta-ui-radius-lg: var(--mantine-radius-md)}.yoopta-block[data-hovered-block=true],.yoopta-block:hover{background:transparent!important}.yoopta-block:last-child{margin-bottom:0}.yoopta-block p,.yoopta-block h1,.yoopta-block h2,.yoopta-block h3,.yoopta-block blockquote,.yoopta-block ul,.yoopta-block ol,.yoopta-block li{margin:var(--mantine-spacing-xs) 0}.yoopta-block h1,.yoopta-block h2,.yoopta-block h3{margin-bottom:0;margin-top:var(--mantine-spacing-lg);color:var(--mantine-color-bright)}.yoopta-slate{outline:none;border-radius:var(--mantine-radius-xs);box-shadow:inset 0 0 0 1px transparent;transition:box-shadow .25s ease;padding:var(--mantine-spacing-xs)}.yoopta-block[data-block-selected=true]>.yoopta-slate{box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--mantine-color-violet-3) 75%,var(--surreal-glass-subtle))}.yoopta-placeholder{position:relative}.yoopta-placeholder:before{content:attr(data-placeholder);color:var(--mantine-color-text);opacity:var(--surreal-opacity-strong);position:absolute;top:0;left:0;pointer-events:none;white-space:nowrap}.yoopta-ui-slash-command-group-heading{padding-left:6px!important}.cdn-image-overlay-input{background-color:#ffffff26!important;border-color:#ffffff40!important;color:#fff!important}.cdn-image-overlay-input::placeholder{color:#ffffff80!important}.cdn-image-overlay-input:focus{border-color:#ffffff80!important}.cdn-image-overlay-label{color:#fff!important}.yoopta-heading-link{opacity:0;transition:opacity .15s ease}h1:hover>.yoopta-heading-link,h2:hover>.yoopta-heading-link,h3:hover>.yoopta-heading-link{opacity:1}
|
|
File without changes
|