@surrealdb/ui 1.2.16 → 1.2.18

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.
Files changed (152) hide show
  1. package/dist/assets/1299e4695ad0bd61.svg +5 -0
  2. package/dist/assets/408117e904653548.svg +48 -0
  3. package/dist/assets/b516b020352f6369.svg +1 -0
  4. package/dist/assets/dfde8de566c3c2b9.svg +11 -0
  5. package/dist/assets/e9eae7bc863f211b.svg +1 -0
  6. package/dist/assets/f74ae3707a5d418f.svg +1 -0
  7. package/dist/icons.d.ts +6 -0
  8. package/dist/icons.js +198 -195
  9. package/dist/icons.js.map +1 -1
  10. package/dist/ui.d.ts +18 -0
  11. package/dist/ui.js +1441 -1426
  12. package/dist/ui.js.map +1 -1
  13. package/package.json +8 -2
  14. package/.claude/launch.json +0 -17
  15. package/.oxfmtrc.json +0 -27
  16. package/.oxlintrc.json +0 -23
  17. package/.picto-fix-png/Cursor.png +0 -0
  18. package/.picto-fix-png/DocumentGradient.png +0 -0
  19. package/.picto-fix-png/FileGradient.png +0 -0
  20. package/.picto-fix-png/HealthBubbleGradient.png +0 -0
  21. package/.picto-fix-png/LearnGradient.png +0 -0
  22. package/.picto-fix-png/MediaFileGradient.png +0 -0
  23. package/.zed/settings.json +0 -36
  24. package/AGENTS.md +0 -111
  25. package/CLAUDE.md +0 -111
  26. package/REVIEW.md +0 -81
  27. package/doc/code-style.md +0 -264
  28. package/doc/release.md +0 -102
  29. package/doc/stacker-plan.md +0 -219
  30. package/release-notes.md +0 -39
  31. package/tests/_setup/e2e-helpers.tsx +0 -169
  32. package/tests/_setup/markdown-classes.ts +0 -3
  33. package/tests/_setup/portable-stories.ts +0 -33
  34. 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
  35. package/tests/e2e/MarkdownEditor/__screenshots__/content-blocks.test.tsx/MarkdownEditor---content-blocks-renders-a-diagram-for-the-fenced-mermaid-block-1.png +0 -0
  36. package/tests/e2e/MarkdownEditor/__screenshots__/content-blocks.test.tsx/MarkdownEditor---content-blocks-renders-a-horizontal-rule-as-a-separator-1.png +0 -0
  37. 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
  38. package/tests/e2e/MarkdownEditor/__screenshots__/content-blocks.test.tsx/MarkdownEditor---content-blocks-shows-blockquote-text-from-the-sample-document-1.png +0 -0
  39. package/tests/e2e/MarkdownEditor/__screenshots__/content-blocks.test.tsx/MarkdownEditor---content-blocks-shows-fenced-TypeScript-sample-code-in-the-document-1.png +0 -0
  40. 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
  41. 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
  42. package/tests/e2e/MarkdownEditor/__screenshots__/edits.test.tsx/MarkdownEditor---edits-reflects-typed-characters-in-the-underlying-document-1.png +0 -0
  43. 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
  44. 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
  45. 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
  46. 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
  47. 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
  48. 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
  49. 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
  50. 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
  51. 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
  52. 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
  53. 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
  54. 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
  55. 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
  56. 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
  57. 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
  58. 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
  59. 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
  60. package/tests/e2e/MarkdownEditor/__screenshots__/media-sizing.test.tsx/MarkdownEditor---media-sizing-matches-MarkdownViewer-image-width-in-the-split-playground-1.png +0 -0
  61. package/tests/e2e/MarkdownEditor/__screenshots__/modes.test.tsx/MarkdownEditor---modes-standalone-MarkdownViewer-is-not-a-CodeMirror-surface-1.png +0 -0
  62. package/tests/e2e/MarkdownEditor/__screenshots__/modes.test.tsx/MarkdownEditor---modes-toggling-the-SegmentedControl-swaps-mode-without-remounting-the-editor-1.png +0 -0
  63. package/tests/e2e/MarkdownEditor/__screenshots__/regressions.test.tsx/MarkdownEditor---regressions-repeated-checkbox-toggles-do-not-duplicate-or-drift-task-markers-1.png +0 -0
  64. 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
  65. 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
  66. 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
  67. 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
  68. 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
  69. 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
  70. package/tests/e2e/MarkdownEditor/__screenshots__/undo-redo.test.tsx/MarkdownEditor---undo-and-redo-restores-undone-document-edits-via-redo-1.png +0 -0
  71. package/tests/e2e/MarkdownEditor/content-blocks.test.tsx +0 -153
  72. package/tests/e2e/MarkdownEditor/edits.test.tsx +0 -112
  73. package/tests/e2e/MarkdownEditor/heading-fold.test.tsx +0 -45
  74. package/tests/e2e/MarkdownEditor/hybrid-widgets.test.tsx +0 -193
  75. package/tests/e2e/MarkdownEditor/image-on-image.test.tsx +0 -73
  76. package/tests/e2e/MarkdownEditor/jsx-block-content.test.tsx +0 -243
  77. package/tests/e2e/MarkdownEditor/jsx-highlight.test.tsx +0 -69
  78. package/tests/e2e/MarkdownEditor/jsx-inline-badges.test.tsx +0 -60
  79. package/tests/e2e/MarkdownEditor/jsx-inline-click.test.tsx +0 -54
  80. package/tests/e2e/MarkdownEditor/jsx-selection.test.tsx +0 -44
  81. package/tests/e2e/MarkdownEditor/link-placeholder.test.tsx +0 -68
  82. package/tests/e2e/MarkdownEditor/media-align.test.tsx +0 -58
  83. package/tests/e2e/MarkdownEditor/media-edit.test.tsx +0 -64
  84. package/tests/e2e/MarkdownEditor/media-sizing.test.tsx +0 -124
  85. package/tests/e2e/MarkdownEditor/modes.test.tsx +0 -94
  86. package/tests/e2e/MarkdownEditor/onimage-parity.test.tsx +0 -50
  87. package/tests/e2e/MarkdownEditor/regressions.test.tsx +0 -223
  88. package/tests/e2e/MarkdownEditor/slash-commands.test.tsx +0 -100
  89. package/tests/e2e/MarkdownEditor/table-click.test.tsx +0 -48
  90. package/tests/e2e/MarkdownEditor/table-controls.test.tsx +0 -57
  91. package/tests/e2e/MarkdownEditor/table-format.test.tsx +0 -42
  92. package/tests/e2e/MarkdownEditor/undo-redo.test.tsx +0 -39
  93. package/tests/e2e/MarkdownViewer/__screenshots__/parity.test.tsx/MarkdownViewer---editor-parity-matches-computed-font-size-on-first-heading-1.png +0 -0
  94. package/tests/e2e/MarkdownViewer/__screenshots__/parity.test.tsx/MarkdownViewer---editor-parity-matches-counts-for-shared-structural-classes-1.png +0 -0
  95. package/tests/e2e/MarkdownViewer/__screenshots__/parity.test.tsx/MarkdownViewer---editor-parity-matches-visible-text-between-preview-editor--blurred--and-MarkdownViewer-1.png +0 -0
  96. package/tests/e2e/MarkdownViewer/__screenshots__/render.test.tsx/MarkdownViewer---render-exercises-shared-preview-class-names-without-mounting-CodeMirror-1.png +0 -0
  97. package/tests/e2e/MarkdownViewer/parity.test.tsx +0 -191
  98. package/tests/e2e/MarkdownViewer/render.test.tsx +0 -36
  99. package/tests/e2e/Stacker/stacker.test.tsx +0 -304
  100. package/tests/unit/Editor/helpers.test.ts +0 -42
  101. package/tests/unit/MarkdownConformance/error-isolation.test.tsx +0 -44
  102. package/tests/unit/MarkdownConformance/indentation.test.tsx +0 -30
  103. package/tests/unit/MarkdownConformance/inline-code-fuzz.test.tsx +0 -255
  104. package/tests/unit/MarkdownConformance/jsx-attributes.test.tsx +0 -57
  105. package/tests/unit/MarkdownConformance/jsx-fuzz.test.tsx +0 -165
  106. package/tests/unit/MarkdownConformance/links.test.tsx +0 -95
  107. package/tests/unit/MarkdownConformance/render-helpers.tsx +0 -88
  108. package/tests/unit/MarkdownEditor/code-info.test.ts +0 -63
  109. package/tests/unit/MarkdownEditor/decorations.test.ts +0 -490
  110. package/tests/unit/MarkdownEditor/editor-ready.test.ts +0 -36
  111. package/tests/unit/MarkdownEditor/html-descriptors.test.ts +0 -94
  112. package/tests/unit/MarkdownEditor/indented-fence.test.ts +0 -103
  113. package/tests/unit/MarkdownEditor/jsx-attr-scan.test.ts +0 -115
  114. package/tests/unit/MarkdownEditor/jsx-block-widget.test.ts +0 -92
  115. package/tests/unit/MarkdownEditor/jsx-tag-grammar.test.ts +0 -88
  116. package/tests/unit/MarkdownEditor/list-indent.test.ts +0 -95
  117. package/tests/unit/MarkdownEditor/slash-commands.test.ts +0 -213
  118. package/tests/unit/MarkdownEditor/table-format.test.ts +0 -83
  119. package/tests/unit/MarkdownEditor/table.test.ts +0 -119
  120. package/tests/unit/MarkdownEditor/triggers.test.ts +0 -244
  121. package/tests/unit/MarkdownEditor/widget-store.test.ts +0 -105
  122. package/tests/unit/MarkdownViewer/callout.test.tsx +0 -60
  123. package/tests/unit/MarkdownViewer/code-highlight.test.tsx +0 -61
  124. package/tests/unit/MarkdownViewer/code-title.test.tsx +0 -62
  125. package/tests/unit/MarkdownViewer/features.test.tsx +0 -164
  126. package/tests/unit/MarkdownViewer/headings.test.tsx +0 -40
  127. package/tests/unit/MarkdownViewer/indented-jsx-children.test.tsx +0 -98
  128. package/tests/unit/MarkdownViewer/jsx-block-nesting.test.tsx +0 -234
  129. package/tests/unit/MarkdownViewer/jsx.test.tsx +0 -211
  130. package/tests/unit/MarkdownViewer/list-bullets.test.tsx +0 -49
  131. package/tests/unit/MarkdownViewer/list-code.test.tsx +0 -65
  132. package/tests/unit/MarkdownViewer/preserve-newlines.test.tsx +0 -143
  133. package/tests/unit/MarkdownViewer/read-time.test.ts +0 -21
  134. package/tests/unit/MarkdownViewer/renderers.test.tsx +0 -95
  135. package/tests/unit/MarkdownViewer/runnable.test.tsx +0 -69
  136. package/tests/unit/MarkdownViewer/ssr.test.tsx +0 -94
  137. package/tests/unit/MarkdownViewer/syntax-highlighting.test.tsx +0 -74
  138. package/tests/unit/MarkdownViewer/tabs-fenced-code.test.tsx +0 -196
  139. package/tests/unit/MarkdownViewer/tabs.test.tsx +0 -116
  140. package/tests/unit/Stacker/drag.test.ts +0 -21
  141. package/tests/unit/Stacker/helpers.test.ts +0 -105
  142. package/tests/unit/Stacker/reducer.test.ts +0 -107
  143. package/tests/unit/Syntax/language.test.ts +0 -63
  144. package/tools/duplicate-vector-mappings.json +0 -143
  145. package/tools/glass-export-overrides.json +0 -8
  146. package/tools/icon-parser.ts +0 -78
  147. package/tools/integration-gradient-exports.json +0 -57
  148. package/tools/picto-compare.ts +0 -64
  149. package/tools/picto-merge-urls.ts +0 -43
  150. package/tools/picto-sync.ts +0 -630
  151. package/tools/picto-verify-gradients.ts +0 -86
  152. package/tools/vector-export-nodes.json +0 -977
@@ -1,63 +0,0 @@
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
- });
@@ -1,490 +0,0 @@
1
- import { syntaxTree } from "@codemirror/language";
2
- import { EditorSelection, EditorState } from "@codemirror/state";
3
- import { type Decoration, type DecorationSet, EditorView } from "@codemirror/view";
4
- import {
5
- DEFAULT_MARKDOWN_MODE,
6
- type MarkdownEditMode,
7
- markdownModeField,
8
- setMarkdownMode,
9
- } from "@src/lib/markdown";
10
- import { markdownDecorations } from "@src/lib/markdown/editor/decorations";
11
- import { setMarkdownEditorFocused } from "@src/lib/markdown/editor/decorations/editor-focus";
12
- import { CalloutHeaderWidget } from "@src/lib/markdown/editor/widgets/callout-header-widget";
13
- import { CheckboxWidget } from "@src/lib/markdown/editor/widgets/checkbox-widget";
14
- import {
15
- CodeFenceCopyWidget,
16
- CodeFenceLanguageMenuWidget,
17
- } from "@src/lib/markdown/editor/widgets/code-fence-widget";
18
- import { CustomCodeBlockWidget } from "@src/lib/markdown/editor/widgets/custom-code-block-widget";
19
- import { HeadingFoldWidget } from "@src/lib/markdown/editor/widgets/heading-fold-widget";
20
- import { ListBulletWidget } from "@src/lib/markdown/editor/widgets/list-bullet-widget";
21
- import { createWidgetStore } from "@src/lib/markdown/editor/widgets/store";
22
- import { TableWidget } from "@src/lib/markdown/editor/widgets/table-widget";
23
- import { Callout } from "@src/lib/markdown/grammar/callout";
24
- import { createMarkdownLanguage } from "@src/lib/markdown/grammar/language";
25
- import type { CodeRenderer } from "@src/lib/markdown/types";
26
- import { describe, expect, it } from "vitest";
27
-
28
- import { md } from "../../_setup/markdown-classes";
29
-
30
- interface DecorationDescriptor {
31
- from: number;
32
- to: number;
33
- spec: ReturnType<Decoration["spec"]> | Decoration["spec"];
34
- }
35
-
36
- function createView(
37
- doc: string,
38
- mode: MarkdownEditMode = DEFAULT_MARKDOWN_MODE,
39
- options: { codeRenderers?: Record<string, CodeRenderer> } = {},
40
- ): EditorView {
41
- const widgetStore = createWidgetStore();
42
- const state = EditorState.create({
43
- doc,
44
- extensions: [
45
- createMarkdownLanguage({ extensions: [Callout] }),
46
- markdownModeField,
47
- markdownDecorations({ widgetStore, codeRenderers: options.codeRenderers }),
48
- ],
49
- });
50
-
51
- const parent = document.createElement("div");
52
- document.body.appendChild(parent);
53
-
54
- const view = new EditorView({
55
- state,
56
- parent,
57
- });
58
-
59
- if (mode !== DEFAULT_MARKDOWN_MODE) {
60
- view.dispatch({ effects: setMarkdownMode.of(mode) });
61
- }
62
-
63
- view.focus();
64
- view.dispatch({ effects: setMarkdownEditorFocused.of(true) });
65
-
66
- return view;
67
- }
68
-
69
- function getDecorations(view: EditorView): DecorationDescriptor[] {
70
- const out: DecorationDescriptor[] = [];
71
- for (const value of (view as unknown as { docView: { decorations: DecorationSet[] } }).docView
72
- .decorations) {
73
- const cursor = value.iter();
74
- while (cursor.value) {
75
- out.push({ from: cursor.from, to: cursor.to, spec: cursor.value.spec });
76
- cursor.next();
77
- }
78
- }
79
- return out;
80
- }
81
-
82
- function collectWidgets(view: EditorView): unknown[] {
83
- return getDecorations(view)
84
- .map((d) => (d.spec as { widget?: unknown }).widget)
85
- .filter((value): value is object => value != null);
86
- }
87
-
88
- function getDecorationClasses(view: EditorView): string[] {
89
- return getDecorations(view)
90
- .map((d) => (d.spec as { class?: string }).class)
91
- .filter((value): value is string => typeof value === "string");
92
- }
93
-
94
- function getHiddenRanges(view: EditorView): Array<{ from: number; to: number }> {
95
- return getDecorations(view)
96
- .filter((d) => (d.spec as { class?: string }).class === md.markHidden)
97
- .map((d) => ({ from: d.from, to: d.to }));
98
- }
99
-
100
- describe("CheckboxWidget", () => {
101
- it("compare is referential so TileCache must not skip updateDOM for a fresh instance", () => {
102
- const store = createWidgetStore();
103
- const a = new CheckboxWidget(
104
- { id: "checkbox:10-13", checked: true, markerFrom: 11, markerTo: 12 },
105
- store,
106
- );
107
- const b = new CheckboxWidget(
108
- { id: "checkbox:10-13", checked: true, markerFrom: 11, markerTo: 12 },
109
- store,
110
- );
111
- expect(a.compare(b)).toBe(false);
112
- expect(a.compare(a)).toBe(true);
113
- });
114
- });
115
-
116
- describe("markdownDecorations", () => {
117
- it("emits heading line classes", () => {
118
- const view = createView("# Hello world\n\nbody");
119
- const classes = getDecorationClasses(view);
120
- expect(classes.some((c) => c.includes(md.h1))).toBe(true);
121
- view.destroy();
122
- });
123
-
124
- it("hides heading marks when block is inactive in preview mode", () => {
125
- const view = createView("# Title\n\nparagraph after the cursor");
126
- view.dispatch({ selection: EditorSelection.cursor(view.state.doc.length) });
127
- const classes = getDecorationClasses(view);
128
- expect(classes).toContain(md.markHidden);
129
- view.destroy();
130
- });
131
-
132
- it("shows heading marks when cursor is on the heading line in preview mode", () => {
133
- const view = createView("# Title\n");
134
- view.dispatch({ selection: EditorSelection.cursor(3) });
135
- const classes = getDecorationClasses(view);
136
- expect(classes).toContain(md.markShown);
137
- expect(classes).not.toContain(md.markHidden);
138
- view.destroy();
139
- });
140
-
141
- it("hides heading hashes in preview when unfocused even if the caret is on the heading line", () => {
142
- const view = createView("# Title\n");
143
- view.dispatch({
144
- selection: EditorSelection.cursor(3),
145
- effects: setMarkdownEditorFocused.of(false),
146
- });
147
- const classes = getDecorationClasses(view);
148
- expect(classes).toContain(md.markHidden);
149
- expect(classes).not.toContain(md.markShown);
150
- view.destroy();
151
- });
152
-
153
- it("hides custom {#id} heading suffix when the heading line is inactive in preview mode", () => {
154
- const doc = "## Playground {#playground-anchor}\n\nbody";
155
- const view = createView(doc);
156
- view.dispatch({ selection: EditorSelection.cursor(doc.length) });
157
- const hidden = getHiddenRanges(view);
158
- const suffixFrom = doc.indexOf("{#playground-anchor}");
159
- const suffixTo = suffixFrom + "{#playground-anchor}".length;
160
- expect(hidden.some((r) => r.from <= suffixFrom && r.to >= suffixTo)).toBe(true);
161
- view.destroy();
162
- });
163
-
164
- it("shows custom {#id} heading suffix when the heading line is active in preview mode", () => {
165
- const doc = "## Playground {#playground-anchor}\n";
166
- const view = createView(doc);
167
- view.dispatch({ selection: EditorSelection.cursor(doc.indexOf("Playground") + 2) });
168
- const classes = getDecorationClasses(view);
169
- expect(classes).toContain(md.markShown);
170
- view.destroy();
171
- });
172
-
173
- it("does not emit hidden marks in source mode", () => {
174
- const view = createView("# Title\n\n**bold**", "source");
175
- const classes = getDecorationClasses(view);
176
- expect(classes).not.toContain(md.markHidden);
177
- view.destroy();
178
- });
179
-
180
- it("reveals inline strong markers only when caret intersects the node", () => {
181
- // Document: "before **bold** after"
182
- // positions: 0 4 8 10 14 16 ...
183
- const view = createView("before **bold** after");
184
- // Caret on the word "bold" (inside the StrongEmphasis node).
185
- view.dispatch({ selection: EditorSelection.cursor(11) });
186
- let classes = getDecorationClasses(view);
187
- expect(classes).toContain(md.markShown);
188
- expect(classes).not.toContain(md.markHidden);
189
-
190
- // Caret elsewhere on the same line but outside the **bold** node.
191
- view.dispatch({ selection: EditorSelection.cursor(2) });
192
- classes = getDecorationClasses(view);
193
- expect(classes).toContain(md.markHidden);
194
- expect(classes).not.toContain(md.markShown);
195
-
196
- view.destroy();
197
- });
198
-
199
- it("keeps inline source visible when caret sits at the node boundary", () => {
200
- const view = createView("before **bold** after");
201
- // The StrongEmphasis node spans positions 7..15 (inclusive of `**`).
202
- // Caret immediately before the opening `**` should still reveal it.
203
- view.dispatch({ selection: EditorSelection.cursor(7) });
204
- let classes = getDecorationClasses(view);
205
- expect(classes).toContain(md.markShown);
206
- expect(classes).not.toContain(md.markHidden);
207
-
208
- // And likewise immediately after the closing `**`.
209
- view.dispatch({ selection: EditorSelection.cursor(15) });
210
- classes = getDecorationClasses(view);
211
- expect(classes).toContain(md.markShown);
212
- expect(classes).not.toContain(md.markHidden);
213
-
214
- view.destroy();
215
- });
216
-
217
- it("does not emit a ListBulletWidget for task list items in preview mode", () => {
218
- // Cursor on line 1 keeps the list line (line 3) inactive so that
219
- // both the bullet and checkbox widgets would normally render.
220
- const view = createView("body\n\n- [ ] item");
221
- view.dispatch({ selection: EditorSelection.cursor(0) });
222
-
223
- const widgets = collectWidgets(view);
224
- expect(widgets.some((w) => w instanceof CheckboxWidget)).toBe(true);
225
- expect(widgets.some((w) => w instanceof ListBulletWidget)).toBe(false);
226
-
227
- view.destroy();
228
- });
229
-
230
- it("still emits a ListBulletWidget for plain bullet list items", () => {
231
- const view = createView("body\n\n- item");
232
- view.dispatch({ selection: EditorSelection.cursor(0) });
233
-
234
- const widgets = collectWidgets(view);
235
- expect(widgets.some((w) => w instanceof ListBulletWidget)).toBe(true);
236
- expect(widgets.some((w) => w instanceof CheckboxWidget)).toBe(false);
237
-
238
- view.destroy();
239
- });
240
-
241
- it("keeps ListBulletWidget when the caret is in the list item body in preview mode", () => {
242
- const view = createView("- item\n");
243
- const listLine = view.state.doc.line(1);
244
- const caretInBody = listLine.from + "- item".indexOf("i");
245
- view.dispatch({ selection: EditorSelection.cursor(caretInBody) });
246
-
247
- expect(collectWidgets(view).some((w) => w instanceof ListBulletWidget)).toBe(true);
248
-
249
- view.destroy();
250
- });
251
-
252
- it("omits ListBulletWidget when the caret is inside the list mark in preview mode", () => {
253
- const view = createView("- item\n");
254
- let listMarkFrom = -1;
255
- syntaxTree(view.state).iterate({
256
- from: 0,
257
- to: view.state.doc.length,
258
- enter(n) {
259
- if (n.name === "ListMark") listMarkFrom = n.from;
260
- },
261
- });
262
- expect(listMarkFrom).toBeGreaterThanOrEqual(0);
263
- view.dispatch({ selection: EditorSelection.cursor(listMarkFrom + 1) });
264
- view.dispatch({ selection: EditorSelection.cursor(listMarkFrom) });
265
-
266
- expect(collectWidgets(view).some((w) => w instanceof ListBulletWidget)).toBe(false);
267
-
268
- view.destroy();
269
- });
270
-
271
- it("keeps CheckboxWidget when the caret is in the task item body in preview mode", () => {
272
- const view = createView("- [ ] task\n");
273
- const listLine = view.state.doc.line(1);
274
- const caretInBody = listLine.from + "- [ ] task".indexOf("t");
275
- view.dispatch({ selection: EditorSelection.cursor(caretInBody) });
276
-
277
- expect(collectWidgets(view).some((w) => w instanceof CheckboxWidget)).toBe(true);
278
-
279
- view.destroy();
280
- });
281
-
282
- it("omits CheckboxWidget when the caret is inside the task marker in preview mode", () => {
283
- const view = createView("- [ ] task\n");
284
- const bracketPos = view.state.doc.toString().indexOf("[");
285
- view.dispatch({ selection: EditorSelection.cursor(bracketPos) });
286
-
287
- expect(collectWidgets(view).some((w) => w instanceof CheckboxWidget)).toBe(false);
288
-
289
- view.destroy();
290
- });
291
-
292
- it("does not emit HeadingFoldWidget in preview or source mode", () => {
293
- const preview = createView("# Heading\n\nbody", "preview");
294
- expect(collectWidgets(preview).some((w) => w instanceof HeadingFoldWidget)).toBe(false);
295
- preview.destroy();
296
-
297
- const source = createView("# Heading\n\nbody", "source");
298
- expect(collectWidgets(source).some((w) => w instanceof HeadingFoldWidget)).toBe(false);
299
- source.destroy();
300
- });
301
-
302
- it("does not emit a CalloutHeaderWidget when the caret is on the callout title row", () => {
303
- // `> [!warning] Heads up\n> body`
304
- // Position 2 is `[`; position 13 is somewhere inside `Heads up`.
305
- const view = createView("> [!warning] Heads up\n> body");
306
- view.dispatch({ selection: EditorSelection.cursor(13) });
307
-
308
- const widgets = collectWidgets(view);
309
- expect(widgets.some((w) => w instanceof CalloutHeaderWidget)).toBe(false);
310
-
311
- view.destroy();
312
- });
313
-
314
- it("emits a CalloutHeaderWidget when the caret is far from the callout", () => {
315
- const view = createView("body line\n\n> [!warning] Heads up\n> inside");
316
- view.dispatch({ selection: EditorSelection.cursor(0) });
317
-
318
- const widgets = collectWidgets(view);
319
- expect(widgets.some((w) => w instanceof CalloutHeaderWidget)).toBe(true);
320
-
321
- view.destroy();
322
- });
323
-
324
- it("does not emit the CalloutHeaderWidget when the caret sits at the title's end-of-line", () => {
325
- // Caret at the end of the first line (right after `Heads up`)
326
- // should still count as touching the header range.
327
- const view = createView("> [!warning] Heads up\n> body");
328
- const eol = view.state.doc.line(1).to;
329
- view.dispatch({ selection: EditorSelection.cursor(eol) });
330
-
331
- const widgets = collectWidgets(view);
332
- expect(widgets.some((w) => w instanceof CalloutHeaderWidget)).toBe(false);
333
-
334
- view.destroy();
335
- });
336
-
337
- it("does not emit a CalloutHeaderWidget when the caret is anywhere inside the callout block", () => {
338
- const view = createView("> [!warning] Heads up\n> body");
339
- const bodyStart = view.state.doc.line(2).from + 2;
340
- view.dispatch({ selection: EditorSelection.cursor(bodyStart) });
341
-
342
- const widgets = collectWidgets(view);
343
- expect(widgets.some((w) => w instanceof CalloutHeaderWidget)).toBe(false);
344
-
345
- view.destroy();
346
- });
347
-
348
- it("emits blockquote / callout line decorations", () => {
349
- const view = createView("> [!warning] Heads up\n> body");
350
- const decorations = getDecorations(view);
351
- const callout = decorations.find(
352
- (d) => (d.spec as { class?: string }).class === md.callout,
353
- );
354
- expect(callout).toBeDefined();
355
- expect(
356
- // oxlint-disable-next-line no-unsafe-optional-chaining
357
- (callout?.spec as { attributes?: Record<string, string> }).attributes?.[
358
- "data-callout-kind"
359
- ],
360
- ).toBe("warning");
361
- view.destroy();
362
- });
363
- });
364
-
365
- describe("markdownDecorations / GFM tables", () => {
366
- const TABLE = `| Feature | Status |
367
- | --- | --- |
368
- | Headers | Live |`;
369
-
370
- it("emits TableWidget when the table block is inactive in preview mode", () => {
371
- const view = createView(`intro\n\n${TABLE}\n\nafter`);
372
- view.dispatch({ selection: EditorSelection.cursor(0) });
373
-
374
- expect(collectWidgets(view).some((w) => w instanceof TableWidget)).toBe(true);
375
-
376
- view.destroy();
377
- });
378
-
379
- it("omits TableWidget when the caret is inside the table block in preview mode", () => {
380
- const view = createView(TABLE);
381
- const inside = view.state.doc.toString().indexOf("Feature") + 2;
382
- view.dispatch({ selection: EditorSelection.cursor(inside) });
383
-
384
- expect(collectWidgets(view).some((w) => w instanceof TableWidget)).toBe(false);
385
-
386
- view.destroy();
387
- });
388
-
389
- it("does not emit TableWidget in source mode", () => {
390
- const view = createView(TABLE, "source");
391
- expect(collectWidgets(view).some((w) => w instanceof TableWidget)).toBe(false);
392
- view.destroy();
393
- });
394
-
395
- it("applies monospace to the table and colours delimiters when the block is active", () => {
396
- const view = createView(TABLE);
397
- const inside = view.state.doc.toString().indexOf("Feature") + 2;
398
- view.dispatch({ selection: EditorSelection.cursor(inside) });
399
-
400
- const classes = getDecorationClasses(view);
401
- expect(classes).toContain(md.tableSource);
402
- expect(classes).toContain(md.tableDelimiter);
403
-
404
- view.destroy();
405
- });
406
-
407
- it("applies monospace but not delimiter colour when the block is inactive in preview mode", () => {
408
- const view = createView(`intro\n\n${TABLE}\n\nafter`);
409
- view.dispatch({ selection: EditorSelection.cursor(0) });
410
-
411
- const classes = getDecorationClasses(view);
412
- expect(classes).toContain(md.tableSource);
413
- expect(classes).not.toContain(md.tableDelimiter);
414
-
415
- view.destroy();
416
- });
417
- });
418
-
419
- describe("markdownDecorations / fenced code", () => {
420
- const FENCED = "```rust\nfn main() {}\n```";
421
-
422
- it("hides fence and language marks when the block is inactive in preview mode", () => {
423
- const view = createView(`paragraph\n\n${FENCED}\n`, "preview");
424
- view.dispatch({ selection: EditorSelection.cursor(0) });
425
-
426
- const classes = getDecorationClasses(view);
427
- expect(classes).toContain(md.markHidden);
428
-
429
- const widgets = collectWidgets(view);
430
- expect(widgets.some((w) => w instanceof CodeFenceCopyWidget)).toBe(true);
431
- expect(widgets.some((w) => w instanceof CodeFenceLanguageMenuWidget)).toBe(false);
432
-
433
- view.destroy();
434
- });
435
-
436
- it("renders CodeFenceLanguageMenuWidget when the block is active in preview focused mode", () => {
437
- const view = createView(FENCED, "preview");
438
- const codeBody = view.state.doc.toString().indexOf("fn ");
439
- view.dispatch({ selection: EditorSelection.cursor(codeBody) });
440
-
441
- const widgets = collectWidgets(view);
442
- expect(widgets.some((w) => w instanceof CodeFenceLanguageMenuWidget)).toBe(true);
443
- expect(widgets.some((w) => w instanceof CodeFenceCopyWidget)).toBe(false);
444
-
445
- view.destroy();
446
- });
447
-
448
- it("does not emit fenced-code chrome widgets in source mode", () => {
449
- const view = createView(FENCED, "source");
450
- const widgets = collectWidgets(view);
451
- expect(widgets.some((w) => w instanceof CodeFenceLanguageMenuWidget)).toBe(false);
452
- expect(widgets.some((w) => w instanceof CodeFenceCopyWidget)).toBe(false);
453
- view.destroy();
454
- });
455
-
456
- it("invokes a registered code renderer for matching languages when inactive", () => {
457
- const renderer: CodeRenderer = () => null;
458
- const view = createView(`paragraph\n\n\`\`\`mermaid\ngraph TD;A-->B\n\`\`\`\n`, "preview", {
459
- codeRenderers: { mermaid: renderer },
460
- });
461
-
462
- const widgets = collectWidgets(view);
463
- expect(widgets.some((w) => w instanceof CustomCodeBlockWidget)).toBe(true);
464
-
465
- view.destroy();
466
- });
467
-
468
- it("falls back to fence widgets when an active block has a custom renderer", () => {
469
- const renderer: CodeRenderer = () => null;
470
- const doc = `\`\`\`mermaid\ngraph TD;A-->B\n\`\`\`\n`;
471
- const view = createView(doc, "preview", { codeRenderers: { mermaid: renderer } });
472
- const codeBody = view.state.doc.toString().indexOf("graph");
473
- view.dispatch({ selection: EditorSelection.cursor(codeBody) });
474
-
475
- const widgets = collectWidgets(view);
476
- expect(widgets.some((w) => w instanceof CustomCodeBlockWidget)).toBe(false);
477
- expect(widgets.some((w) => w instanceof CodeFenceLanguageMenuWidget)).toBe(true);
478
-
479
- view.destroy();
480
- });
481
-
482
- it("emits code-block line classes regardless of mode", () => {
483
- for (const mode of ["source", "preview"] as const) {
484
- const view = createView(FENCED, mode);
485
- const classes = getDecorationClasses(view);
486
- expect(classes).toContain(md.codeBlock);
487
- view.destroy();
488
- }
489
- });
490
- });
@@ -1,36 +0,0 @@
1
- import { EditorState } from "@codemirror/state";
2
- import { EditorView } from "@codemirror/view";
3
- import { getMarkdownEditorReady, markdownEditorReady } from "@src/lib/markdown/editor/editor-ready";
4
- import { afterEach, describe, expect, it, vi } from "vitest";
5
-
6
- describe("markdownEditorReady", () => {
7
- afterEach(() => {
8
- vi.useRealTimers();
9
- });
10
-
11
- it("sets data-markdown-ready after the first animation frame", async () => {
12
- vi.useFakeTimers({ toFake: ["requestAnimationFrame"] });
13
-
14
- const parent = document.createElement("div");
15
- document.body.appendChild(parent);
16
-
17
- const view = new EditorView({
18
- state: EditorState.create({
19
- doc: "# Title",
20
- extensions: [markdownEditorReady()],
21
- }),
22
- parent,
23
- });
24
-
25
- expect(getMarkdownEditorReady(view.state)).toBe(false);
26
- expect(view.dom.getAttribute("data-markdown-ready")).toBeNull();
27
-
28
- await vi.runAllTimersAsync();
29
-
30
- expect(getMarkdownEditorReady(view.state)).toBe(true);
31
- expect(view.dom.getAttribute("data-markdown-ready")).toBe("true");
32
-
33
- view.destroy();
34
- parent.remove();
35
- });
36
- });
@@ -1,94 +0,0 @@
1
- import {
2
- collectHtmlBlockTokens,
3
- collectParagraphJsxDescriptors,
4
- findDescriptorForOpenBlock,
5
- isBlockHtmlNodeDescriptor,
6
- outermostHtmlDescriptors,
7
- pairDocumentHtmlBlocks,
8
- pairHtmlTokens,
9
- pairInlineHtmlTags,
10
- } from "@src/lib/markdown/tree/html";
11
- import { parseMarkdownTree } from "@src/lib/markdown/tree/parse";
12
- import { markdownSourceFromString } from "@src/lib/markdown/tree/source";
13
- import { describe, expect, it } from "vitest";
14
-
15
- const TABLE_MARKDOWN = `<table>
16
- <thead><tr><th>Fn</th></tr></thead>
17
- <tbody><tr><td>**bold**</td></tr></tbody>
18
- </table>`;
19
-
20
- describe("HTML descriptors", () => {
21
- it("findDescriptorForOpenBlock picks the outermost table, not nested th/td", () => {
22
- const tree = parseMarkdownTree(TABLE_MARKDOWN);
23
- const node = tree.topNode.firstChild;
24
- expect(node?.name).toBe("HTMLBlock");
25
- if (!node) return;
26
-
27
- const source = markdownSourceFromString(TABLE_MARKDOWN);
28
- const cross = pairDocumentHtmlBlocks([node], source);
29
- const picked = findDescriptorForOpenBlock(node, cross);
30
-
31
- expect(picked?.tagName).toBe("table");
32
- expect(picked?.closeTo).not.toBeNull();
33
- });
34
-
35
- it("outermostHtmlDescriptors keeps only the table for a single HTML block", () => {
36
- const tree = parseMarkdownTree(TABLE_MARKDOWN);
37
- const node = tree.topNode.firstChild;
38
- expect(node).toBeDefined();
39
- if (!node) return;
40
-
41
- const source = markdownSourceFromString(TABLE_MARKDOWN);
42
- const tokens = collectHtmlBlockTokens(node, source);
43
- const outer = outermostHtmlDescriptors(pairHtmlTokens(tokens));
44
-
45
- expect(outer).toHaveLength(1);
46
- expect(outer[0]?.tagName).toBe("table");
47
- });
48
-
49
- it("treats inline <Since /> as non-block inside a paragraph", () => {
50
- const text = 'Text <Since v="1" /> end.';
51
- const source = markdownSourceFromString(text);
52
- const tree = parseMarkdownTree(text);
53
- const para = tree.topNode.firstChild;
54
- expect(para?.name).toBe("Paragraph");
55
- if (!para) return;
56
-
57
- const descriptors = pairInlineHtmlTags(para, source);
58
- const since = descriptors.find((d) => d.isJsx && d.rawTagName === "Since");
59
- expect(since).toBeDefined();
60
- // Unregistered components default to inline.
61
- expect(since && isBlockHtmlNodeDescriptor(since, undefined)).toBe(false);
62
- // Explicitly registering as block flips the resolved block-ness.
63
- expect(
64
- since &&
65
- isBlockHtmlNodeDescriptor(since, {
66
- Since: { component: () => null, block: true },
67
- }),
68
- ).toBe(true);
69
- });
70
-
71
- it("pairs multiple self-closing tags in one paragraph", () => {
72
- const text = '<Label label="Required" /><Label label="Optional" />';
73
- const source = markdownSourceFromString(text);
74
- const tree = parseMarkdownTree(text);
75
- const para = tree.topNode.firstChild;
76
- expect(para?.name).toBe("Paragraph");
77
- if (!para) return;
78
-
79
- expect(pairInlineHtmlTags(para, source)).toHaveLength(2);
80
- });
81
-
82
- it("collectParagraphJsxDescriptors scans plain paragraphs without HTMLTag nodes", () => {
83
- const text = 'Before <Card label="x" /> after.';
84
- const source = markdownSourceFromString(text);
85
- const tree = parseMarkdownTree(text);
86
- const para = tree.topNode.firstChild;
87
- expect(para?.name).toBe("Paragraph");
88
- if (!para) return;
89
-
90
- const descriptors = collectParagraphJsxDescriptors(para, source);
91
- expect(descriptors).toHaveLength(1);
92
- expect(descriptors[0]?.rawTagName).toBe("Card");
93
- });
94
- });