@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,47 @@
|
|
|
1
|
+
import { EditorSelection } from "@codemirror/state";
|
|
2
|
+
import * as MarkdownStories from "@src/stories/playground/Markdown.stories";
|
|
3
|
+
import { composeStories } from "@storybook/react-vite";
|
|
4
|
+
import { describe, expect, it } from "vitest";
|
|
5
|
+
import {
|
|
6
|
+
mountStory,
|
|
7
|
+
nextFrame,
|
|
8
|
+
page,
|
|
9
|
+
preparePreviewEditor,
|
|
10
|
+
scrollEditorTo,
|
|
11
|
+
} from "../../_setup/e2e-helpers";
|
|
12
|
+
import { md } from "../../_setup/markdown-classes";
|
|
13
|
+
|
|
14
|
+
const Stories = composeStories(MarkdownStories);
|
|
15
|
+
|
|
16
|
+
describe("MarkdownEditor / table cell click", () => {
|
|
17
|
+
it("places the caret in the source cell when a rendered cell is clicked", async () => {
|
|
18
|
+
mountStory(<Stories.Editor />);
|
|
19
|
+
const view = await preparePreviewEditor();
|
|
20
|
+
const doc = view.state.doc.toString();
|
|
21
|
+
|
|
22
|
+
const tablePos = doc.indexOf("| Feature |");
|
|
23
|
+
expect(tablePos).toBeGreaterThan(-1);
|
|
24
|
+
await scrollEditorTo(view, tablePos);
|
|
25
|
+
|
|
26
|
+
// Park the caret far away so the table renders as a widget.
|
|
27
|
+
view.dispatch({ selection: EditorSelection.cursor(0) });
|
|
28
|
+
await nextFrame();
|
|
29
|
+
await nextFrame();
|
|
30
|
+
|
|
31
|
+
const widget = document.querySelector(`.${md.tableWidget}`);
|
|
32
|
+
expect(widget).not.toBeNull();
|
|
33
|
+
|
|
34
|
+
// Click the "Headers" body cell.
|
|
35
|
+
const cell = page.getByText("Headers", { exact: true });
|
|
36
|
+
await cell.click();
|
|
37
|
+
await nextFrame();
|
|
38
|
+
|
|
39
|
+
// The caret should land on the "Headers" cell inside the table source.
|
|
40
|
+
const expected = doc.indexOf("Headers", tablePos);
|
|
41
|
+
expect(view.state.selection.main.head).toBe(expected);
|
|
42
|
+
|
|
43
|
+
// Clicking activates the block, revealing the raw source (widget gone).
|
|
44
|
+
await nextFrame();
|
|
45
|
+
expect(document.querySelector(`.${md.tableWidget}`)).toBeNull();
|
|
46
|
+
});
|
|
47
|
+
});
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { EditorSelection } from "@codemirror/state";
|
|
2
|
+
import * as MarkdownStories from "@src/stories/playground/Markdown.stories";
|
|
3
|
+
import { composeStories } from "@storybook/react-vite";
|
|
4
|
+
import { describe, expect, it } from "vitest";
|
|
5
|
+
import { getEditorView, mountStory, nextFrame } from "../../_setup/e2e-helpers";
|
|
6
|
+
|
|
7
|
+
const Stories = composeStories(MarkdownStories);
|
|
8
|
+
|
|
9
|
+
function clickControl(label: string): boolean {
|
|
10
|
+
const button = document.querySelector<HTMLButtonElement>(`button[aria-label="${label}"]`);
|
|
11
|
+
if (!button) return false;
|
|
12
|
+
button.dispatchEvent(new MouseEvent("mousedown", { bubbles: true, button: 0 }));
|
|
13
|
+
button.dispatchEvent(new MouseEvent("click", { bubbles: true, button: 0 }));
|
|
14
|
+
return true;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
describe("MarkdownEditor / table controls", () => {
|
|
18
|
+
it("adds and removes rows and columns from the active table", async () => {
|
|
19
|
+
mountStory(<Stories.EditorTableFormatter />);
|
|
20
|
+
const view = await getEditorView();
|
|
21
|
+
await nextFrame();
|
|
22
|
+
|
|
23
|
+
// Park the caret inside the table to reveal the floating controls.
|
|
24
|
+
const adaPos = view.state.doc.toString().indexOf("Ada");
|
|
25
|
+
view.dispatch({ selection: EditorSelection.cursor(adaPos) });
|
|
26
|
+
view.focus();
|
|
27
|
+
await nextFrame();
|
|
28
|
+
await nextFrame();
|
|
29
|
+
|
|
30
|
+
const tableLines = () =>
|
|
31
|
+
view.state.doc
|
|
32
|
+
.toString()
|
|
33
|
+
.split("\n")
|
|
34
|
+
.filter((l) => l.includes("|"));
|
|
35
|
+
const headerPipes = () => (tableLines()[0]?.match(/\|/g) ?? []).length;
|
|
36
|
+
|
|
37
|
+
const beforeCols = headerPipes();
|
|
38
|
+
const beforeRows = tableLines().length;
|
|
39
|
+
|
|
40
|
+
expect(clickControl("Add column")).toBe(true);
|
|
41
|
+
await nextFrame();
|
|
42
|
+
expect(headerPipes()).toBe(beforeCols + 1);
|
|
43
|
+
|
|
44
|
+
expect(clickControl("Add row")).toBe(true);
|
|
45
|
+
await nextFrame();
|
|
46
|
+
expect(tableLines().length).toBe(beforeRows + 1);
|
|
47
|
+
|
|
48
|
+
expect(clickControl("Remove row")).toBe(true);
|
|
49
|
+
await nextFrame();
|
|
50
|
+
expect(tableLines().length).toBe(beforeRows);
|
|
51
|
+
|
|
52
|
+
expect(clickControl("Remove column")).toBe(true);
|
|
53
|
+
await nextFrame();
|
|
54
|
+
expect(headerPipes()).toBe(beforeCols);
|
|
55
|
+
});
|
|
56
|
+
});
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { EditorSelection } from "@codemirror/state";
|
|
2
|
+
import * as MarkdownStories from "@src/stories/playground/Markdown.stories";
|
|
3
|
+
import { composeStories } from "@storybook/react-vite";
|
|
4
|
+
import { describe, expect, it } from "vitest";
|
|
5
|
+
import {
|
|
6
|
+
focusEditor,
|
|
7
|
+
getEditorView,
|
|
8
|
+
mountStory,
|
|
9
|
+
nextFrame,
|
|
10
|
+
userEvent,
|
|
11
|
+
} from "../../_setup/e2e-helpers";
|
|
12
|
+
|
|
13
|
+
const Stories = composeStories(MarkdownStories);
|
|
14
|
+
|
|
15
|
+
describe("MarkdownEditor / table formatter", () => {
|
|
16
|
+
it("realigns the table columns after typing in a cell", async () => {
|
|
17
|
+
mountStory(<Stories.EditorTableFormatter />);
|
|
18
|
+
const view = await getEditorView();
|
|
19
|
+
await nextFrame();
|
|
20
|
+
|
|
21
|
+
const doc = view.state.doc.toString();
|
|
22
|
+
const target = doc.indexOf("Ada") + "Ada".length;
|
|
23
|
+
expect(target).toBeGreaterThan(0);
|
|
24
|
+
|
|
25
|
+
view.dispatch({ selection: EditorSelection.cursor(target) });
|
|
26
|
+
await focusEditor();
|
|
27
|
+
await userEvent.keyboard("lyn"); // "Ada" -> "Adalyn"
|
|
28
|
+
await nextFrame();
|
|
29
|
+
await nextFrame();
|
|
30
|
+
|
|
31
|
+
const next = view.state.doc.toString();
|
|
32
|
+
// Column 1 grew to fit "Adalyn"; the delimiter row should align to it.
|
|
33
|
+
expect(next).toContain("Adalyn");
|
|
34
|
+
const lines = next.split("\n");
|
|
35
|
+
const widths = lines
|
|
36
|
+
.filter((l) => l.includes("|"))
|
|
37
|
+
.map((l) => l.indexOf("|", l.indexOf("|") + 1));
|
|
38
|
+
// All rows share the same position for the second pipe (aligned column 1).
|
|
39
|
+
expect(new Set(widths).size).toBe(1);
|
|
40
|
+
});
|
|
41
|
+
});
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { redo, undo } from "@codemirror/commands";
|
|
2
|
+
import { EditorSelection } from "@codemirror/state";
|
|
3
|
+
import * as MarkdownStories from "@src/stories/playground/Markdown.stories";
|
|
4
|
+
import { composeStories } from "@storybook/react-vite";
|
|
5
|
+
import { describe, expect, it } from "vitest";
|
|
6
|
+
import { getEditorView, mountStory, nextFrame } from "../../_setup/e2e-helpers";
|
|
7
|
+
|
|
8
|
+
const Stories = composeStories(MarkdownStories);
|
|
9
|
+
|
|
10
|
+
describe("MarkdownEditor / undo and redo", () => {
|
|
11
|
+
it("restores undone document edits via redo", async () => {
|
|
12
|
+
mountStory(<Stories.Editor />);
|
|
13
|
+
const view = await getEditorView();
|
|
14
|
+
|
|
15
|
+
const start = Date.now();
|
|
16
|
+
while (view.state.doc.length === 0 && Date.now() - start < 5000) {
|
|
17
|
+
await nextFrame();
|
|
18
|
+
}
|
|
19
|
+
expect(view.state.doc.length).toBeGreaterThan(0);
|
|
20
|
+
|
|
21
|
+
const before = view.state.doc.toString();
|
|
22
|
+
|
|
23
|
+
view.dispatch({
|
|
24
|
+
changes: { from: view.state.doc.length, insert: "\nredo-marker" },
|
|
25
|
+
selection: EditorSelection.cursor(view.state.doc.length),
|
|
26
|
+
});
|
|
27
|
+
await nextFrame();
|
|
28
|
+
|
|
29
|
+
const afterEdit = view.state.doc.toString();
|
|
30
|
+
expect(afterEdit).toContain("redo-marker");
|
|
31
|
+
|
|
32
|
+
expect(undo(view)).toBe(true);
|
|
33
|
+
expect(view.state.doc.toString()).toBe(before);
|
|
34
|
+
|
|
35
|
+
expect(redo(view)).toBe(true);
|
|
36
|
+
expect(view.state.doc.toString()).toBe(afterEdit);
|
|
37
|
+
});
|
|
38
|
+
});
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import { Grid, MantineProvider, Paper, v8CssVariablesResolver } from "@mantine/core";
|
|
2
|
+
import { MarkdownEditor } from "@src/primitives/MarkdownEditor";
|
|
3
|
+
import { MarkdownViewer } from "@src/primitives/MarkdownViewer";
|
|
4
|
+
import { MANTINE_THEME } from "@src/theme/mantine";
|
|
5
|
+
import { type FC, useState } from "react";
|
|
6
|
+
import { describe, expect, it } from "vitest";
|
|
7
|
+
import { getEditorView, mountStory, nextFrame } from "../../_setup/e2e-helpers";
|
|
8
|
+
import { md, mdViewer } from "../../_setup/markdown-classes";
|
|
9
|
+
|
|
10
|
+
const PARITY_FIXTURE = `# Title
|
|
11
|
+
|
|
12
|
+
Paragraph *italic* and **bold** and \`code\`.
|
|
13
|
+
|
|
14
|
+
## Section
|
|
15
|
+
|
|
16
|
+
Visit [link](https://surrealdb.com).
|
|
17
|
+
|
|
18
|
+
- One
|
|
19
|
+
- Two
|
|
20
|
+
|
|
21
|
+
- [ ] todo one
|
|
22
|
+
- [x] todo two **with bold**
|
|
23
|
+
|
|
24
|
+
\`\`\`ts
|
|
25
|
+
return true;
|
|
26
|
+
\`\`\`
|
|
27
|
+
|
|
28
|
+
<details>
|
|
29
|
+
<summary>Expand</summary>
|
|
30
|
+
|
|
31
|
+
Inside details.
|
|
32
|
+
|
|
33
|
+
</details>
|
|
34
|
+
`;
|
|
35
|
+
|
|
36
|
+
const ParityLayout: FC = () => {
|
|
37
|
+
const [doc] = useState(PARITY_FIXTURE);
|
|
38
|
+
return (
|
|
39
|
+
<MantineProvider
|
|
40
|
+
theme={MANTINE_THEME}
|
|
41
|
+
cssVariablesResolver={v8CssVariablesResolver}
|
|
42
|
+
forceColorScheme="dark"
|
|
43
|
+
>
|
|
44
|
+
<Grid gap="md">
|
|
45
|
+
<Grid.Col span={{ base: 12, md: 6 }}>
|
|
46
|
+
<Paper
|
|
47
|
+
p="md"
|
|
48
|
+
radius="md"
|
|
49
|
+
withBorder
|
|
50
|
+
>
|
|
51
|
+
<MarkdownEditor
|
|
52
|
+
mode="preview"
|
|
53
|
+
document={doc}
|
|
54
|
+
autoFocus={false}
|
|
55
|
+
mih={200}
|
|
56
|
+
/>
|
|
57
|
+
</Paper>
|
|
58
|
+
</Grid.Col>
|
|
59
|
+
<Grid.Col span={{ base: 12, md: 6 }}>
|
|
60
|
+
<Paper
|
|
61
|
+
p="md"
|
|
62
|
+
radius="md"
|
|
63
|
+
withBorder
|
|
64
|
+
>
|
|
65
|
+
<MarkdownViewer content={doc} />
|
|
66
|
+
</Paper>
|
|
67
|
+
</Grid.Col>
|
|
68
|
+
</Grid>
|
|
69
|
+
</MantineProvider>
|
|
70
|
+
);
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
function visibleText(el: Element): string {
|
|
74
|
+
const clone = el.cloneNode(true) as Element;
|
|
75
|
+
|
|
76
|
+
for (const n of clone.querySelectorAll(`.${md.markHidden}, .cm-line-folded`)) {
|
|
77
|
+
n.remove();
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return clone.textContent?.replace(/\s+/g, "") ?? "";
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const PARITY_CLASSES = [md.h1, md.h2, md.strong, md.emphasis, md.codeInline, md.link] as const;
|
|
84
|
+
|
|
85
|
+
describe("MarkdownViewer / editor parity", () => {
|
|
86
|
+
it("renders checklist text alongside checkboxes in the viewer", async () => {
|
|
87
|
+
mountStory(<ParityLayout />);
|
|
88
|
+
await getEditorView();
|
|
89
|
+
await nextFrame();
|
|
90
|
+
|
|
91
|
+
const vrEl = document.querySelector(`.${mdViewer.viewerRoot}`);
|
|
92
|
+
expect(vrEl).not.toBeNull();
|
|
93
|
+
if (vrEl === null) return;
|
|
94
|
+
|
|
95
|
+
const taskItems = vrEl.querySelectorAll(`li.${md.taskItem}`);
|
|
96
|
+
expect(taskItems.length).toBe(2);
|
|
97
|
+
|
|
98
|
+
const text = (i: number) =>
|
|
99
|
+
(taskItems[i]?.querySelector(`.${md.taskText}`)?.textContent ?? "").trim();
|
|
100
|
+
expect(text(0)).toContain("todo one");
|
|
101
|
+
expect(text(1)).toContain("todo two");
|
|
102
|
+
expect(taskItems[1]?.querySelector(`.${md.strong}`)?.textContent).toBe("with bold");
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("uses the same link colour as the editor", async () => {
|
|
106
|
+
mountStory(<ParityLayout />);
|
|
107
|
+
const view = await getEditorView();
|
|
108
|
+
view.contentDOM.blur();
|
|
109
|
+
await nextFrame();
|
|
110
|
+
|
|
111
|
+
const cmLink = document.querySelector(`.cm-content .${md.link}`);
|
|
112
|
+
const vwLink = document.querySelector(`.${mdViewer.viewerRoot} .${md.link}`);
|
|
113
|
+
expect(cmLink).not.toBeNull();
|
|
114
|
+
expect(vwLink).not.toBeNull();
|
|
115
|
+
if (cmLink === null || vwLink === null) return;
|
|
116
|
+
|
|
117
|
+
expect(getComputedStyle(vwLink).color).toBe(getComputedStyle(cmLink).color);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it("matches visible text between preview editor (blurred) and MarkdownViewer", async () => {
|
|
121
|
+
mountStory(<ParityLayout />);
|
|
122
|
+
const view = await getEditorView();
|
|
123
|
+
view.contentDOM.blur();
|
|
124
|
+
await nextFrame();
|
|
125
|
+
await nextFrame();
|
|
126
|
+
|
|
127
|
+
const cmEl = document.querySelector(".cm-content");
|
|
128
|
+
const vrEl = document.querySelector(`.${mdViewer.viewerRoot}`);
|
|
129
|
+
expect(cmEl).not.toBeNull();
|
|
130
|
+
expect(vrEl).not.toBeNull();
|
|
131
|
+
if (cmEl === null || vrEl === null) {
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
expect(visibleText(vrEl)).toBe(visibleText(cmEl));
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it("matches counts for shared structural classes", async () => {
|
|
139
|
+
mountStory(<ParityLayout />);
|
|
140
|
+
const view = await getEditorView();
|
|
141
|
+
view.contentDOM.blur();
|
|
142
|
+
await nextFrame();
|
|
143
|
+
|
|
144
|
+
const cmEl = document.querySelector(".cm-content");
|
|
145
|
+
const vrEl = document.querySelector(`.${mdViewer.viewerRoot}`);
|
|
146
|
+
expect(cmEl).not.toBeNull();
|
|
147
|
+
expect(vrEl).not.toBeNull();
|
|
148
|
+
if (cmEl === null || vrEl === null) {
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
for (const cls of PARITY_CLASSES) {
|
|
153
|
+
expect(vrEl.querySelectorAll(`.${cls}`).length).toBe(
|
|
154
|
+
cmEl.querySelectorAll(`.${cls}`).length,
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it("matches computed font size on first heading", async () => {
|
|
160
|
+
mountStory(<ParityLayout />);
|
|
161
|
+
const view = await getEditorView();
|
|
162
|
+
view.contentDOM.blur();
|
|
163
|
+
await nextFrame();
|
|
164
|
+
|
|
165
|
+
const cmH1 = document.querySelector(`.cm-content .${md.h1}`);
|
|
166
|
+
const vwH1 = document.querySelector(`.${mdViewer.viewerRoot} .${md.h1}`);
|
|
167
|
+
expect(cmH1).not.toBeNull();
|
|
168
|
+
expect(vwH1).not.toBeNull();
|
|
169
|
+
if (cmH1 === null || vwH1 === null) {
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const a = getComputedStyle(cmH1);
|
|
174
|
+
const b = getComputedStyle(vwH1);
|
|
175
|
+
expect(a.fontSize).toBe(b.fontSize);
|
|
176
|
+
expect(a.fontWeight).toBe(b.fontWeight);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it("renders HTML details in both editor preview and viewer", async () => {
|
|
180
|
+
mountStory(<ParityLayout />);
|
|
181
|
+
const view = await getEditorView();
|
|
182
|
+
view.contentDOM.blur();
|
|
183
|
+
await nextFrame();
|
|
184
|
+
|
|
185
|
+
const vrEl = document.querySelector(`.${mdViewer.viewerRoot}`);
|
|
186
|
+
expect(vrEl?.textContent).toContain("Expand");
|
|
187
|
+
expect(vrEl?.textContent).toContain("Inside details");
|
|
188
|
+
expect(document.querySelector(".cm-content")?.textContent).toContain("Inside details");
|
|
189
|
+
});
|
|
190
|
+
});
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { MantineProvider, v8CssVariablesResolver } from "@mantine/core";
|
|
2
|
+
import { MarkdownViewer } from "@src/primitives/MarkdownViewer";
|
|
3
|
+
import { MANTINE_THEME } from "@src/theme/mantine";
|
|
4
|
+
import { describe, expect, it } from "vitest";
|
|
5
|
+
import { mountStory, nextFrame } from "../../_setup/e2e-helpers";
|
|
6
|
+
import { md, mdViewer } from "../../_setup/markdown-classes";
|
|
7
|
+
|
|
8
|
+
const FIXTURE = `# Hello
|
|
9
|
+
|
|
10
|
+
Paragraph **bold**.
|
|
11
|
+
`;
|
|
12
|
+
|
|
13
|
+
describe("MarkdownViewer / render", () => {
|
|
14
|
+
it("exercises shared preview class names without mounting CodeMirror", async () => {
|
|
15
|
+
mountStory(
|
|
16
|
+
<MantineProvider
|
|
17
|
+
theme={MANTINE_THEME}
|
|
18
|
+
cssVariablesResolver={v8CssVariablesResolver}
|
|
19
|
+
forceColorScheme="dark"
|
|
20
|
+
>
|
|
21
|
+
<MarkdownViewer content={FIXTURE} />
|
|
22
|
+
</MantineProvider>,
|
|
23
|
+
);
|
|
24
|
+
await nextFrame();
|
|
25
|
+
await nextFrame();
|
|
26
|
+
|
|
27
|
+
const root = document.querySelector(`.${mdViewer.viewerRoot}`);
|
|
28
|
+
expect(root).not.toBeNull();
|
|
29
|
+
expect(document.querySelector(".cm-editor")).toBeNull();
|
|
30
|
+
|
|
31
|
+
expect(root?.querySelector(`.${md.heading}`)).not.toBeNull();
|
|
32
|
+
expect(root?.querySelector(`.${md.h1}`)).not.toBeNull();
|
|
33
|
+
expect(root?.querySelector(`.${md.strong}`)).not.toBeNull();
|
|
34
|
+
});
|
|
35
|
+
});
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { EditorState } from "@codemirror/state";
|
|
2
|
+
import { EditorView } from "@codemirror/view";
|
|
3
|
+
import { setEditorText } from "@src/lib/editor/helpers";
|
|
4
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
5
|
+
|
|
6
|
+
describe("setEditorText", () => {
|
|
7
|
+
let container: HTMLDivElement;
|
|
8
|
+
let view: EditorView;
|
|
9
|
+
|
|
10
|
+
afterEach(() => {
|
|
11
|
+
view?.destroy();
|
|
12
|
+
container?.remove();
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
function createView(doc: string, anchor: number): EditorView {
|
|
16
|
+
container = document.createElement("div");
|
|
17
|
+
document.body.appendChild(container);
|
|
18
|
+
|
|
19
|
+
return new EditorView({
|
|
20
|
+
parent: container,
|
|
21
|
+
state: EditorState.create({ doc, selection: { anchor, head: anchor } }),
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
it("clamps the selection when replacing with shorter text", () => {
|
|
26
|
+
view = createView("abcd", 4);
|
|
27
|
+
|
|
28
|
+
expect(() => setEditorText(view, "ab")).not.toThrow();
|
|
29
|
+
|
|
30
|
+
expect(view.state.doc.toString()).toBe("ab");
|
|
31
|
+
expect(view.state.selection.main.head).toBe(2);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("preserves a valid selection when replacing with longer text", () => {
|
|
35
|
+
view = createView("ab", 2);
|
|
36
|
+
|
|
37
|
+
setEditorText(view, "abcd");
|
|
38
|
+
|
|
39
|
+
expect(view.state.doc.toString()).toBe("abcd");
|
|
40
|
+
expect(view.state.selection.main.head).toBe(2);
|
|
41
|
+
});
|
|
42
|
+
});
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { codeInfoString, parseCodeInfo } from "@src/lib/markdown/tree/code-info";
|
|
2
|
+
import { describe, expect, it } from "vitest";
|
|
3
|
+
|
|
4
|
+
describe("parseCodeInfo", () => {
|
|
5
|
+
it("returns an empty language and attributes for a blank info string", () => {
|
|
6
|
+
expect(parseCodeInfo("")).toEqual({ language: "", attributes: {} });
|
|
7
|
+
expect(parseCodeInfo(" ")).toEqual({ language: "", attributes: {} });
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it("reads the language as the first token, lower-cased", () => {
|
|
11
|
+
expect(parseCodeInfo("TypeScript")).toEqual({
|
|
12
|
+
language: "typescript",
|
|
13
|
+
attributes: {},
|
|
14
|
+
});
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it("resolves loose flags to true", () => {
|
|
18
|
+
const parsed = parseCodeInfo("surql runnable");
|
|
19
|
+
expect(parsed.language).toBe("surql");
|
|
20
|
+
expect(parsed.attributes.runnable).toBe(true);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("parses double- and single-quoted values", () => {
|
|
24
|
+
const parsed = parseCodeInfo(`js title="My Example" label='other'`);
|
|
25
|
+
expect(parsed.attributes.title).toBe("My Example");
|
|
26
|
+
expect(parsed.attributes.label).toBe("other");
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("keeps an empty string value for empty quotes", () => {
|
|
30
|
+
const parsed = parseCodeInfo(`js title=""`);
|
|
31
|
+
expect(parsed.attributes.title).toBe("");
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("parses bare unquoted values", () => {
|
|
35
|
+
const parsed = parseCodeInfo("ts height=200");
|
|
36
|
+
expect(parsed.attributes.height).toBe("200");
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("parses braced expression values", () => {
|
|
40
|
+
const parsed = parseCodeInfo("surql runnable={SELECT 1}");
|
|
41
|
+
expect(parsed.attributes.runnable).toBe("{SELECT 1}");
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("mixes loose flags and key/value pairs", () => {
|
|
45
|
+
const parsed = parseCodeInfo(`surql runnable title="Demo"`);
|
|
46
|
+
expect(parsed).toEqual({
|
|
47
|
+
language: "surql",
|
|
48
|
+
attributes: { runnable: true, title: "Demo" },
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("preserves attribute value casing", () => {
|
|
53
|
+
const parsed = parseCodeInfo(`surql runnable="SELECT 1"`);
|
|
54
|
+
expect(parsed.attributes.runnable).toBe("SELECT 1");
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("codeInfoString returns string values and undefined for flags", () => {
|
|
58
|
+
const parsed = parseCodeInfo(`surql runnable title="Demo"`);
|
|
59
|
+
expect(codeInfoString(parsed, "title")).toBe("Demo");
|
|
60
|
+
expect(codeInfoString(parsed, "runnable")).toBeUndefined();
|
|
61
|
+
expect(codeInfoString(parsed, "missing")).toBeUndefined();
|
|
62
|
+
});
|
|
63
|
+
});
|