@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,255 +0,0 @@
|
|
|
1
|
-
import { EditorState } from "@codemirror/state";
|
|
2
|
-
import { EditorView } from "@codemirror/view";
|
|
3
|
-
import { MantineProvider, v8CssVariablesResolver } from "@mantine/core";
|
|
4
|
-
import { markdownModeField } from "@src/lib/markdown";
|
|
5
|
-
import { mergeMarkdownComponents } from "@src/lib/markdown/components/defaults";
|
|
6
|
-
import { markdownDecorations } from "@src/lib/markdown/editor/decorations";
|
|
7
|
-
import { setMarkdownEditorFocused } from "@src/lib/markdown/editor/decorations/editor-focus";
|
|
8
|
-
import { setMarkdownMode } from "@src/lib/markdown/editor/mode";
|
|
9
|
-
import { createWidgetStore } from "@src/lib/markdown/editor/widgets/store";
|
|
10
|
-
import { Callout } from "@src/lib/markdown/grammar/callout";
|
|
11
|
-
import { createMarkdownLanguage } from "@src/lib/markdown/grammar/language";
|
|
12
|
-
import { MarkdownViewer } from "@src/primitives/MarkdownViewer";
|
|
13
|
-
import { MANTINE_THEME } from "@src/theme/mantine";
|
|
14
|
-
import { renderToString } from "react-dom/server";
|
|
15
|
-
import { beforeAll, describe, expect, it } from "vitest";
|
|
16
|
-
|
|
17
|
-
beforeAll(() => {
|
|
18
|
-
if (typeof window !== "undefined" && typeof window.matchMedia !== "function") {
|
|
19
|
-
Object.defineProperty(window, "matchMedia", {
|
|
20
|
-
writable: true,
|
|
21
|
-
value: (query: string) => ({
|
|
22
|
-
matches: false,
|
|
23
|
-
media: query,
|
|
24
|
-
onchange: null,
|
|
25
|
-
addListener: () => {},
|
|
26
|
-
removeListener: () => {},
|
|
27
|
-
addEventListener: () => {},
|
|
28
|
-
removeEventListener: () => {},
|
|
29
|
-
dispatchEvent: () => false,
|
|
30
|
-
}),
|
|
31
|
-
});
|
|
32
|
-
}
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
/** Deterministic LCG so failures reproduce (no Math.random). */
|
|
36
|
-
function makeRng(seed: number): () => number {
|
|
37
|
-
let s = seed >>> 0;
|
|
38
|
-
return () => {
|
|
39
|
-
s = (Math.imul(s, 1664525) + 1013904223) >>> 0;
|
|
40
|
-
return s / 0x100000000;
|
|
41
|
-
};
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
const COMPONENTS = mergeMarkdownComponents({});
|
|
45
|
-
|
|
46
|
-
function renderViewerHtml(doc: string): string {
|
|
47
|
-
return renderToString(
|
|
48
|
-
<MantineProvider
|
|
49
|
-
theme={MANTINE_THEME}
|
|
50
|
-
cssVariablesResolver={v8CssVariablesResolver}
|
|
51
|
-
forceColorScheme="dark"
|
|
52
|
-
>
|
|
53
|
-
<MarkdownViewer
|
|
54
|
-
content={doc}
|
|
55
|
-
components={COMPONENTS}
|
|
56
|
-
/>
|
|
57
|
-
</MantineProvider>,
|
|
58
|
-
);
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/** Build an editor over the doc and sweep the caret so decoration paths run. */
|
|
62
|
-
function buildEditor(doc: string): void {
|
|
63
|
-
const state = EditorState.create({
|
|
64
|
-
doc,
|
|
65
|
-
extensions: [
|
|
66
|
-
createMarkdownLanguage({ extensions: [Callout] }),
|
|
67
|
-
markdownModeField,
|
|
68
|
-
markdownDecorations({ widgetStore: createWidgetStore() }),
|
|
69
|
-
],
|
|
70
|
-
});
|
|
71
|
-
const parent = document.createElement("div");
|
|
72
|
-
document.body.appendChild(parent);
|
|
73
|
-
const view = new EditorView({ state, parent });
|
|
74
|
-
view.dispatch({ effects: setMarkdownMode.of("preview") });
|
|
75
|
-
view.dispatch({ effects: setMarkdownEditorFocused.of(true) });
|
|
76
|
-
for (let pos = 0; pos <= doc.length; pos += Math.max(1, Math.floor(doc.length / 6))) {
|
|
77
|
-
view.dispatch({ selection: { anchor: pos } });
|
|
78
|
-
}
|
|
79
|
-
view.destroy();
|
|
80
|
-
parent.remove();
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
function decodeEntities(text: string): string {
|
|
84
|
-
return text
|
|
85
|
-
.replace(/</g, "<")
|
|
86
|
-
.replace(/>/g, ">")
|
|
87
|
-
.replace(/"/g, '"')
|
|
88
|
-
.replace(/'/g, "'")
|
|
89
|
-
.replace(/'/g, "'")
|
|
90
|
-
.replace(/&/g, "&");
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
/** Decoded text content of every inline `<code>` element in the HTML. */
|
|
94
|
-
function codeContents(html: string): string[] {
|
|
95
|
-
const out: string[] = [];
|
|
96
|
-
const re = /<code\b[^>]*>([\s\S]*?)<\/code>/g;
|
|
97
|
-
let m: RegExpExecArray | null;
|
|
98
|
-
// biome-ignore lint/suspicious/noAssignInExpressions: standard regex iteration
|
|
99
|
-
while ((m = re.exec(html)) !== null) {
|
|
100
|
-
out.push(decodeEntities(m[1] ?? ""));
|
|
101
|
-
}
|
|
102
|
-
return out;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
/** Visible text: strip styles, all tags, then decode entities. */
|
|
106
|
-
function visibleText(html: string): string {
|
|
107
|
-
return decodeEntities(
|
|
108
|
-
html.replace(/<style[^>]*>[\s\S]*?<\/style>/g, "").replace(/<[^>]+>/g, ""),
|
|
109
|
-
);
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
// Inner contents for inline code spans — all rich in the characters that
|
|
113
|
-
// previously derailed the paragraph scanner (`<`, `>`, `{`, `}`, `=`).
|
|
114
|
-
const CODE_INNERS = [
|
|
115
|
-
"<target>",
|
|
116
|
-
"<T>",
|
|
117
|
-
"<a>",
|
|
118
|
-
"<div>",
|
|
119
|
-
"<Tabs>",
|
|
120
|
-
"</Tabs>",
|
|
121
|
-
"<Tabs><TabItem/></Tabs>",
|
|
122
|
-
".{..+collect}",
|
|
123
|
-
".{..+shortest=<target>}",
|
|
124
|
-
"a < b > c",
|
|
125
|
-
"x <= y >= z",
|
|
126
|
-
"map<string, int>",
|
|
127
|
-
"Array<Record<string, T>>",
|
|
128
|
-
"<not closed",
|
|
129
|
-
"unclosed>",
|
|
130
|
-
"<<multi>>",
|
|
131
|
-
"{...spread}",
|
|
132
|
-
'<a href="x">',
|
|
133
|
-
"<!-- comment -->",
|
|
134
|
-
"generic<Foo<Bar>>",
|
|
135
|
-
"i++ < len && j-- > 0",
|
|
136
|
-
];
|
|
137
|
-
|
|
138
|
-
// Plain words with no markdown-significant characters.
|
|
139
|
-
const WORDS = ["returns", "the", "unique", "node", "set", "path", "and", "every", "shortest"];
|
|
140
|
-
|
|
141
|
-
// Real inline HTML tags that should still pair when they sit beside code.
|
|
142
|
-
const REAL_TAGS = ["<strong>bold</strong>", "<em>italic</em>", "text <b>x</b> more"];
|
|
143
|
-
|
|
144
|
-
const BLOCK_PREFIXES = ["", "", "", "> ", "# ", "- ", "1. "];
|
|
145
|
-
|
|
146
|
-
function buildParagraph(rng: () => number): { doc: string; expectedCodes: string[] } {
|
|
147
|
-
const tokenCount = 1 + Math.floor(rng() * 6);
|
|
148
|
-
const parts: string[] = [];
|
|
149
|
-
const expectedCodes: string[] = [];
|
|
150
|
-
|
|
151
|
-
for (let i = 0; i < tokenCount; i++) {
|
|
152
|
-
const roll = rng();
|
|
153
|
-
if (roll < 0.55) {
|
|
154
|
-
const inner = CODE_INNERS[Math.floor(rng() * CODE_INNERS.length)] ?? "";
|
|
155
|
-
// Occasionally use double-backtick fencing.
|
|
156
|
-
if (rng() < 0.25) {
|
|
157
|
-
parts.push(`\`\` ${inner} \`\``);
|
|
158
|
-
expectedCodes.push(inner);
|
|
159
|
-
} else {
|
|
160
|
-
parts.push(`\`${inner}\``);
|
|
161
|
-
expectedCodes.push(inner);
|
|
162
|
-
}
|
|
163
|
-
} else if (roll < 0.85) {
|
|
164
|
-
parts.push(WORDS[Math.floor(rng() * WORDS.length)] ?? "");
|
|
165
|
-
} else {
|
|
166
|
-
parts.push(REAL_TAGS[Math.floor(rng() * REAL_TAGS.length)] ?? "");
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
const prefix = BLOCK_PREFIXES[Math.floor(rng() * BLOCK_PREFIXES.length)] ?? "";
|
|
171
|
-
return { doc: prefix + parts.join(" "), expectedCodes };
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
function buildDoc(rng: () => number): { doc: string; expectedCodes: string[] } {
|
|
175
|
-
// Optionally prepend a fenced code block — the reported bug appeared right
|
|
176
|
-
// after one, so keep exercising that adjacency.
|
|
177
|
-
const paras: string[] = [];
|
|
178
|
-
const expectedCodes: string[] = [];
|
|
179
|
-
if (rng() < 0.4) {
|
|
180
|
-
paras.push("```surql\nSELECT * FROM person WHERE age > 18;\n```");
|
|
181
|
-
}
|
|
182
|
-
const count = 1 + Math.floor(rng() * 2);
|
|
183
|
-
for (let i = 0; i < count; i++) {
|
|
184
|
-
const { doc, expectedCodes: codes } = buildParagraph(rng);
|
|
185
|
-
paras.push(doc);
|
|
186
|
-
expectedCodes.push(...codes);
|
|
187
|
-
}
|
|
188
|
-
return { doc: paras.join("\n\n"), expectedCodes };
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
describe("MarkdownConformance / inline-code-with-angle-brackets fuzz", () => {
|
|
192
|
-
it("never leaks backticks and preserves every inline code span", () => {
|
|
193
|
-
const rng = makeRng(0x5eed42);
|
|
194
|
-
for (let i = 0; i < 400; i++) {
|
|
195
|
-
const { doc, expectedCodes } = buildDoc(rng);
|
|
196
|
-
let html = "";
|
|
197
|
-
try {
|
|
198
|
-
html = renderViewerHtml(doc);
|
|
199
|
-
buildEditor(doc);
|
|
200
|
-
} catch (err) {
|
|
201
|
-
throw new Error(`Fuzz #${i} threw for:\n${JSON.stringify(doc)}\n\n${String(err)}`);
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
const visible = visibleText(html);
|
|
205
|
-
// The backtick delimiters must never survive into visible output.
|
|
206
|
-
expect(
|
|
207
|
-
visible.includes("`"),
|
|
208
|
-
`Fuzz #${i} leaked a backtick.\nDOC: ${JSON.stringify(doc)}\nVISIBLE: ${JSON.stringify(visible)}`,
|
|
209
|
-
).toBe(false);
|
|
210
|
-
|
|
211
|
-
// Every inline code's content must appear inside a <code> element.
|
|
212
|
-
const rendered = codeContents(html);
|
|
213
|
-
for (const expected of expectedCodes) {
|
|
214
|
-
const found = rendered.some((c) => c.includes(expected));
|
|
215
|
-
expect(
|
|
216
|
-
found,
|
|
217
|
-
`Fuzz #${i} lost inline code ${JSON.stringify(expected)}.\nDOC: ${JSON.stringify(doc)}\nCODES: ${JSON.stringify(rendered)}`,
|
|
218
|
-
).toBe(true);
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
}, 60_000);
|
|
222
|
-
|
|
223
|
-
it("handles hand-picked adversarial inline-code snippets", () => {
|
|
224
|
-
const cases: Array<{ md: string; code: string }> = [
|
|
225
|
-
{ md: "`<target>`", code: "<target>" },
|
|
226
|
-
{ md: "`.{..+shortest=<target>}`", code: ".{..+shortest=<target>}" },
|
|
227
|
-
{ md: "before `<div>` after", code: "<div>" },
|
|
228
|
-
{ md: "`</Tabs>` orphan close", code: "</Tabs>" },
|
|
229
|
-
{ md: "`Array<Record<string, T>>`", code: "Array<Record<string, T>>" },
|
|
230
|
-
{ md: "a `x < y` and `p > q` end", code: "x < y" },
|
|
231
|
-
{ md: "*em with `<code>` inside*", code: "<code>" },
|
|
232
|
-
{ md: "[link `<x>` text](https://s.example)", code: "<x>" },
|
|
233
|
-
{ md: "> quote `<target>` here", code: "<target>" },
|
|
234
|
-
{ md: "# heading `<T>` end", code: "<T>" },
|
|
235
|
-
{ md: "- item `<li>` text", code: "<li>" },
|
|
236
|
-
{ md: '`<a href="x">` unbalanced open', code: '<a href="x">' },
|
|
237
|
-
{ md: "`<<nested>>` chevrons", code: "<<nested>>" },
|
|
238
|
-
{ md: "| head | `<td>` |\n| - | - |\n| a | `<b>` |", code: "<td>" },
|
|
239
|
-
];
|
|
240
|
-
|
|
241
|
-
for (const { md, code } of cases) {
|
|
242
|
-
const html = renderViewerHtml(md);
|
|
243
|
-
const visible = visibleText(html);
|
|
244
|
-
expect(visible.includes("`"), `backtick leaked for ${JSON.stringify(md)}`).toBe(false);
|
|
245
|
-
const found = codeContents(html).some((c) => c.includes(code));
|
|
246
|
-
expect(found, `lost ${JSON.stringify(code)} for ${JSON.stringify(md)}`).toBe(true);
|
|
247
|
-
}
|
|
248
|
-
});
|
|
249
|
-
|
|
250
|
-
it("still renders a real inline tag adjacent to bracketed inline code", () => {
|
|
251
|
-
const html = renderViewerHtml("`<target>` then <strong>go</strong> now");
|
|
252
|
-
expect(html).toContain("<strong>go</strong>");
|
|
253
|
-
expect(codeContents(html).some((c) => c.includes("<target>"))).toBe(true);
|
|
254
|
-
});
|
|
255
|
-
});
|
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
import type { MarkdownComponents } from "@src/lib/markdown";
|
|
2
|
-
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
3
|
-
|
|
4
|
-
import { renderMarkdown, visibleText } from "./render-helpers";
|
|
5
|
-
|
|
6
|
-
/** Echo component that serialises the props it receives for assertion. */
|
|
7
|
-
function Echo(props: Record<string, unknown>): string {
|
|
8
|
-
const { children: _, ...rest } = props;
|
|
9
|
-
return `props=${JSON.stringify(rest)}`;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
const components: MarkdownComponents = { Echo };
|
|
13
|
-
|
|
14
|
-
afterEach(() => {
|
|
15
|
-
vi.restoreAllMocks();
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
describe("Conformance / JSX attribute JSON", () => {
|
|
19
|
-
it("parses a JSON object value", () => {
|
|
20
|
-
const html = renderMarkdown('<Echo value={{ "a": 1, "b": [2, 3] }} />', { components });
|
|
21
|
-
expect(visibleText(html)).toContain('"value":{"a":1,"b":[2,3]}');
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
it("parses JSON number, boolean, and array values", () => {
|
|
25
|
-
const html = renderMarkdown("<Echo n={42} flag={true} list={[1,2]} />", { components });
|
|
26
|
-
const text = visibleText(html);
|
|
27
|
-
expect(text).toContain('"n":42');
|
|
28
|
-
expect(text).toContain('"flag":true');
|
|
29
|
-
expect(text).toContain('"list":[1,2]');
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
it("parses a quoted string value", () => {
|
|
33
|
-
const html = renderMarkdown('<Echo label="hello" />', { components });
|
|
34
|
-
expect(visibleText(html)).toContain('"label":"hello"');
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
it("treats a bare attribute as a boolean flag", () => {
|
|
38
|
-
const html = renderMarkdown("<Echo disabled />", { components });
|
|
39
|
-
// Bare booleans resolve to `undefined` props (not serialised) by design.
|
|
40
|
-
expect(visibleText(html)).toContain("props=");
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
it("drops an invalid-JSON value and warns in development", () => {
|
|
44
|
-
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
45
|
-
const html = renderMarkdown("<Echo value={{ a: 1 }} />", { components });
|
|
46
|
-
// Single-unquoted-key object is not valid JSON → the prop is dropped.
|
|
47
|
-
expect(visibleText(html)).not.toContain('"value"');
|
|
48
|
-
expect(warn).toHaveBeenCalled();
|
|
49
|
-
});
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
describe("Conformance / inline expressions are literal", () => {
|
|
53
|
-
it("renders a lone brace expression as text", () => {
|
|
54
|
-
const html = renderMarkdown("Count: {count} items");
|
|
55
|
-
expect(visibleText(html)).toContain("Count: {count} items");
|
|
56
|
-
});
|
|
57
|
-
});
|
|
@@ -1,165 +0,0 @@
|
|
|
1
|
-
import { EditorState } from "@codemirror/state";
|
|
2
|
-
import { EditorView } from "@codemirror/view";
|
|
3
|
-
import { MantineProvider, v8CssVariablesResolver } from "@mantine/core";
|
|
4
|
-
import { markdownModeField, markdownSourceFromString, parseMarkdownTree } from "@src/lib/markdown";
|
|
5
|
-
import { mergeMarkdownComponents } from "@src/lib/markdown/components/defaults";
|
|
6
|
-
import { markdownDecorations } from "@src/lib/markdown/editor/decorations";
|
|
7
|
-
import { setMarkdownEditorFocused } from "@src/lib/markdown/editor/decorations/editor-focus";
|
|
8
|
-
import { setMarkdownMode } from "@src/lib/markdown/editor/mode";
|
|
9
|
-
import { createWidgetStore } from "@src/lib/markdown/editor/widgets/store";
|
|
10
|
-
import { Callout } from "@src/lib/markdown/grammar/callout";
|
|
11
|
-
import { createMarkdownLanguage } from "@src/lib/markdown/grammar/language";
|
|
12
|
-
import { collectBlockHtmlDescriptors } from "@src/lib/markdown/tree/html";
|
|
13
|
-
import { MarkdownViewer } from "@src/primitives/MarkdownViewer";
|
|
14
|
-
import { MANTINE_THEME } from "@src/theme/mantine";
|
|
15
|
-
import { renderToString } from "react-dom/server";
|
|
16
|
-
import { beforeAll, describe, expect, it } from "vitest";
|
|
17
|
-
|
|
18
|
-
beforeAll(() => {
|
|
19
|
-
if (typeof window !== "undefined" && typeof window.matchMedia !== "function") {
|
|
20
|
-
Object.defineProperty(window, "matchMedia", {
|
|
21
|
-
writable: true,
|
|
22
|
-
value: (query: string) => ({
|
|
23
|
-
matches: false,
|
|
24
|
-
media: query,
|
|
25
|
-
onchange: null,
|
|
26
|
-
addListener: () => {},
|
|
27
|
-
removeListener: () => {},
|
|
28
|
-
addEventListener: () => {},
|
|
29
|
-
removeEventListener: () => {},
|
|
30
|
-
dispatchEvent: () => false,
|
|
31
|
-
}),
|
|
32
|
-
});
|
|
33
|
-
}
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
/** Deterministic LCG so failures reproduce (no Math.random). */
|
|
37
|
-
function makeRng(seed: number): () => number {
|
|
38
|
-
let s = seed >>> 0;
|
|
39
|
-
return () => {
|
|
40
|
-
s = (Math.imul(s, 1664525) + 1013904223) >>> 0;
|
|
41
|
-
return s / 0x100000000;
|
|
42
|
-
};
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
// Fragments chosen to stress tag pairing, attributes, nesting, blank lines,
|
|
46
|
-
// indentation, and malformed/adversarial shapes.
|
|
47
|
-
const FRAGMENTS = [
|
|
48
|
-
"<Tabs>",
|
|
49
|
-
"</Tabs>",
|
|
50
|
-
'<Tabs syncKey="a b c">',
|
|
51
|
-
'<TabItem label="One">',
|
|
52
|
-
'<TabItem label="Two" icon="star">',
|
|
53
|
-
"</TabItem>",
|
|
54
|
-
"<Since />",
|
|
55
|
-
'<Since v="1.0.0" />',
|
|
56
|
-
'<Button label="Go" href="https://x.example" />',
|
|
57
|
-
"<div>",
|
|
58
|
-
"</div>",
|
|
59
|
-
"<details>",
|
|
60
|
-
"<summary>Toggle</summary>",
|
|
61
|
-
"</details>",
|
|
62
|
-
"<strong>bold</strong>",
|
|
63
|
-
"<em>em",
|
|
64
|
-
"</em>",
|
|
65
|
-
'text before <TabItem label="Inline"> and after',
|
|
66
|
-
'<Echo value={{ "k": [1, 2, {"n": 3}] }} />',
|
|
67
|
-
"<Echo value={ broken json } />",
|
|
68
|
-
'<Echo value="unterminated ',
|
|
69
|
-
'<Tabs><TabItem label="Nested">deep</TabItem></Tabs>',
|
|
70
|
-
"\tindented content",
|
|
71
|
-
" four space indent",
|
|
72
|
-
"para line one",
|
|
73
|
-
"",
|
|
74
|
-
"",
|
|
75
|
-
"# heading",
|
|
76
|
-
"> quote",
|
|
77
|
-
"- list item",
|
|
78
|
-
"`code`",
|
|
79
|
-
"<https://autolink.example>",
|
|
80
|
-
"<not a tag",
|
|
81
|
-
"a > b < c",
|
|
82
|
-
"</Orphan>",
|
|
83
|
-
"<Weird a={{{}}} b='x' c=\"y\" />",
|
|
84
|
-
"| a | b |\n| - | - |\n| 1 | 2 |",
|
|
85
|
-
];
|
|
86
|
-
|
|
87
|
-
const COMPONENTS = mergeMarkdownComponents({
|
|
88
|
-
Echo: (props: Record<string, unknown>) => JSON.stringify(props.value ?? null),
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
function buildDoc(rng: () => number): string {
|
|
92
|
-
const lines = 1 + Math.floor(rng() * 12);
|
|
93
|
-
const parts: string[] = [];
|
|
94
|
-
for (let i = 0; i < lines; i++) {
|
|
95
|
-
parts.push(FRAGMENTS[Math.floor(rng() * FRAGMENTS.length)] ?? "");
|
|
96
|
-
}
|
|
97
|
-
return parts.join("\n");
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
function renderViewer(doc: string): void {
|
|
101
|
-
renderToString(
|
|
102
|
-
<MantineProvider
|
|
103
|
-
theme={MANTINE_THEME}
|
|
104
|
-
cssVariablesResolver={v8CssVariablesResolver}
|
|
105
|
-
forceColorScheme="dark"
|
|
106
|
-
>
|
|
107
|
-
<MarkdownViewer
|
|
108
|
-
content={doc}
|
|
109
|
-
components={COMPONENTS}
|
|
110
|
-
/>
|
|
111
|
-
</MantineProvider>,
|
|
112
|
-
);
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
function buildEditor(doc: string): void {
|
|
116
|
-
const state = EditorState.create({
|
|
117
|
-
doc,
|
|
118
|
-
extensions: [
|
|
119
|
-
createMarkdownLanguage({ extensions: [Callout] }),
|
|
120
|
-
markdownModeField,
|
|
121
|
-
markdownDecorations({ widgetStore: createWidgetStore() }),
|
|
122
|
-
],
|
|
123
|
-
});
|
|
124
|
-
const parent = document.createElement("div");
|
|
125
|
-
document.body.appendChild(parent);
|
|
126
|
-
const view = new EditorView({ state, parent });
|
|
127
|
-
view.dispatch({ effects: setMarkdownMode.of("preview") });
|
|
128
|
-
view.dispatch({ effects: setMarkdownEditorFocused.of(true) });
|
|
129
|
-
// Sweep the caret through the doc so active/reveal paths are exercised.
|
|
130
|
-
for (let pos = 0; pos <= doc.length; pos += Math.max(1, Math.floor(doc.length / 6))) {
|
|
131
|
-
view.dispatch({ selection: { anchor: pos } });
|
|
132
|
-
}
|
|
133
|
-
view.destroy();
|
|
134
|
-
parent.remove();
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
describe("MarkdownConformance / JSX fuzz", () => {
|
|
138
|
-
// Each case runs a full Mantine SSR render plus an EditorView build with a
|
|
139
|
-
// caret sweep, so keep the count modest and grant a generous timeout for CI.
|
|
140
|
-
it("never throws and keeps descriptor offsets sane across random JSX/HTML docs", () => {
|
|
141
|
-
const rng = makeRng(0xc0ffee);
|
|
142
|
-
for (let i = 0; i < 200; i++) {
|
|
143
|
-
const doc = buildDoc(rng);
|
|
144
|
-
try {
|
|
145
|
-
const tree = parseMarkdownTree(doc);
|
|
146
|
-
const src = markdownSourceFromString(doc);
|
|
147
|
-
const descs = collectBlockHtmlDescriptors(tree, src, COMPONENTS);
|
|
148
|
-
for (const d of descs) {
|
|
149
|
-
expect(d.openFrom).toBeGreaterThanOrEqual(0);
|
|
150
|
-
expect(d.openTo).toBeGreaterThan(d.openFrom);
|
|
151
|
-
if (d.closeTo !== null) {
|
|
152
|
-
expect(d.closeTo).toBeGreaterThanOrEqual(d.openTo);
|
|
153
|
-
expect(d.closeTo).toBeLessThanOrEqual(doc.length);
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
renderViewer(doc);
|
|
157
|
-
buildEditor(doc);
|
|
158
|
-
} catch (err) {
|
|
159
|
-
throw new Error(
|
|
160
|
-
`Fuzz case #${i} threw for input:\n${JSON.stringify(doc)}\n\n${String(err)}`,
|
|
161
|
-
);
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
}, 30_000);
|
|
165
|
-
});
|
|
@@ -1,95 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from "vitest";
|
|
2
|
-
|
|
3
|
-
import { renderMarkdown, visibleText } from "./render-helpers";
|
|
4
|
-
|
|
5
|
-
describe("Conformance / autolinks", () => {
|
|
6
|
-
it("keeps an absolute URL href unchanged", () => {
|
|
7
|
-
const html = renderMarkdown("Go https://example.com now");
|
|
8
|
-
expect(html).toContain('href="https://example.com"');
|
|
9
|
-
});
|
|
10
|
-
|
|
11
|
-
it("adds https:// to a www. autolink", () => {
|
|
12
|
-
const html = renderMarkdown("Visit www.example.com today");
|
|
13
|
-
expect(html).toContain('href="https://www.example.com"');
|
|
14
|
-
});
|
|
15
|
-
|
|
16
|
-
it("adds mailto: to a bare email autolink", () => {
|
|
17
|
-
const html = renderMarkdown("Mail me@example.com please");
|
|
18
|
-
expect(html).toContain('href="mailto:me@example.com"');
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
it("renders an angle-bracket URL autolink as an anchor", () => {
|
|
22
|
-
const html = renderMarkdown("See <https://example.com> here");
|
|
23
|
-
expect(html).toContain('href="https://example.com"');
|
|
24
|
-
expect(visibleText(html)).toContain("https://example.com");
|
|
25
|
-
// The angle brackets are delimiters, not visible content.
|
|
26
|
-
expect(visibleText(html)).not.toContain("<https://example.com>");
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
it("renders an angle-bracket email autolink with mailto:", () => {
|
|
30
|
-
const html = renderMarkdown("Mail <me@example.com> ok");
|
|
31
|
-
expect(html).toContain('href="mailto:me@example.com"');
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
it("does not linkify a URL inside inline code", () => {
|
|
35
|
-
const html = renderMarkdown("Use `https://example.com` verbatim");
|
|
36
|
-
expect(html).not.toContain('href="https://example.com"');
|
|
37
|
-
expect(html).toContain("https://example.com");
|
|
38
|
-
});
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
describe("Conformance / angle brackets in inline code", () => {
|
|
42
|
-
// Regression: `<…>` inside inline code was scanned as an HTML/JSX tag,
|
|
43
|
-
// which pushed the whole paragraph down the descriptor path and rendered
|
|
44
|
-
// every backtick literally while dropping the bracketed text.
|
|
45
|
-
it("keeps inline code intact when it contains an angle-bracket placeholder", () => {
|
|
46
|
-
const html = renderMarkdown(
|
|
47
|
-
"`.{..+collect}` returns the set; and `.{..+shortest=<target>}` returns the shortest path.",
|
|
48
|
-
);
|
|
49
|
-
expect(html).toContain("<code");
|
|
50
|
-
expect(html).toContain(".{..+collect}");
|
|
51
|
-
expect(html).toContain(".{..+shortest=<target>}");
|
|
52
|
-
// Backticks must not leak into the visible output.
|
|
53
|
-
expect(visibleText(html)).not.toContain("`");
|
|
54
|
-
expect(visibleText(html)).toContain(".{..+shortest=<target>}");
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
it("still pairs a real inline tag that sits beside inline code", () => {
|
|
58
|
-
const html = renderMarkdown("Press `Enter` then <strong>go</strong> now");
|
|
59
|
-
expect(html).toContain("<code");
|
|
60
|
-
expect(html).toContain("<strong>go</strong>");
|
|
61
|
-
});
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
describe("Conformance / reference-style links", () => {
|
|
65
|
-
it("resolves a full reference link [text][id]", () => {
|
|
66
|
-
const html = renderMarkdown("[click here][site] now\n\n[site]: https://example.com");
|
|
67
|
-
expect(html).toContain('href="https://example.com"');
|
|
68
|
-
expect(visibleText(html)).toContain("click here");
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
it("resolves a collapsed reference link [id][]", () => {
|
|
72
|
-
const html = renderMarkdown("[site][] now\n\n[site]: https://example.com");
|
|
73
|
-
expect(html).toContain('href="https://example.com"');
|
|
74
|
-
expect(visibleText(html)).toContain("site");
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
it("resolves a shortcut reference link [id]", () => {
|
|
78
|
-
const html = renderMarkdown("[site] now\n\n[site]: https://example.com");
|
|
79
|
-
expect(html).toContain('href="https://example.com"');
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
it("matches reference labels case-insensitively", () => {
|
|
83
|
-
const html = renderMarkdown("[Click][SITE] now\n\n[site]: https://example.com");
|
|
84
|
-
expect(html).toContain('href="https://example.com"');
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
it("emits the visible text (no anchor) when the reference is undefined", () => {
|
|
88
|
-
const html = renderMarkdown("see [missing][nope] end");
|
|
89
|
-
expect(html).not.toContain("href=");
|
|
90
|
-
const text = visibleText(html);
|
|
91
|
-
expect(text).toContain("missing");
|
|
92
|
-
// Reference brackets/label are not shown as literal text.
|
|
93
|
-
expect(text).not.toContain("[missing]");
|
|
94
|
-
});
|
|
95
|
-
});
|
|
@@ -1,88 +0,0 @@
|
|
|
1
|
-
import { MantineProvider, v8CssVariablesResolver } from "@mantine/core";
|
|
2
|
-
import type { MarkdownViewerProps } from "@src/primitives/MarkdownViewer";
|
|
3
|
-
import { MarkdownViewer } from "@src/primitives/MarkdownViewer";
|
|
4
|
-
import { MANTINE_THEME } from "@src/theme/mantine";
|
|
5
|
-
import { act } from "react";
|
|
6
|
-
import { createRoot } from "react-dom/client";
|
|
7
|
-
import { renderToString } from "react-dom/server";
|
|
8
|
-
|
|
9
|
-
// jsdom lacks matchMedia, which Mantine touches during render.
|
|
10
|
-
if (typeof window !== "undefined" && typeof window.matchMedia !== "function") {
|
|
11
|
-
Object.defineProperty(window, "matchMedia", {
|
|
12
|
-
writable: true,
|
|
13
|
-
value: (query: string) => ({
|
|
14
|
-
matches: false,
|
|
15
|
-
media: query,
|
|
16
|
-
onchange: null,
|
|
17
|
-
addListener: () => {},
|
|
18
|
-
removeListener: () => {},
|
|
19
|
-
addEventListener: () => {},
|
|
20
|
-
removeEventListener: () => {},
|
|
21
|
-
dispatchEvent: () => false,
|
|
22
|
-
}),
|
|
23
|
-
});
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/** Render a `MarkdownViewer` to an HTML string inside a Mantine provider. */
|
|
27
|
-
export function renderMarkdown(content: string, props?: Partial<MarkdownViewerProps>): string {
|
|
28
|
-
return renderToString(
|
|
29
|
-
<MantineProvider
|
|
30
|
-
theme={MANTINE_THEME}
|
|
31
|
-
cssVariablesResolver={v8CssVariablesResolver}
|
|
32
|
-
forceColorScheme="dark"
|
|
33
|
-
>
|
|
34
|
-
<MarkdownViewer
|
|
35
|
-
content={content}
|
|
36
|
-
{...props}
|
|
37
|
-
/>
|
|
38
|
-
</MantineProvider>,
|
|
39
|
-
);
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* Client-render a `MarkdownViewer` into jsdom and return its HTML. Unlike SSR
|
|
44
|
-
* `renderToString`, this exercises React error boundaries, which recover on the
|
|
45
|
-
* client (the environment where a throwing embedded component would blank the UI).
|
|
46
|
-
*/
|
|
47
|
-
export function renderMarkdownClient(
|
|
48
|
-
content: string,
|
|
49
|
-
props?: Partial<MarkdownViewerProps>,
|
|
50
|
-
): string {
|
|
51
|
-
const container = document.createElement("div");
|
|
52
|
-
document.body.appendChild(container);
|
|
53
|
-
const root = createRoot(container);
|
|
54
|
-
act(() => {
|
|
55
|
-
root.render(
|
|
56
|
-
<MantineProvider
|
|
57
|
-
theme={MANTINE_THEME}
|
|
58
|
-
cssVariablesResolver={v8CssVariablesResolver}
|
|
59
|
-
forceColorScheme="dark"
|
|
60
|
-
>
|
|
61
|
-
<MarkdownViewer
|
|
62
|
-
content={content}
|
|
63
|
-
{...props}
|
|
64
|
-
/>
|
|
65
|
-
</MantineProvider>,
|
|
66
|
-
);
|
|
67
|
-
});
|
|
68
|
-
const html = container.innerHTML;
|
|
69
|
-
act(() => {
|
|
70
|
-
root.unmount();
|
|
71
|
-
});
|
|
72
|
-
container.remove();
|
|
73
|
-
return html;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/** Strip all HTML tags and decode common entities, returning the visible text. */
|
|
77
|
-
export function visibleText(html: string): string {
|
|
78
|
-
return html
|
|
79
|
-
.replace(/<style[^>]*>[\s\S]*?<\/style>/g, "")
|
|
80
|
-
.replace(/<[^>]+>/g, "")
|
|
81
|
-
.replace(/"/g, '"')
|
|
82
|
-
.replace(/'/g, "'")
|
|
83
|
-
.replace(/</g, "<")
|
|
84
|
-
.replace(/>/g, ">")
|
|
85
|
-
.replace(/&/g, "&")
|
|
86
|
-
.replace(/\s+/g, " ")
|
|
87
|
-
.trim();
|
|
88
|
-
}
|