@surrealdb/ui 1.2.16 → 1.2.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/1299e4695ad0bd61.svg +5 -0
- package/dist/assets/408117e904653548.svg +48 -0
- package/dist/assets/b516b020352f6369.svg +1 -0
- package/dist/assets/dfde8de566c3c2b9.svg +11 -0
- package/dist/assets/e9eae7bc863f211b.svg +1 -0
- package/dist/assets/f74ae3707a5d418f.svg +1 -0
- package/dist/ui.d.ts +12 -0
- package/dist/ui.js +659 -647
- package/dist/ui.js.map +1 -1
- package/package.json +8 -2
- package/.claude/launch.json +0 -17
- package/.oxfmtrc.json +0 -27
- package/.oxlintrc.json +0 -23
- 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/.zed/settings.json +0 -36
- package/AGENTS.md +0 -111
- package/CLAUDE.md +0 -111
- package/REVIEW.md +0 -81
- package/doc/code-style.md +0 -264
- package/doc/release.md +0 -102
- package/doc/stacker-plan.md +0 -219
- package/release-notes.md +0 -39
- package/tests/_setup/e2e-helpers.tsx +0 -169
- package/tests/_setup/markdown-classes.ts +0 -3
- package/tests/_setup/portable-stories.ts +0 -33
- 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-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/__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 +0 -153
- package/tests/e2e/MarkdownEditor/edits.test.tsx +0 -112
- package/tests/e2e/MarkdownEditor/heading-fold.test.tsx +0 -45
- package/tests/e2e/MarkdownEditor/hybrid-widgets.test.tsx +0 -193
- package/tests/e2e/MarkdownEditor/image-on-image.test.tsx +0 -73
- package/tests/e2e/MarkdownEditor/jsx-block-content.test.tsx +0 -243
- package/tests/e2e/MarkdownEditor/jsx-highlight.test.tsx +0 -69
- package/tests/e2e/MarkdownEditor/jsx-inline-badges.test.tsx +0 -60
- package/tests/e2e/MarkdownEditor/jsx-inline-click.test.tsx +0 -54
- package/tests/e2e/MarkdownEditor/jsx-selection.test.tsx +0 -44
- package/tests/e2e/MarkdownEditor/link-placeholder.test.tsx +0 -68
- package/tests/e2e/MarkdownEditor/media-align.test.tsx +0 -58
- package/tests/e2e/MarkdownEditor/media-edit.test.tsx +0 -64
- package/tests/e2e/MarkdownEditor/media-sizing.test.tsx +0 -124
- package/tests/e2e/MarkdownEditor/modes.test.tsx +0 -94
- package/tests/e2e/MarkdownEditor/onimage-parity.test.tsx +0 -50
- package/tests/e2e/MarkdownEditor/regressions.test.tsx +0 -223
- package/tests/e2e/MarkdownEditor/slash-commands.test.tsx +0 -100
- package/tests/e2e/MarkdownEditor/table-click.test.tsx +0 -48
- package/tests/e2e/MarkdownEditor/table-controls.test.tsx +0 -57
- package/tests/e2e/MarkdownEditor/table-format.test.tsx +0 -42
- package/tests/e2e/MarkdownEditor/undo-redo.test.tsx +0 -39
- 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 +0 -191
- package/tests/e2e/MarkdownViewer/render.test.tsx +0 -36
- package/tests/e2e/Stacker/stacker.test.tsx +0 -304
- package/tests/unit/Editor/helpers.test.ts +0 -42
- package/tests/unit/MarkdownConformance/error-isolation.test.tsx +0 -44
- package/tests/unit/MarkdownConformance/indentation.test.tsx +0 -30
- package/tests/unit/MarkdownConformance/inline-code-fuzz.test.tsx +0 -255
- package/tests/unit/MarkdownConformance/jsx-attributes.test.tsx +0 -57
- package/tests/unit/MarkdownConformance/jsx-fuzz.test.tsx +0 -165
- package/tests/unit/MarkdownConformance/links.test.tsx +0 -95
- package/tests/unit/MarkdownConformance/render-helpers.tsx +0 -88
- package/tests/unit/MarkdownEditor/code-info.test.ts +0 -63
- package/tests/unit/MarkdownEditor/decorations.test.ts +0 -490
- package/tests/unit/MarkdownEditor/editor-ready.test.ts +0 -36
- package/tests/unit/MarkdownEditor/html-descriptors.test.ts +0 -94
- package/tests/unit/MarkdownEditor/indented-fence.test.ts +0 -103
- package/tests/unit/MarkdownEditor/jsx-attr-scan.test.ts +0 -115
- package/tests/unit/MarkdownEditor/jsx-block-widget.test.ts +0 -92
- package/tests/unit/MarkdownEditor/jsx-tag-grammar.test.ts +0 -88
- package/tests/unit/MarkdownEditor/list-indent.test.ts +0 -95
- package/tests/unit/MarkdownEditor/slash-commands.test.ts +0 -213
- package/tests/unit/MarkdownEditor/table-format.test.ts +0 -83
- package/tests/unit/MarkdownEditor/table.test.ts +0 -119
- package/tests/unit/MarkdownEditor/triggers.test.ts +0 -244
- package/tests/unit/MarkdownEditor/widget-store.test.ts +0 -105
- package/tests/unit/MarkdownViewer/callout.test.tsx +0 -60
- package/tests/unit/MarkdownViewer/code-highlight.test.tsx +0 -61
- package/tests/unit/MarkdownViewer/code-title.test.tsx +0 -62
- package/tests/unit/MarkdownViewer/features.test.tsx +0 -164
- package/tests/unit/MarkdownViewer/headings.test.tsx +0 -40
- package/tests/unit/MarkdownViewer/indented-jsx-children.test.tsx +0 -98
- package/tests/unit/MarkdownViewer/jsx-block-nesting.test.tsx +0 -234
- package/tests/unit/MarkdownViewer/jsx.test.tsx +0 -211
- package/tests/unit/MarkdownViewer/list-bullets.test.tsx +0 -49
- package/tests/unit/MarkdownViewer/list-code.test.tsx +0 -65
- package/tests/unit/MarkdownViewer/preserve-newlines.test.tsx +0 -143
- package/tests/unit/MarkdownViewer/read-time.test.ts +0 -21
- package/tests/unit/MarkdownViewer/renderers.test.tsx +0 -95
- package/tests/unit/MarkdownViewer/runnable.test.tsx +0 -69
- package/tests/unit/MarkdownViewer/ssr.test.tsx +0 -94
- package/tests/unit/MarkdownViewer/syntax-highlighting.test.tsx +0 -74
- package/tests/unit/MarkdownViewer/tabs-fenced-code.test.tsx +0 -196
- package/tests/unit/MarkdownViewer/tabs.test.tsx +0 -116
- package/tests/unit/Stacker/drag.test.ts +0 -21
- package/tests/unit/Stacker/helpers.test.ts +0 -105
- package/tests/unit/Stacker/reducer.test.ts +0 -107
- package/tests/unit/Syntax/language.test.ts +0 -63
- package/tools/duplicate-vector-mappings.json +0 -143
- package/tools/glass-export-overrides.json +0 -8
- package/tools/icon-parser.ts +0 -78
- package/tools/integration-gradient-exports.json +0 -57
- package/tools/picto-compare.ts +0 -64
- package/tools/picto-merge-urls.ts +0 -43
- package/tools/picto-sync.ts +0 -630
- package/tools/picto-verify-gradients.ts +0 -86
- package/tools/vector-export-nodes.json +0 -977
|
@@ -1,103 +0,0 @@
|
|
|
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
|
-
});
|
|
@@ -1,115 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
buildJsxTagHighlights,
|
|
3
|
-
findJsonAttributeClose,
|
|
4
|
-
parseAttributes,
|
|
5
|
-
parseJsonAttribute,
|
|
6
|
-
resolveAttributeValue,
|
|
7
|
-
} from "@src/lib/markdown/grammar/html-attrs";
|
|
8
|
-
import { describe, expect, it } from "vitest";
|
|
9
|
-
|
|
10
|
-
describe("findJsonAttributeClose", () => {
|
|
11
|
-
it("finds the closing brace for nested JSON objects and strings", () => {
|
|
12
|
-
const json = '{"version": "2.0.0", "items": [{"id": 1}]}';
|
|
13
|
-
const full = `{${json}}`;
|
|
14
|
-
const bounds = findJsonAttributeClose(`${full} trailing`, 0);
|
|
15
|
-
expect(bounds).toEqual({
|
|
16
|
-
contentFrom: 1,
|
|
17
|
-
contentTo: full.length - 1,
|
|
18
|
-
closeBrace: full.length - 1,
|
|
19
|
-
});
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
it("handles braces inside JSON strings", () => {
|
|
23
|
-
const json = '{"note": "}"}';
|
|
24
|
-
const full = `{${json}}`;
|
|
25
|
-
const bounds = findJsonAttributeClose(full, 0);
|
|
26
|
-
expect(bounds?.closeBrace).toBe(full.length - 1);
|
|
27
|
-
});
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
describe("parseJsonAttribute", () => {
|
|
31
|
-
it("parses JSON literals without evaluation", () => {
|
|
32
|
-
expect(parseJsonAttribute("42")).toBe(42);
|
|
33
|
-
expect(parseJsonAttribute('"hello"')).toBe("hello");
|
|
34
|
-
expect(parseJsonAttribute('{"enabled": true}')).toEqual({ enabled: true });
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
it("returns undefined for invalid JSON", () => {
|
|
38
|
-
expect(parseJsonAttribute("{ foo: 1 }")).toBeUndefined();
|
|
39
|
-
expect(parseJsonAttribute("count")).toBeUndefined();
|
|
40
|
-
});
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
describe("resolveAttributeValue", () => {
|
|
44
|
-
it("resolves JSON attribute values", () => {
|
|
45
|
-
expect(resolveAttributeValue({ type: "json", value: "100" })).toBe(100);
|
|
46
|
-
expect(resolveAttributeValue({ type: "json", value: '"2.0.0"' })).toBe("2.0.0");
|
|
47
|
-
});
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
describe("buildJsxTagHighlights", () => {
|
|
51
|
-
const tag = '<Since v={"2.0.0"} t="true" b="false" />';
|
|
52
|
-
|
|
53
|
-
it("covers the attribute section without gaps for the nested HTML parser", () => {
|
|
54
|
-
const highlights = buildJsxTagHighlights(tag);
|
|
55
|
-
const attrStart = tag.indexOf(" v=");
|
|
56
|
-
const attrEnd = tag.lastIndexOf(">");
|
|
57
|
-
|
|
58
|
-
const gaps = leftoverGaps(attrStart, attrEnd, highlights);
|
|
59
|
-
expect(gaps).toEqual([]);
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
it("labels attribute names and quoted literals after a JSON value", () => {
|
|
63
|
-
const highlights = buildJsxTagHighlights(tag);
|
|
64
|
-
const names = textAtKinds(tag, highlights, "name");
|
|
65
|
-
const strings = textAtKinds(tag, highlights, "string");
|
|
66
|
-
|
|
67
|
-
expect(names).toEqual(["v", "t", "b"]);
|
|
68
|
-
expect(strings).toEqual(['"true"', '"false"']);
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
it("matches parseAttributes JSON boundaries", () => {
|
|
72
|
-
const { endIndex } = readTagNameForTest(tag);
|
|
73
|
-
const attrString = tag.slice(endIndex, tag.lastIndexOf(">"));
|
|
74
|
-
const attrs = parseAttributes(attrString);
|
|
75
|
-
const expr = buildJsxTagHighlights(tag).find((h) => h.kind === "expression-content");
|
|
76
|
-
|
|
77
|
-
expect(attrs.v).toEqual({ type: "json", value: '"2.0.0"' });
|
|
78
|
-
expect(expr).toBeDefined();
|
|
79
|
-
if (!expr) return;
|
|
80
|
-
expect(tag.slice(expr.from, expr.to)).toBe('"2.0.0"');
|
|
81
|
-
expect(attrs.t).toBe("true");
|
|
82
|
-
expect(attrs.b).toBe("false");
|
|
83
|
-
});
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
function readTagNameForTest(text: string): { endIndex: number } {
|
|
87
|
-
let i = 1;
|
|
88
|
-
while (i < text.length && !/[\s/>]/.test(text[i] ?? "")) i++;
|
|
89
|
-
return { endIndex: i };
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
function leftoverGaps(
|
|
93
|
-
from: number,
|
|
94
|
-
to: number,
|
|
95
|
-
spans: { from: number; to: number }[],
|
|
96
|
-
): { from: number; to: number }[] {
|
|
97
|
-
const gaps: { from: number; to: number }[] = [];
|
|
98
|
-
let pos = from;
|
|
99
|
-
|
|
100
|
-
for (const span of spans) {
|
|
101
|
-
if (span.from > pos) gaps.push({ from: pos, to: span.from });
|
|
102
|
-
pos = Math.max(pos, span.to);
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
if (pos < to) gaps.push({ from: pos, to });
|
|
106
|
-
return gaps;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
function textAtKinds(
|
|
110
|
-
text: string,
|
|
111
|
-
highlights: ReturnType<typeof buildJsxTagHighlights>,
|
|
112
|
-
kind: "name" | "string",
|
|
113
|
-
): string[] {
|
|
114
|
-
return highlights.filter((h) => h.kind === kind).map((h) => text.slice(h.from, h.to));
|
|
115
|
-
}
|
|
@@ -1,92 +0,0 @@
|
|
|
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
|
-
});
|
|
@@ -1,88 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
JSX_ATTR_NAME_NODE,
|
|
3
|
-
JSX_ATTR_STRING_NODE,
|
|
4
|
-
JSX_EXPRESSION_CONTENT_NODE,
|
|
5
|
-
JSX_EXPRESSION_MARK_NODE,
|
|
6
|
-
} from "@src/lib/markdown/grammar/jsx-tag";
|
|
7
|
-
import { createMarkdownLanguage } from "@src/lib/markdown/grammar/language";
|
|
8
|
-
import { describe, expect, it } from "vitest";
|
|
9
|
-
|
|
10
|
-
function parse(text: string) {
|
|
11
|
-
const support = createMarkdownLanguage({ addKeymap: false, pasteURLAsLink: false });
|
|
12
|
-
return support.language.parser.parse(text);
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
function nodeNames(text: string): string[] {
|
|
16
|
-
const tree = parse(text);
|
|
17
|
-
const names: string[] = [];
|
|
18
|
-
tree.iterate({
|
|
19
|
-
enter: (n) => {
|
|
20
|
-
names.push(n.name);
|
|
21
|
-
},
|
|
22
|
-
});
|
|
23
|
-
return names;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
function htmlTagSpan(text: string): { from: number; to: number } | null {
|
|
27
|
-
const tree = parse(text);
|
|
28
|
-
let span: { from: number; to: number } | null = null;
|
|
29
|
-
tree.iterate({
|
|
30
|
-
enter: (n) => {
|
|
31
|
-
if (n.name === "HTMLTag") span = { from: n.from, to: n.to };
|
|
32
|
-
},
|
|
33
|
-
});
|
|
34
|
-
return span;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
describe("JsxTag grammar extension", () => {
|
|
38
|
-
it("keeps plain quoted attributes on the built-in HTMLTag parser", () => {
|
|
39
|
-
expect(nodeNames('<Since v="1" />')).toEqual(["Document", "Paragraph", "HTMLTag"]);
|
|
40
|
-
expect(nodeNames('<Since v="1" />')).not.toContain(JSX_EXPRESSION_CONTENT_NODE);
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
it("parses a full HTMLTag when the attribute value is a JSX expression", () => {
|
|
44
|
-
const text = '<Component prop={"a"} />';
|
|
45
|
-
expect(htmlTagSpan(text)).toEqual({ from: 0, to: text.length });
|
|
46
|
-
expect(nodeNames(text)).toContain(JSX_EXPRESSION_CONTENT_NODE);
|
|
47
|
-
expect(nodeNames(text)).toContain(JSX_EXPRESSION_MARK_NODE);
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
it("does not truncate at > inside a JSX expression", () => {
|
|
51
|
-
const text = "<Component prop={foo > bar} />";
|
|
52
|
-
expect(htmlTagSpan(text)).toEqual({ from: 0, to: text.length });
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
it("parses multiple JSX tags on one line", () => {
|
|
56
|
-
const text = '<Tabs defaultValue={"a"}><TabItem label={x} /></Tabs>';
|
|
57
|
-
const tree = parse(text);
|
|
58
|
-
const tags: string[] = [];
|
|
59
|
-
tree.iterate({
|
|
60
|
-
enter: (n) => {
|
|
61
|
-
if (n.name === "HTMLTag") tags.push(text.slice(n.from, n.to));
|
|
62
|
-
},
|
|
63
|
-
});
|
|
64
|
-
expect(tags).toEqual(['<Tabs defaultValue={"a"}>', "<TabItem label={x} />", "</Tabs>"]);
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
it("highlights quoted attributes that follow a JSX expression", () => {
|
|
68
|
-
const text = '<Since v={"2.0.0"} t="true" b="false" />';
|
|
69
|
-
expect(htmlTagSpan(text)).toEqual({ from: 0, to: text.length });
|
|
70
|
-
|
|
71
|
-
const tree = parse(text);
|
|
72
|
-
const names = collectNodes(tree, text, JSX_ATTR_NAME_NODE);
|
|
73
|
-
const strings = collectNodes(tree, text, JSX_ATTR_STRING_NODE);
|
|
74
|
-
|
|
75
|
-
expect(names).toEqual(["v", "t", "b"]);
|
|
76
|
-
expect(strings).toEqual(['"true"', '"false"']);
|
|
77
|
-
});
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
function collectNodes(tree: ReturnType<typeof parse>, text: string, name: string): string[] {
|
|
81
|
-
const out: string[] = [];
|
|
82
|
-
tree.iterate({
|
|
83
|
-
enter: (n) => {
|
|
84
|
-
if (n.name === name) out.push(text.slice(n.from, n.to));
|
|
85
|
-
},
|
|
86
|
-
});
|
|
87
|
-
return out;
|
|
88
|
-
}
|
|
@@ -1,95 +0,0 @@
|
|
|
1
|
-
import { EditorSelection, EditorState } from "@codemirror/state";
|
|
2
|
-
import { EditorView } from "@codemirror/view";
|
|
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
|
-
import { afterEach, describe, expect, it } from "vitest";
|
|
7
|
-
|
|
8
|
-
let view: EditorView;
|
|
9
|
-
|
|
10
|
-
afterEach(() => {
|
|
11
|
-
view?.destroy();
|
|
12
|
-
});
|
|
13
|
-
|
|
14
|
-
function createView(doc: string, cursor: number): EditorView {
|
|
15
|
-
const state = EditorState.create({
|
|
16
|
-
doc,
|
|
17
|
-
selection: EditorSelection.cursor(cursor),
|
|
18
|
-
extensions: [createMarkdownLanguage({ extensions: [Callout] })],
|
|
19
|
-
});
|
|
20
|
-
const parent = document.createElement("div");
|
|
21
|
-
document.body.appendChild(parent);
|
|
22
|
-
view = new EditorView({ state, parent });
|
|
23
|
-
return view;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/** Index just after the marker + space of the line containing `needle`. */
|
|
27
|
-
function cursorInItem(doc: string, needle: string): number {
|
|
28
|
-
return doc.indexOf(needle) + 1;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
describe("indentMarkdownList", () => {
|
|
32
|
-
it("indents an item under its preceding sibling", () => {
|
|
33
|
-
const doc = "- one\n- two";
|
|
34
|
-
const v = createView(doc, cursorInItem(doc, "two"));
|
|
35
|
-
const handled = indentMarkdownList(v, false);
|
|
36
|
-
expect(handled).toBe(true);
|
|
37
|
-
expect(v.state.doc.toString()).toBe("- one\n - two");
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
it("consumes Tab but does not change the first item (no preceding sibling)", () => {
|
|
41
|
-
const doc = "- one\n- two";
|
|
42
|
-
const v = createView(doc, cursorInItem(doc, "one"));
|
|
43
|
-
const handled = indentMarkdownList(v, false);
|
|
44
|
-
expect(handled).toBe(true);
|
|
45
|
-
expect(v.state.doc.toString()).toBe(doc);
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
it("outdents a nested item to its parent level", () => {
|
|
49
|
-
const doc = "- one\n - two";
|
|
50
|
-
const v = createView(doc, cursorInItem(doc, "two"));
|
|
51
|
-
const handled = indentMarkdownList(v, true);
|
|
52
|
-
expect(handled).toBe(true);
|
|
53
|
-
expect(v.state.doc.toString()).toBe("- one\n- two");
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
it("consumes Shift-Tab but does not outdent a top-level item", () => {
|
|
57
|
-
const doc = "- one\n- two";
|
|
58
|
-
const v = createView(doc, cursorInItem(doc, "two"));
|
|
59
|
-
const handled = indentMarkdownList(v, true);
|
|
60
|
-
expect(handled).toBe(true);
|
|
61
|
-
expect(v.state.doc.toString()).toBe(doc);
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
it("returns false outside a list", () => {
|
|
65
|
-
const doc = "just a paragraph";
|
|
66
|
-
const v = createView(doc, 4);
|
|
67
|
-
expect(indentMarkdownList(v, false)).toBe(false);
|
|
68
|
-
expect(indentMarkdownList(v, true)).toBe(false);
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
it("indents ordered list items by the marker width", () => {
|
|
72
|
-
const doc = "1. one\n2. two";
|
|
73
|
-
const v = createView(doc, cursorInItem(doc, "two"));
|
|
74
|
-
const handled = indentMarkdownList(v, false);
|
|
75
|
-
expect(handled).toBe(true);
|
|
76
|
-
expect(v.state.doc.toString()).toBe("1. one\n 2. two");
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
it("indents an item together with its nested children", () => {
|
|
80
|
-
const doc = "- one\n- two\n - child";
|
|
81
|
-
const v = createView(doc, cursorInItem(doc, "two"));
|
|
82
|
-
const handled = indentMarkdownList(v, false);
|
|
83
|
-
expect(handled).toBe(true);
|
|
84
|
-
expect(v.state.doc.toString()).toBe("- one\n - two\n - child");
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
it("keeps the cursor with the indented text", () => {
|
|
88
|
-
const doc = "- one\n- two";
|
|
89
|
-
const start = doc.indexOf("two");
|
|
90
|
-
const v = createView(doc, start);
|
|
91
|
-
indentMarkdownList(v, false);
|
|
92
|
-
// Two spaces inserted before the line shift the cursor right by two.
|
|
93
|
-
expect(v.state.selection.main.head).toBe(start + 2);
|
|
94
|
-
});
|
|
95
|
-
});
|