feat: 升级 CodeMirror 编辑器并新增 Markdown 工具栏
This commit is contained in:
@@ -10,9 +10,14 @@
|
|||||||
"typecheck": "tsc -b --pretty false"
|
"typecheck": "tsc -b --pretty false"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@codemirror/commands": "^6.10.4",
|
||||||
|
"@codemirror/lang-markdown": "^6.5.1",
|
||||||
|
"@codemirror/state": "^6.7.1",
|
||||||
|
"@codemirror/view": "^6.43.7",
|
||||||
"@md-to-pdf/core": "0.1.0",
|
"@md-to-pdf/core": "0.1.0",
|
||||||
"@md-to-pdf/markdown-echarts": "0.1.0",
|
"@md-to-pdf/markdown-echarts": "0.1.0",
|
||||||
"@md-to-pdf/preview-engine": "0.1.0",
|
"@md-to-pdf/preview-engine": "0.1.0",
|
||||||
|
"@uiw/react-codemirror": "^4.25.11",
|
||||||
"highlight.js": "^11.11.1",
|
"highlight.js": "^11.11.1",
|
||||||
"katex": "^0.16.28",
|
"katex": "^0.16.28",
|
||||||
"pdfjs-dist": "6.1.200",
|
"pdfjs-dist": "6.1.200",
|
||||||
|
|||||||
+40
-14
@@ -9,6 +9,7 @@ import {
|
|||||||
useRef,
|
useRef,
|
||||||
useState
|
useState
|
||||||
} from "react";
|
} from "react";
|
||||||
|
import type { EditorView } from "@codemirror/view";
|
||||||
import {
|
import {
|
||||||
createPagedDocumentPayload,
|
createPagedDocumentPayload,
|
||||||
getPaperDimensionsMm,
|
getPaperDimensionsMm,
|
||||||
@@ -71,6 +72,11 @@ import {
|
|||||||
applyThemeDefaults,
|
applyThemeDefaults,
|
||||||
selectTheme
|
selectTheme
|
||||||
} from "./theme-margins";
|
} from "./theme-margins";
|
||||||
|
import {
|
||||||
|
MarkdownEditor,
|
||||||
|
type MarkdownEditorHandle
|
||||||
|
} from "./MarkdownEditor";
|
||||||
|
import { MarkdownToolbar } from "./MarkdownToolbar";
|
||||||
|
|
||||||
const PrecisePdfPreview = lazy(async () => {
|
const PrecisePdfPreview = lazy(async () => {
|
||||||
const module = await import("./PrecisePdfPreview");
|
const module = await import("./PrecisePdfPreview");
|
||||||
@@ -107,6 +113,11 @@ const initialDocumentKind: DocumentKind = startsWithBlankMarkdown
|
|||||||
|
|
||||||
export function App() {
|
export function App() {
|
||||||
const [markdown, setMarkdown] = useState(initialMarkdown);
|
const [markdown, setMarkdown] = useState(initialMarkdown);
|
||||||
|
const [editorDocumentVersion, setEditorDocumentVersion] =
|
||||||
|
useState(0);
|
||||||
|
const [editorView, setEditorView] = useState<EditorView | null>(
|
||||||
|
null
|
||||||
|
);
|
||||||
const [fileName, setFileName] = useState(initialFileName);
|
const [fileName, setFileName] = useState(initialFileName);
|
||||||
const [documentKind, setDocumentKind] =
|
const [documentKind, setDocumentKind] =
|
||||||
useState<DocumentKind>(initialDocumentKind);
|
useState<DocumentKind>(initialDocumentKind);
|
||||||
@@ -141,7 +152,7 @@ export function App() {
|
|||||||
current: 0,
|
current: 0,
|
||||||
total: 0
|
total: 0
|
||||||
});
|
});
|
||||||
const editorRef = useRef<HTMLTextAreaElement>(null);
|
const editorRef = useRef<MarkdownEditorHandle>(null);
|
||||||
const markdownFileInputRef = useRef<HTMLInputElement>(null);
|
const markdownFileInputRef = useRef<HTMLInputElement>(null);
|
||||||
const moreMenuRef = useRef<HTMLDivElement>(null);
|
const moreMenuRef = useRef<HTMLDivElement>(null);
|
||||||
const previewScrollRef = useRef<HTMLDivElement>(null);
|
const previewScrollRef = useRef<HTMLDivElement>(null);
|
||||||
@@ -158,6 +169,11 @@ export function App() {
|
|||||||
const themeRefreshTargetRef = useRef<number | undefined>(undefined);
|
const themeRefreshTargetRef = useRef<number | undefined>(undefined);
|
||||||
documentDirtyRef.current = documentDirty;
|
documentDirtyRef.current = documentDirty;
|
||||||
|
|
||||||
|
function replaceMarkdownDocument(content: string) {
|
||||||
|
setMarkdown(content);
|
||||||
|
setEditorDocumentVersion((version) => version + 1);
|
||||||
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const bridge = window.mdToPdfDesktop;
|
const bridge = window.mdToPdfDesktop;
|
||||||
if (!bridge) {
|
if (!bridge) {
|
||||||
@@ -191,7 +207,7 @@ export function App() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setAppError("");
|
setAppError("");
|
||||||
setMarkdown(opened.markdown);
|
replaceMarkdownDocument(opened.markdown);
|
||||||
setFileName(opened.fileName);
|
setFileName(opened.fileName);
|
||||||
setDocumentKind("file");
|
setDocumentKind("file");
|
||||||
setSavedMarkdown(opened.markdown);
|
setSavedMarkdown(opened.markdown);
|
||||||
@@ -658,9 +674,13 @@ export function App() {
|
|||||||
|
|
||||||
function synchronizeScroll(origin: "editor" | "preview") {
|
function synchronizeScroll(origin: "editor" | "preview") {
|
||||||
const source =
|
const source =
|
||||||
origin === "editor" ? editorRef.current : getPreviewScroller();
|
origin === "editor"
|
||||||
|
? editorRef.current?.getScrollElement()
|
||||||
|
: getPreviewScroller();
|
||||||
const target =
|
const target =
|
||||||
origin === "editor" ? getPreviewScroller() : editorRef.current;
|
origin === "editor"
|
||||||
|
? getPreviewScroller()
|
||||||
|
: editorRef.current?.getScrollElement();
|
||||||
if (!source || !target) {
|
if (!source || !target) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -866,7 +886,7 @@ export function App() {
|
|||||||
try {
|
try {
|
||||||
setAppError("");
|
setAppError("");
|
||||||
const content = await file.text();
|
const content = await file.text();
|
||||||
setMarkdown(content);
|
replaceMarkdownDocument(content);
|
||||||
setFileName(file.name);
|
setFileName(file.name);
|
||||||
setDocumentKind("file");
|
setDocumentKind("file");
|
||||||
setSavedMarkdown(content);
|
setSavedMarkdown(content);
|
||||||
@@ -885,7 +905,7 @@ export function App() {
|
|||||||
if (!opened) {
|
if (!opened) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setMarkdown(opened.markdown);
|
replaceMarkdownDocument(opened.markdown);
|
||||||
setFileName(opened.fileName);
|
setFileName(opened.fileName);
|
||||||
setDocumentKind("file");
|
setDocumentKind("file");
|
||||||
setSavedMarkdown(opened.markdown);
|
setSavedMarkdown(opened.markdown);
|
||||||
@@ -923,7 +943,7 @@ export function App() {
|
|||||||
try {
|
try {
|
||||||
setAppError("");
|
setAppError("");
|
||||||
await window.mdToPdfDesktop?.startNewMarkdown();
|
await window.mdToPdfDesktop?.startNewMarkdown();
|
||||||
setMarkdown("");
|
replaceMarkdownDocument("");
|
||||||
setFileName("");
|
setFileName("");
|
||||||
setDocumentKind("new");
|
setDocumentKind("new");
|
||||||
setSavedMarkdown("");
|
setSavedMarkdown("");
|
||||||
@@ -1034,7 +1054,7 @@ export function App() {
|
|||||||
try {
|
try {
|
||||||
setAppError("");
|
setAppError("");
|
||||||
await window.mdToPdfDesktop?.startNewMarkdown();
|
await window.mdToPdfDesktop?.startNewMarkdown();
|
||||||
setMarkdown(sample.markdown);
|
replaceMarkdownDocument(sample.markdown);
|
||||||
setFileName(sample.fileName);
|
setFileName(sample.fileName);
|
||||||
setDocumentKind("sample");
|
setDocumentKind("sample");
|
||||||
setSavedMarkdown(sample.markdown);
|
setSavedMarkdown(sample.markdown);
|
||||||
@@ -1078,7 +1098,7 @@ export function App() {
|
|||||||
try {
|
try {
|
||||||
setAppError("");
|
setAppError("");
|
||||||
await window.mdToPdfDesktop?.startNewMarkdown();
|
await window.mdToPdfDesktop?.startNewMarkdown();
|
||||||
setMarkdown(tutorial.markdown);
|
replaceMarkdownDocument(tutorial.markdown);
|
||||||
setFileName(tutorial.fileName);
|
setFileName(tutorial.fileName);
|
||||||
setDocumentKind("sample");
|
setDocumentKind("sample");
|
||||||
setSavedMarkdown(tutorial.markdown);
|
setSavedMarkdown(tutorial.markdown);
|
||||||
@@ -1522,12 +1542,13 @@ export function App() {
|
|||||||
收起
|
收起
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<textarea
|
<MarkdownToolbar editorView={editorView} />
|
||||||
|
<MarkdownEditor
|
||||||
ref={editorRef}
|
ref={editorRef}
|
||||||
aria-label="Markdown 内容"
|
|
||||||
value={markdown}
|
value={markdown}
|
||||||
spellCheck={false}
|
documentVersion={editorDocumentVersion}
|
||||||
onChange={(event) => setMarkdown(event.target.value)}
|
onChange={setMarkdown}
|
||||||
|
onEditorReady={setEditorView}
|
||||||
onScroll={() => synchronizeScroll("editor")}
|
onScroll={() => synchronizeScroll("editor")}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -1537,7 +1558,12 @@ export function App() {
|
|||||||
className="source-panel-restore"
|
className="source-panel-restore"
|
||||||
aria-controls="markdown-source-content"
|
aria-controls="markdown-source-content"
|
||||||
aria-expanded="false"
|
aria-expanded="false"
|
||||||
onClick={() => setSourcePanelCollapsed(false)}
|
onClick={() => {
|
||||||
|
setSourcePanelCollapsed(false);
|
||||||
|
window.requestAnimationFrame(() =>
|
||||||
|
editorRef.current?.focus()
|
||||||
|
);
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
展开源文本
|
展开源文本
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -0,0 +1,158 @@
|
|||||||
|
import {
|
||||||
|
forwardRef,
|
||||||
|
useEffect,
|
||||||
|
useImperativeHandle,
|
||||||
|
useMemo,
|
||||||
|
useRef
|
||||||
|
} from "react";
|
||||||
|
import CodeMirror, {
|
||||||
|
type ReactCodeMirrorRef
|
||||||
|
} from "@uiw/react-codemirror";
|
||||||
|
import { markdown } from "@codemirror/lang-markdown";
|
||||||
|
import {
|
||||||
|
EditorView,
|
||||||
|
keymap,
|
||||||
|
type KeyBinding
|
||||||
|
} from "@codemirror/view";
|
||||||
|
import {
|
||||||
|
executeMarkdownEditorCommand,
|
||||||
|
markdownEditorCommands
|
||||||
|
} from "./markdown-editor-commands";
|
||||||
|
|
||||||
|
export interface MarkdownEditorHandle {
|
||||||
|
focus: () => void;
|
||||||
|
getScrollElement: () => HTMLElement | null;
|
||||||
|
getView: () => EditorView | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MarkdownEditorProps {
|
||||||
|
value: string;
|
||||||
|
documentVersion: number;
|
||||||
|
onChange: (value: string) => void;
|
||||||
|
onEditorReady?: (view: EditorView | null) => void;
|
||||||
|
onScroll?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const editorTheme = EditorView.theme({
|
||||||
|
"&": {
|
||||||
|
height: "100%",
|
||||||
|
backgroundColor: "#f6f7f5",
|
||||||
|
color: "#26312d",
|
||||||
|
fontSize: "0.86rem"
|
||||||
|
},
|
||||||
|
"&.cm-focused": {
|
||||||
|
outline: "none",
|
||||||
|
boxShadow: "inset 3px 0 #74a88f"
|
||||||
|
},
|
||||||
|
".cm-scroller": {
|
||||||
|
fontFamily:
|
||||||
|
'"Cascadia Code", "SFMono-Regular", Consolas, monospace',
|
||||||
|
lineHeight: "1.72",
|
||||||
|
tabSize: "2",
|
||||||
|
overflow: "auto"
|
||||||
|
},
|
||||||
|
".cm-content": {
|
||||||
|
minHeight: "100%",
|
||||||
|
padding: "24px"
|
||||||
|
},
|
||||||
|
".cm-line": {
|
||||||
|
padding: "0"
|
||||||
|
},
|
||||||
|
".cm-gutters": {
|
||||||
|
border: "0",
|
||||||
|
backgroundColor: "#eef1ef",
|
||||||
|
color: "#8a9690"
|
||||||
|
},
|
||||||
|
".cm-activeLine, .cm-activeLineGutter": {
|
||||||
|
backgroundColor: "rgb(116 168 143 / 9%)"
|
||||||
|
},
|
||||||
|
".cm-selectionBackground, &.cm-focused .cm-selectionBackground": {
|
||||||
|
backgroundColor: "rgb(89 135 113 / 24%)"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export const MarkdownEditor = forwardRef<
|
||||||
|
MarkdownEditorHandle,
|
||||||
|
MarkdownEditorProps
|
||||||
|
>(function MarkdownEditor(
|
||||||
|
{
|
||||||
|
value,
|
||||||
|
documentVersion,
|
||||||
|
onChange,
|
||||||
|
onEditorReady,
|
||||||
|
onScroll
|
||||||
|
},
|
||||||
|
forwardedRef
|
||||||
|
) {
|
||||||
|
const codeMirrorRef = useRef<ReactCodeMirrorRef>(null);
|
||||||
|
const viewRef = useRef<EditorView | null>(null);
|
||||||
|
const shortcutBindings = useMemo(
|
||||||
|
() =>
|
||||||
|
markdownEditorCommands.flatMap<KeyBinding>((command) =>
|
||||||
|
command.shortcut
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
key: command.shortcut,
|
||||||
|
run: (view) =>
|
||||||
|
executeMarkdownEditorCommand(command.id, { view })
|
||||||
|
}
|
||||||
|
]
|
||||||
|
: []
|
||||||
|
),
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
const extensions = useMemo(
|
||||||
|
() => [markdown(), keymap.of(shortcutBindings)],
|
||||||
|
[shortcutBindings]
|
||||||
|
);
|
||||||
|
|
||||||
|
useImperativeHandle(
|
||||||
|
forwardedRef,
|
||||||
|
() => ({
|
||||||
|
focus: () => viewRef.current?.focus(),
|
||||||
|
getScrollElement: () => viewRef.current?.scrollDOM ?? null,
|
||||||
|
getView: () => viewRef.current
|
||||||
|
}),
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(
|
||||||
|
() => () => {
|
||||||
|
onEditorReady?.(null);
|
||||||
|
viewRef.current = null;
|
||||||
|
},
|
||||||
|
[documentVersion, onEditorReady]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="markdown-editor"
|
||||||
|
onScrollCapture={onScroll}
|
||||||
|
>
|
||||||
|
<CodeMirror
|
||||||
|
key={documentVersion}
|
||||||
|
ref={codeMirrorRef}
|
||||||
|
value={value}
|
||||||
|
height="100%"
|
||||||
|
basicSetup={{
|
||||||
|
autocompletion: false,
|
||||||
|
bracketMatching: true,
|
||||||
|
closeBrackets: true,
|
||||||
|
foldGutter: false,
|
||||||
|
highlightActiveLine: true,
|
||||||
|
highlightActiveLineGutter: true,
|
||||||
|
lineNumbers: true,
|
||||||
|
searchKeymap: true
|
||||||
|
}}
|
||||||
|
extensions={extensions}
|
||||||
|
theme={editorTheme}
|
||||||
|
aria-label="Markdown 内容"
|
||||||
|
onChange={onChange}
|
||||||
|
onCreateEditor={(view) => {
|
||||||
|
viewRef.current = view;
|
||||||
|
onEditorReady?.(view);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
@@ -0,0 +1,208 @@
|
|||||||
|
import {
|
||||||
|
type KeyboardEvent,
|
||||||
|
useEffect,
|
||||||
|
useRef,
|
||||||
|
useState
|
||||||
|
} from "react";
|
||||||
|
import type { EditorView } from "@codemirror/view";
|
||||||
|
import { insertMarkdownTable } from "./markdown-editor-commands";
|
||||||
|
|
||||||
|
interface MarkdownTableToolProps {
|
||||||
|
editorView: EditorView | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const maximumColumns = 10;
|
||||||
|
const maximumRows = 10;
|
||||||
|
const minimumRows = 2;
|
||||||
|
|
||||||
|
interface TableSize {
|
||||||
|
columns: number;
|
||||||
|
rows: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
function clamp(value: number, minimum: number, maximum: number) {
|
||||||
|
return Math.min(maximum, Math.max(minimum, value));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function MarkdownTableTool({
|
||||||
|
editorView
|
||||||
|
}: MarkdownTableToolProps) {
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const [size, setSize] = useState<TableSize>({
|
||||||
|
columns: 3,
|
||||||
|
rows: 3
|
||||||
|
});
|
||||||
|
const rootRef = useRef<HTMLDivElement>(null);
|
||||||
|
const triggerRef = useRef<HTMLButtonElement>(null);
|
||||||
|
|
||||||
|
function focusSize(nextSize: TableSize) {
|
||||||
|
window.requestAnimationFrame(() => {
|
||||||
|
rootRef.current
|
||||||
|
?.querySelector<HTMLButtonElement>(
|
||||||
|
`[data-table-size="${nextSize.columns}x${nextSize.rows}"]`
|
||||||
|
)
|
||||||
|
?.focus();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!open) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
focusSize(size);
|
||||||
|
const handlePointerDown = (event: PointerEvent) => {
|
||||||
|
if (
|
||||||
|
event.target instanceof Node &&
|
||||||
|
!rootRef.current?.contains(event.target)
|
||||||
|
) {
|
||||||
|
setOpen(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const handleKeyDown = (event: globalThis.KeyboardEvent) => {
|
||||||
|
if (event.key === "Escape") {
|
||||||
|
event.preventDefault();
|
||||||
|
setOpen(false);
|
||||||
|
triggerRef.current?.focus();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
document.addEventListener("pointerdown", handlePointerDown);
|
||||||
|
document.addEventListener("keydown", handleKeyDown);
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener("pointerdown", handlePointerDown);
|
||||||
|
document.removeEventListener("keydown", handleKeyDown);
|
||||||
|
};
|
||||||
|
}, [open]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!editorView) {
|
||||||
|
setOpen(false);
|
||||||
|
}
|
||||||
|
}, [editorView]);
|
||||||
|
|
||||||
|
function moveSelection(
|
||||||
|
event: KeyboardEvent<HTMLButtonElement>,
|
||||||
|
columnsDelta: number,
|
||||||
|
rowsDelta: number
|
||||||
|
) {
|
||||||
|
event.preventDefault();
|
||||||
|
const nextSize = {
|
||||||
|
columns: clamp(
|
||||||
|
size.columns + columnsDelta,
|
||||||
|
1,
|
||||||
|
maximumColumns
|
||||||
|
),
|
||||||
|
rows: clamp(
|
||||||
|
size.rows + rowsDelta,
|
||||||
|
minimumRows,
|
||||||
|
maximumRows
|
||||||
|
)
|
||||||
|
};
|
||||||
|
setSize(nextSize);
|
||||||
|
focusSize(nextSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
function insertTable(selectedSize: TableSize) {
|
||||||
|
if (!editorView) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
insertMarkdownTable(
|
||||||
|
editorView,
|
||||||
|
selectedSize.columns,
|
||||||
|
selectedSize.rows
|
||||||
|
);
|
||||||
|
setOpen(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="markdown-table-tool" ref={rootRef}>
|
||||||
|
<button
|
||||||
|
ref={triggerRef}
|
||||||
|
type="button"
|
||||||
|
className="markdown-toolbar-button"
|
||||||
|
aria-label="插入表格"
|
||||||
|
aria-haspopup="dialog"
|
||||||
|
aria-expanded={open}
|
||||||
|
title="插入表格"
|
||||||
|
disabled={!editorView}
|
||||||
|
onMouseDown={(event) => event.preventDefault()}
|
||||||
|
onClick={() => setOpen((current) => !current)}
|
||||||
|
>
|
||||||
|
表格
|
||||||
|
</button>
|
||||||
|
{open ? (
|
||||||
|
<div
|
||||||
|
className="markdown-table-popover"
|
||||||
|
role="dialog"
|
||||||
|
aria-label="选择表格尺寸"
|
||||||
|
>
|
||||||
|
<strong>
|
||||||
|
{size.columns} 列 × {size.rows} 行
|
||||||
|
</strong>
|
||||||
|
<span>首行为表头</span>
|
||||||
|
<div
|
||||||
|
className="markdown-table-grid"
|
||||||
|
role="grid"
|
||||||
|
aria-label="表格尺寸"
|
||||||
|
aria-rowcount={maximumRows - minimumRows + 1}
|
||||||
|
aria-colcount={maximumColumns}
|
||||||
|
>
|
||||||
|
{Array.from(
|
||||||
|
{
|
||||||
|
length:
|
||||||
|
maximumColumns *
|
||||||
|
(maximumRows - minimumRows + 1)
|
||||||
|
},
|
||||||
|
(_, index) => {
|
||||||
|
const columns = (index % maximumColumns) + 1;
|
||||||
|
const rows =
|
||||||
|
Math.floor(index / maximumColumns) + minimumRows;
|
||||||
|
const selected =
|
||||||
|
columns <= size.columns && rows <= size.rows;
|
||||||
|
const active =
|
||||||
|
columns === size.columns && rows === size.rows;
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={`${columns}x${rows}`}
|
||||||
|
type="button"
|
||||||
|
role="gridcell"
|
||||||
|
className={
|
||||||
|
selected
|
||||||
|
? "markdown-table-cell is-selected"
|
||||||
|
: "markdown-table-cell"
|
||||||
|
}
|
||||||
|
data-table-size={`${columns}x${rows}`}
|
||||||
|
aria-label={`插入 ${columns} 列 × ${rows} 行表格`}
|
||||||
|
aria-selected={active}
|
||||||
|
tabIndex={active ? 0 : -1}
|
||||||
|
onPointerEnter={() =>
|
||||||
|
setSize({ columns, rows })
|
||||||
|
}
|
||||||
|
onFocus={() => setSize({ columns, rows })}
|
||||||
|
onKeyDown={(event) => {
|
||||||
|
if (event.key === "ArrowLeft") {
|
||||||
|
moveSelection(event, -1, 0);
|
||||||
|
} else if (event.key === "ArrowRight") {
|
||||||
|
moveSelection(event, 1, 0);
|
||||||
|
} else if (event.key === "ArrowUp") {
|
||||||
|
moveSelection(event, 0, -1);
|
||||||
|
} else if (event.key === "ArrowDown") {
|
||||||
|
moveSelection(event, 0, 1);
|
||||||
|
} else if (
|
||||||
|
event.key === "Enter" ||
|
||||||
|
event.key === " "
|
||||||
|
) {
|
||||||
|
event.preventDefault();
|
||||||
|
insertTable({ columns, rows });
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onClick={() => insertTable({ columns, rows })}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
import type { MouseEvent } from "react";
|
||||||
|
import type { EditorView } from "@codemirror/view";
|
||||||
|
import {
|
||||||
|
executeMarkdownEditorCommand,
|
||||||
|
getMarkdownEditorCommand
|
||||||
|
} from "./markdown-editor-commands";
|
||||||
|
import { MarkdownTableTool } from "./MarkdownTableTool";
|
||||||
|
|
||||||
|
interface MarkdownToolbarProps {
|
||||||
|
editorView: EditorView | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const toolbarGroups = [
|
||||||
|
["undo", "redo"],
|
||||||
|
["bold", "italic", "strikethrough", "inline-code"],
|
||||||
|
[
|
||||||
|
"code-block",
|
||||||
|
"quote",
|
||||||
|
"unordered-list",
|
||||||
|
"ordered-list",
|
||||||
|
"task-list"
|
||||||
|
],
|
||||||
|
["table", "horizontal-rule"]
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
function preserveEditorSelection(event: MouseEvent<HTMLElement>) {
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function MarkdownToolbar({
|
||||||
|
editorView
|
||||||
|
}: MarkdownToolbarProps) {
|
||||||
|
function execute(commandId: string) {
|
||||||
|
if (!editorView) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
executeMarkdownEditorCommand(commandId, { view: editorView });
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="markdown-toolbar"
|
||||||
|
role="toolbar"
|
||||||
|
aria-label="Markdown 格式工具栏"
|
||||||
|
>
|
||||||
|
<label className="markdown-toolbar-heading">
|
||||||
|
<span className="sr-only">标题级别</span>
|
||||||
|
<select
|
||||||
|
aria-label="设置标题级别"
|
||||||
|
defaultValue=""
|
||||||
|
disabled={!editorView}
|
||||||
|
onChange={(event) => {
|
||||||
|
if (event.target.value) {
|
||||||
|
execute(event.target.value);
|
||||||
|
event.target.value = "";
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<option value="">标题</option>
|
||||||
|
{Array.from({ length: 6 }, (_, index) => (
|
||||||
|
<option
|
||||||
|
key={index + 1}
|
||||||
|
value={`heading-${index + 1}`}
|
||||||
|
>
|
||||||
|
H{index + 1}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
{toolbarGroups.map((group, groupIndex) => (
|
||||||
|
<div
|
||||||
|
className="markdown-toolbar-group"
|
||||||
|
key={group.join("-")}
|
||||||
|
aria-label={`工具组 ${groupIndex + 1}`}
|
||||||
|
>
|
||||||
|
{group.map((commandId) => {
|
||||||
|
if (commandId === "table") {
|
||||||
|
return (
|
||||||
|
<MarkdownTableTool
|
||||||
|
key={commandId}
|
||||||
|
editorView={editorView}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const command = getMarkdownEditorCommand(commandId);
|
||||||
|
if (!command) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const disabled =
|
||||||
|
!editorView ||
|
||||||
|
(command.isEnabled?.({ view: editorView }) === false);
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={command.id}
|
||||||
|
type="button"
|
||||||
|
className={`markdown-toolbar-button command-${command.id}`}
|
||||||
|
aria-label={command.label}
|
||||||
|
title={
|
||||||
|
command.shortcut
|
||||||
|
? `${command.label}(${command.shortcut})`
|
||||||
|
: command.label
|
||||||
|
}
|
||||||
|
disabled={disabled}
|
||||||
|
onMouseDown={preserveEditorSelection}
|
||||||
|
onClick={() => execute(command.id)}
|
||||||
|
>
|
||||||
|
{command.toolbarLabel}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,514 @@
|
|||||||
|
import {
|
||||||
|
EditorSelection,
|
||||||
|
type EditorState,
|
||||||
|
type SelectionRange
|
||||||
|
} from "@codemirror/state";
|
||||||
|
import {
|
||||||
|
redo,
|
||||||
|
redoDepth,
|
||||||
|
undo,
|
||||||
|
undoDepth
|
||||||
|
} from "@codemirror/commands";
|
||||||
|
import type { EditorView } from "@codemirror/view";
|
||||||
|
|
||||||
|
export type MarkdownEditorCommandGroup =
|
||||||
|
| "history"
|
||||||
|
| "text"
|
||||||
|
| "block"
|
||||||
|
| "insert"
|
||||||
|
| "document";
|
||||||
|
|
||||||
|
export interface EditorCommandContext {
|
||||||
|
view: EditorView;
|
||||||
|
openPanel?: (panelId: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MarkdownEditorCommand {
|
||||||
|
id: string;
|
||||||
|
group: MarkdownEditorCommandGroup;
|
||||||
|
label: string;
|
||||||
|
toolbarLabel: string;
|
||||||
|
shortcut?: string;
|
||||||
|
isEnabled?: (context: EditorCommandContext) => boolean;
|
||||||
|
execute: (
|
||||||
|
context: EditorCommandContext
|
||||||
|
) => void | Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MarkdownTextEdit {
|
||||||
|
insert: string;
|
||||||
|
selectionFrom: number;
|
||||||
|
selectionTo: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
type SelectionEditor = (
|
||||||
|
selectedText: string,
|
||||||
|
range: SelectionRange,
|
||||||
|
state: EditorState
|
||||||
|
) => MarkdownTextEdit;
|
||||||
|
|
||||||
|
export function wrapMarkdownSelection(
|
||||||
|
selectedText: string,
|
||||||
|
prefix: string,
|
||||||
|
suffix = prefix,
|
||||||
|
placeholder = "文本"
|
||||||
|
): MarkdownTextEdit {
|
||||||
|
if (
|
||||||
|
selectedText.length >= prefix.length + suffix.length &&
|
||||||
|
selectedText.startsWith(prefix) &&
|
||||||
|
selectedText.endsWith(suffix)
|
||||||
|
) {
|
||||||
|
const insert = selectedText.slice(
|
||||||
|
prefix.length,
|
||||||
|
selectedText.length - suffix.length
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
insert,
|
||||||
|
selectionFrom: 0,
|
||||||
|
selectionTo: insert.length
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const content = selectedText || placeholder;
|
||||||
|
return {
|
||||||
|
insert: `${prefix}${content}${suffix}`,
|
||||||
|
selectionFrom: prefix.length,
|
||||||
|
selectionTo: prefix.length + content.length
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapNonBlankLines(
|
||||||
|
text: string,
|
||||||
|
mapper: (line: string, index: number) => string
|
||||||
|
) {
|
||||||
|
let contentLineIndex = 0;
|
||||||
|
return text
|
||||||
|
.split("\n")
|
||||||
|
.map((line) => {
|
||||||
|
if (!line.trim()) {
|
||||||
|
return line;
|
||||||
|
}
|
||||||
|
const mapped = mapper(line, contentLineIndex);
|
||||||
|
contentLineIndex += 1;
|
||||||
|
return mapped;
|
||||||
|
})
|
||||||
|
.join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
export type MarkdownLinePrefix =
|
||||||
|
| "quote"
|
||||||
|
| "unordered-list"
|
||||||
|
| "ordered-list"
|
||||||
|
| "task-list";
|
||||||
|
|
||||||
|
const linePrefixPatterns: Record<MarkdownLinePrefix, RegExp> = {
|
||||||
|
quote: /^>\s?/u,
|
||||||
|
"unordered-list": /^[-*+]\s+/u,
|
||||||
|
"ordered-list": /^\d+[.)]\s+/u,
|
||||||
|
"task-list": /^[-*+]\s+\[[ xX]\]\s+/u
|
||||||
|
};
|
||||||
|
|
||||||
|
export function toggleMarkdownLinePrefix(
|
||||||
|
text: string,
|
||||||
|
kind: MarkdownLinePrefix
|
||||||
|
) {
|
||||||
|
const pattern = linePrefixPatterns[kind];
|
||||||
|
const nonBlankLines = text
|
||||||
|
.split("\n")
|
||||||
|
.filter((line) => line.trim());
|
||||||
|
const shouldRemove =
|
||||||
|
nonBlankLines.length > 0 &&
|
||||||
|
nonBlankLines.every((line) => pattern.test(line));
|
||||||
|
|
||||||
|
return mapNonBlankLines(text, (line, index) => {
|
||||||
|
if (shouldRemove) {
|
||||||
|
return line.replace(pattern, "");
|
||||||
|
}
|
||||||
|
if (kind === "quote") {
|
||||||
|
return `> ${line.replace(linePrefixPatterns.quote, "")}`;
|
||||||
|
}
|
||||||
|
if (kind === "ordered-list") {
|
||||||
|
return `${index + 1}. ${line.replace(
|
||||||
|
/^(?:[-*+]\s+(?:\[[ xX]\]\s+)?|\d+[.)]\s+)/u,
|
||||||
|
""
|
||||||
|
)}`;
|
||||||
|
}
|
||||||
|
if (kind === "task-list") {
|
||||||
|
return `- [ ] ${line.replace(
|
||||||
|
/^(?:[-*+]\s+(?:\[[ xX]\]\s+)?|\d+[.)]\s+)/u,
|
||||||
|
""
|
||||||
|
)}`;
|
||||||
|
}
|
||||||
|
return `- ${line.replace(
|
||||||
|
/^(?:[-*+]\s+(?:\[[ xX]\]\s+)?|\d+[.)]\s+)/u,
|
||||||
|
""
|
||||||
|
)}`;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setMarkdownHeading(text: string, level: number) {
|
||||||
|
const prefix = level > 0 ? `${"#".repeat(level)} ` : "";
|
||||||
|
return mapNonBlankLines(
|
||||||
|
text,
|
||||||
|
(line) => `${prefix}${line.replace(/^#{1,6}\s+/u, "")}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createMarkdownTable(
|
||||||
|
columnCount: number,
|
||||||
|
rowCount: number
|
||||||
|
): MarkdownTextEdit {
|
||||||
|
const columns = Math.trunc(columnCount);
|
||||||
|
const rows = Math.trunc(rowCount);
|
||||||
|
if (columns < 1 || columns > 10 || rows < 2 || rows > 10) {
|
||||||
|
throw new RangeError("表格尺寸必须为 1~10 列、2~10 行");
|
||||||
|
}
|
||||||
|
|
||||||
|
const headerCells = Array.from(
|
||||||
|
{ length: columns },
|
||||||
|
(_, index) => `列 ${index + 1}`
|
||||||
|
);
|
||||||
|
const separatorCells = Array.from(
|
||||||
|
{ length: columns },
|
||||||
|
() => "---"
|
||||||
|
);
|
||||||
|
const contentRow = `| ${Array.from(
|
||||||
|
{ length: columns },
|
||||||
|
() => "内容"
|
||||||
|
).join(" | ")} |`;
|
||||||
|
const insert = [
|
||||||
|
`| ${headerCells.join(" | ")} |`,
|
||||||
|
`| ${separatorCells.join(" | ")} |`,
|
||||||
|
...Array.from({ length: rows - 1 }, () => contentRow)
|
||||||
|
].join("\n");
|
||||||
|
return {
|
||||||
|
insert,
|
||||||
|
selectionFrom: 2,
|
||||||
|
selectionTo: 5
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function replaceSelections(
|
||||||
|
view: EditorView,
|
||||||
|
createEdit: SelectionEditor
|
||||||
|
) {
|
||||||
|
const transaction = view.state.changeByRange((range) => {
|
||||||
|
const selectedText = view.state.sliceDoc(range.from, range.to);
|
||||||
|
const edit = createEdit(selectedText, range, view.state);
|
||||||
|
return {
|
||||||
|
changes: {
|
||||||
|
from: range.from,
|
||||||
|
to: range.to,
|
||||||
|
insert: edit.insert
|
||||||
|
},
|
||||||
|
range: EditorSelection.range(
|
||||||
|
range.from + edit.selectionFrom,
|
||||||
|
range.from + edit.selectionTo
|
||||||
|
)
|
||||||
|
};
|
||||||
|
});
|
||||||
|
view.dispatch(transaction);
|
||||||
|
view.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
function editSelectedLines(
|
||||||
|
view: EditorView,
|
||||||
|
transform: (text: string) => string
|
||||||
|
) {
|
||||||
|
const transaction = view.state.changeByRange((range) => {
|
||||||
|
const startLine = view.state.doc.lineAt(range.from);
|
||||||
|
const endProbe =
|
||||||
|
range.to > range.from &&
|
||||||
|
view.state.doc.lineAt(range.to).from === range.to
|
||||||
|
? range.to - 1
|
||||||
|
: range.to;
|
||||||
|
const endLine = view.state.doc.lineAt(endProbe);
|
||||||
|
const selectedLines = view.state.sliceDoc(
|
||||||
|
startLine.from,
|
||||||
|
endLine.to
|
||||||
|
);
|
||||||
|
const insert = transform(selectedLines);
|
||||||
|
return {
|
||||||
|
changes: {
|
||||||
|
from: startLine.from,
|
||||||
|
to: endLine.to,
|
||||||
|
insert
|
||||||
|
},
|
||||||
|
range: EditorSelection.range(
|
||||||
|
startLine.from,
|
||||||
|
startLine.from + insert.length
|
||||||
|
)
|
||||||
|
};
|
||||||
|
});
|
||||||
|
view.dispatch(transaction);
|
||||||
|
view.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
function insertMarkdownBlock(
|
||||||
|
block: string,
|
||||||
|
range?: SelectionRange,
|
||||||
|
state?: EditorState
|
||||||
|
): MarkdownTextEdit {
|
||||||
|
const before =
|
||||||
|
range && state ? state.sliceDoc(0, range.from) : "";
|
||||||
|
const after = range && state ? state.sliceDoc(range.to) : "";
|
||||||
|
const leading =
|
||||||
|
!before || before.endsWith("\n\n")
|
||||||
|
? ""
|
||||||
|
: before.endsWith("\n")
|
||||||
|
? "\n"
|
||||||
|
: "\n\n";
|
||||||
|
const trailing =
|
||||||
|
!after || after.startsWith("\n\n")
|
||||||
|
? ""
|
||||||
|
: after.startsWith("\n")
|
||||||
|
? "\n"
|
||||||
|
: "\n\n";
|
||||||
|
const insert = `${leading}${block}${trailing}`;
|
||||||
|
return {
|
||||||
|
insert,
|
||||||
|
selectionFrom: leading.length,
|
||||||
|
selectionTo: leading.length + block.length
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function insertMarkdownTable(
|
||||||
|
view: EditorView,
|
||||||
|
columnCount: number,
|
||||||
|
rowCount: number
|
||||||
|
) {
|
||||||
|
replaceSelections(view, (_, range, state) => {
|
||||||
|
const table = createMarkdownTable(columnCount, rowCount);
|
||||||
|
const block = insertMarkdownBlock(
|
||||||
|
table.insert,
|
||||||
|
range,
|
||||||
|
state
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
insert: block.insert,
|
||||||
|
selectionFrom: block.selectionFrom + table.selectionFrom,
|
||||||
|
selectionTo: block.selectionFrom + table.selectionTo
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function createInlineCommand(
|
||||||
|
id: string,
|
||||||
|
label: string,
|
||||||
|
toolbarLabel: string,
|
||||||
|
prefix: string,
|
||||||
|
suffix = prefix,
|
||||||
|
placeholder = "文本",
|
||||||
|
shortcut?: string
|
||||||
|
): MarkdownEditorCommand {
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
group: "text",
|
||||||
|
label,
|
||||||
|
toolbarLabel,
|
||||||
|
...(shortcut ? { shortcut } : {}),
|
||||||
|
execute: ({ view }) =>
|
||||||
|
replaceSelections(view, (selectedText) =>
|
||||||
|
wrapMarkdownSelection(
|
||||||
|
selectedText,
|
||||||
|
prefix,
|
||||||
|
suffix,
|
||||||
|
placeholder
|
||||||
|
)
|
||||||
|
)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function createLineCommand(
|
||||||
|
id: string,
|
||||||
|
label: string,
|
||||||
|
toolbarLabel: string,
|
||||||
|
kind: MarkdownLinePrefix,
|
||||||
|
shortcut?: string
|
||||||
|
): MarkdownEditorCommand {
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
group: "block",
|
||||||
|
label,
|
||||||
|
toolbarLabel,
|
||||||
|
...(shortcut ? { shortcut } : {}),
|
||||||
|
execute: ({ view }) =>
|
||||||
|
editSelectedLines(view, (text) =>
|
||||||
|
toggleMarkdownLinePrefix(text, kind)
|
||||||
|
)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const historyCommands: MarkdownEditorCommand[] = [
|
||||||
|
{
|
||||||
|
id: "undo",
|
||||||
|
group: "history",
|
||||||
|
label: "撤销",
|
||||||
|
toolbarLabel: "↶",
|
||||||
|
shortcut: "Mod-z",
|
||||||
|
isEnabled: ({ view }) => undoDepth(view.state) > 0,
|
||||||
|
execute: ({ view }) => {
|
||||||
|
undo(view);
|
||||||
|
view.focus();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "redo",
|
||||||
|
group: "history",
|
||||||
|
label: "重做",
|
||||||
|
toolbarLabel: "↷",
|
||||||
|
shortcut: "Mod-Shift-z",
|
||||||
|
isEnabled: ({ view }) => redoDepth(view.state) > 0,
|
||||||
|
execute: ({ view }) => {
|
||||||
|
redo(view);
|
||||||
|
view.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const textCommands: MarkdownEditorCommand[] = [
|
||||||
|
createInlineCommand(
|
||||||
|
"bold",
|
||||||
|
"加粗",
|
||||||
|
"B",
|
||||||
|
"**",
|
||||||
|
"**",
|
||||||
|
"粗体文本",
|
||||||
|
"Mod-b"
|
||||||
|
),
|
||||||
|
createInlineCommand(
|
||||||
|
"italic",
|
||||||
|
"斜体",
|
||||||
|
"I",
|
||||||
|
"*",
|
||||||
|
"*",
|
||||||
|
"斜体文本",
|
||||||
|
"Mod-i"
|
||||||
|
),
|
||||||
|
createInlineCommand(
|
||||||
|
"strikethrough",
|
||||||
|
"删除线",
|
||||||
|
"S",
|
||||||
|
"~~",
|
||||||
|
"~~",
|
||||||
|
"删除线文本",
|
||||||
|
"Mod-Shift-x"
|
||||||
|
),
|
||||||
|
createInlineCommand(
|
||||||
|
"inline-code",
|
||||||
|
"行内代码",
|
||||||
|
"</>",
|
||||||
|
"`",
|
||||||
|
"`",
|
||||||
|
"code"
|
||||||
|
)
|
||||||
|
];
|
||||||
|
|
||||||
|
const blockCommands: MarkdownEditorCommand[] = [
|
||||||
|
{
|
||||||
|
id: "code-block",
|
||||||
|
group: "block",
|
||||||
|
label: "代码块",
|
||||||
|
toolbarLabel: "代码块",
|
||||||
|
execute: ({ view }) =>
|
||||||
|
replaceSelections(view, (selectedText) =>
|
||||||
|
wrapMarkdownSelection(
|
||||||
|
selectedText,
|
||||||
|
"```\n",
|
||||||
|
"\n```",
|
||||||
|
"code"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
createLineCommand("quote", "引用", "❞", "quote"),
|
||||||
|
createLineCommand(
|
||||||
|
"unordered-list",
|
||||||
|
"无序列表",
|
||||||
|
"• 列表",
|
||||||
|
"unordered-list",
|
||||||
|
"Mod-Shift-8"
|
||||||
|
),
|
||||||
|
createLineCommand(
|
||||||
|
"ordered-list",
|
||||||
|
"有序列表",
|
||||||
|
"1. 列表",
|
||||||
|
"ordered-list",
|
||||||
|
"Mod-Shift-7"
|
||||||
|
),
|
||||||
|
createLineCommand("task-list", "任务列表", "☑", "task-list")
|
||||||
|
];
|
||||||
|
|
||||||
|
const insertCommands: MarkdownEditorCommand[] = [
|
||||||
|
{
|
||||||
|
id: "table",
|
||||||
|
group: "insert",
|
||||||
|
label: "插入表格",
|
||||||
|
toolbarLabel: "表格",
|
||||||
|
execute: ({ view, openPanel }) => {
|
||||||
|
if (openPanel) {
|
||||||
|
openPanel("table");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
insertMarkdownTable(view, 3, 3);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "horizontal-rule",
|
||||||
|
group: "insert",
|
||||||
|
label: "插入分隔线",
|
||||||
|
toolbarLabel: "—",
|
||||||
|
execute: ({ view }) =>
|
||||||
|
replaceSelections(view, (_, range, state) =>
|
||||||
|
insertMarkdownBlock("---", range, state)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const headingCommands = Array.from(
|
||||||
|
{ length: 6 },
|
||||||
|
(_, index): MarkdownEditorCommand => {
|
||||||
|
const level = index + 1;
|
||||||
|
return {
|
||||||
|
id: `heading-${level}`,
|
||||||
|
group: "block",
|
||||||
|
label: `${level} 级标题`,
|
||||||
|
toolbarLabel: `H${level}`,
|
||||||
|
execute: ({ view }) =>
|
||||||
|
editSelectedLines(view, (text) =>
|
||||||
|
setMarkdownHeading(text, level)
|
||||||
|
)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export const markdownEditorCommands: readonly MarkdownEditorCommand[] =
|
||||||
|
[
|
||||||
|
...historyCommands,
|
||||||
|
...headingCommands,
|
||||||
|
...textCommands,
|
||||||
|
...blockCommands,
|
||||||
|
...insertCommands
|
||||||
|
];
|
||||||
|
|
||||||
|
const markdownEditorCommandMap = new Map(
|
||||||
|
markdownEditorCommands.map((command) => [command.id, command])
|
||||||
|
);
|
||||||
|
|
||||||
|
export function getMarkdownEditorCommand(commandId: string) {
|
||||||
|
return markdownEditorCommandMap.get(commandId);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function executeMarkdownEditorCommand(
|
||||||
|
commandId: string,
|
||||||
|
context: EditorCommandContext
|
||||||
|
) {
|
||||||
|
const command = getMarkdownEditorCommand(commandId);
|
||||||
|
if (
|
||||||
|
!command ||
|
||||||
|
(command.isEnabled && !command.isEnabled(context))
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
void command.execute(context);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
+123
-13
@@ -551,24 +551,134 @@ h1 {
|
|||||||
border: 0;
|
border: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
textarea {
|
.markdown-editor {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
padding: 24px;
|
|
||||||
border: 0;
|
|
||||||
outline: 0;
|
|
||||||
resize: none;
|
|
||||||
background: #f6f7f5;
|
background: #f6f7f5;
|
||||||
color: #26312d;
|
overflow: hidden;
|
||||||
font-family: "Cascadia Code", "SFMono-Regular", Consolas, monospace;
|
|
||||||
font-size: 0.86rem;
|
|
||||||
line-height: 1.72;
|
|
||||||
tab-size: 2;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
textarea:focus {
|
.markdown-editor > div,
|
||||||
box-shadow: inset 3px 0 #74a88f;
|
.markdown-editor .cm-editor {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-toolbar {
|
||||||
|
display: flex;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
min-height: 44px;
|
||||||
|
padding: 6px 10px;
|
||||||
|
border-bottom: 1px solid #d9dedb;
|
||||||
|
background: #f8faf9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-toolbar-group {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 3px;
|
||||||
|
padding-left: 6px;
|
||||||
|
border-left: 1px solid #d9dedb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-toolbar-button {
|
||||||
|
min-width: 32px;
|
||||||
|
min-height: 30px;
|
||||||
|
padding: 0 7px;
|
||||||
|
border-color: transparent;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: transparent;
|
||||||
|
color: #41584e;
|
||||||
|
font-size: 0.72rem;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-toolbar-button:hover:not(:disabled) {
|
||||||
|
border-color: #c3d1ca;
|
||||||
|
background: #e8efeb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-toolbar-button.command-bold {
|
||||||
|
font-weight: 900;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-toolbar-button.command-italic {
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-toolbar-button.command-strikethrough {
|
||||||
|
text-decoration: line-through;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-toolbar-heading select {
|
||||||
|
height: 30px;
|
||||||
|
padding: 0 24px 0 8px;
|
||||||
|
border: 1px solid #c9d4ce;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: #fff;
|
||||||
|
color: #41584e;
|
||||||
|
font-size: 0.72rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-table-tool {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-table-popover {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 30;
|
||||||
|
top: calc(100% + 8px);
|
||||||
|
left: 0;
|
||||||
|
display: flex;
|
||||||
|
width: 250px;
|
||||||
|
padding: 12px;
|
||||||
|
border: 1px solid #bfcac4;
|
||||||
|
border-radius: 9px;
|
||||||
|
background: #fff;
|
||||||
|
box-shadow: 0 12px 30px rgb(40 58 49 / 18%);
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-table-popover strong {
|
||||||
|
color: #2f493e;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-table-popover > span {
|
||||||
|
color: #718078;
|
||||||
|
font-size: 0.68rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-table-grid {
|
||||||
|
display: grid;
|
||||||
|
margin-top: 6px;
|
||||||
|
grid-template-columns: repeat(10, 18px);
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-table-cell {
|
||||||
|
width: 18px;
|
||||||
|
min-width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
min-height: 18px;
|
||||||
|
padding: 0;
|
||||||
|
border: 1px solid #cbd4cf;
|
||||||
|
border-radius: 3px;
|
||||||
|
background: #f4f7f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-table-cell.is-selected {
|
||||||
|
border-color: #65957d;
|
||||||
|
background: #a9cbb9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-table-cell:focus-visible {
|
||||||
|
outline: 2px solid #356c55;
|
||||||
|
outline-offset: 1px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.preview-panel {
|
.preview-panel {
|
||||||
@@ -1190,7 +1300,7 @@ textarea:focus {
|
|||||||
min-height: 650px;
|
min-height: 650px;
|
||||||
}
|
}
|
||||||
|
|
||||||
textarea {
|
.markdown-editor {
|
||||||
min-height: 560px;
|
min-height: 560px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,87 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import {
|
||||||
|
createMarkdownTable,
|
||||||
|
setMarkdownHeading,
|
||||||
|
toggleMarkdownLinePrefix,
|
||||||
|
wrapMarkdownSelection
|
||||||
|
} from "../src/markdown-editor-commands";
|
||||||
|
|
||||||
|
describe("Markdown 编辑器命令", () => {
|
||||||
|
it("包装、取消包装文本并保持内容选区", () => {
|
||||||
|
expect(wrapMarkdownSelection("正文", "**")).toEqual({
|
||||||
|
insert: "**正文**",
|
||||||
|
selectionFrom: 2,
|
||||||
|
selectionTo: 4
|
||||||
|
});
|
||||||
|
expect(wrapMarkdownSelection("**正文**", "**")).toEqual({
|
||||||
|
insert: "正文",
|
||||||
|
selectionFrom: 0,
|
||||||
|
selectionTo: 2
|
||||||
|
});
|
||||||
|
expect(
|
||||||
|
wrapMarkdownSelection("", "`", "`", "code")
|
||||||
|
).toEqual({
|
||||||
|
insert: "`code`",
|
||||||
|
selectionFrom: 1,
|
||||||
|
selectionTo: 5
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("切换引用和无序列表前缀", () => {
|
||||||
|
expect(toggleMarkdownLinePrefix("甲\n乙", "quote")).toBe(
|
||||||
|
"> 甲\n> 乙"
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
toggleMarkdownLinePrefix("> 甲\n> 乙", "quote")
|
||||||
|
).toBe("甲\n乙");
|
||||||
|
expect(
|
||||||
|
toggleMarkdownLinePrefix("1. 甲\n2. 乙", "unordered-list")
|
||||||
|
).toBe("- 甲\n- 乙");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("为选中的多行生成连续有序列表并保留空行", () => {
|
||||||
|
expect(
|
||||||
|
toggleMarkdownLinePrefix(
|
||||||
|
"第一项\n\n第二项\n第三项",
|
||||||
|
"ordered-list"
|
||||||
|
)
|
||||||
|
).toBe("1. 第一项\n\n2. 第二项\n3. 第三项");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("为选中的多行生成无序列表和任务列表", () => {
|
||||||
|
expect(
|
||||||
|
toggleMarkdownLinePrefix(
|
||||||
|
"第一项\n第二项\n第三项",
|
||||||
|
"unordered-list"
|
||||||
|
)
|
||||||
|
).toBe("- 第一项\n- 第二项\n- 第三项");
|
||||||
|
expect(
|
||||||
|
toggleMarkdownLinePrefix(
|
||||||
|
"第一项\n第二项\n第三项",
|
||||||
|
"task-list"
|
||||||
|
)
|
||||||
|
).toBe("- [ ] 第一项\n- [ ] 第二项\n- [ ] 第三项");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("设置标题时替换现有标题级别", () => {
|
||||||
|
expect(setMarkdownHeading("## 标题\n正文", 3)).toBe(
|
||||||
|
"### 标题\n### 正文"
|
||||||
|
);
|
||||||
|
expect(setMarkdownHeading("### 标题", 1)).toBe("# 标题");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("生成指定列数和总行数的 Markdown 表格", () => {
|
||||||
|
expect(createMarkdownTable(3, 4)).toEqual({
|
||||||
|
insert:
|
||||||
|
"| 列 1 | 列 2 | 列 3 |\n" +
|
||||||
|
"| --- | --- | --- |\n" +
|
||||||
|
"| 内容 | 内容 | 内容 |\n" +
|
||||||
|
"| 内容 | 内容 | 内容 |\n" +
|
||||||
|
"| 内容 | 内容 | 内容 |",
|
||||||
|
selectionFrom: 2,
|
||||||
|
selectionTo: 5
|
||||||
|
});
|
||||||
|
expect(() => createMarkdownTable(0, 2)).toThrow(RangeError);
|
||||||
|
expect(() => createMarkdownTable(3, 11)).toThrow(RangeError);
|
||||||
|
});
|
||||||
|
});
|
||||||
+12
-1
@@ -83,6 +83,14 @@ Schema、资源、Pandoc capability、错误码、结果、诊断、耗时、MIM
|
|||||||
现有主题、图片和 Markdown 安全渲染能力,为后续 Server 与 Desktop DOCX
|
现有主题、图片和 Markdown 安全渲染能力,为后续 Server 与 Desktop DOCX
|
||||||
引擎返回同一种准备结果。本阶段未新增 HTTP 路由、IPC 或 Pandoc 调用。
|
引擎返回同一种准备结果。本阶段未新增 HTTP 路由、IPC 或 Pandoc 调用。
|
||||||
|
|
||||||
|
`v0.6.0` 阶段 4 已将左侧原生文本框升级为 CodeMirror 6,并建立项目
|
||||||
|
自有 Markdown 命令注册层和响应式基础工具栏。当前支持撤销、重做、
|
||||||
|
H1~H6、加粗、斜体、删除线、行内代码、代码块、引用、三类列表、尺寸
|
||||||
|
表格和分隔线;链接、图片等功能化工具延后。现有实现保留受控 Markdown
|
||||||
|
状态、防抖预览、文件工作流、文档脏状态、折叠恢复焦点及编辑/预览滚动
|
||||||
|
同步。外部文档替换会重置历史,单次工具栏操作形成一个 CodeMirror
|
||||||
|
事务。
|
||||||
|
|
||||||
## 2. 已完成
|
## 2. 已完成
|
||||||
|
|
||||||
### 2.1 项目骨架
|
### 2.1 项目骨架
|
||||||
@@ -919,6 +927,9 @@ ECharts 第二阶段浏览器与 PDF 验证结果:
|
|||||||
- `apps/web/src/PreviewPageControl.tsx` 和 `preview-page.ts` 负责当前页输入、
|
- `apps/web/src/PreviewPageControl.tsx` 和 `preview-page.ts` 负责当前页输入、
|
||||||
页码边界、滚动页识别和快速跳转。
|
页码边界、滚动页识别和快速跳转。
|
||||||
- `apps/web/src/preview-zoom.ts` 和 `PreviewZoomControl.tsx` 负责预览缩放缓存、边界和控制界面。
|
- `apps/web/src/preview-zoom.ts` 和 `PreviewZoomControl.tsx` 负责预览缩放缓存、边界和控制界面。
|
||||||
|
- `apps/web/src/MarkdownEditor.tsx` 负责 CodeMirror 受控状态、文档历史
|
||||||
|
重建、快捷键、焦点和滚动容器适配;`MarkdownToolbar.tsx` 与
|
||||||
|
`markdown-editor-commands.ts` 负责可扩展命令注册和基础工具栏。
|
||||||
- `packages/markdown-echarts` 负责 ECharts YAML 协议、验证、安全过滤、
|
- `packages/markdown-echarts` 负责 ECharts YAML 协议、验证、安全过滤、
|
||||||
Markdown-it 插件、浏览器 SVG 渲染和分页样式。
|
Markdown-it 插件、浏览器 SVG 渲染和分页样式。
|
||||||
- `apps/server/src/pdf-engine.ts` 负责 Chromium 生命周期、并发、网络限制、分页调用和 PDF 输出。
|
- `apps/server/src/pdf-engine.ts` 负责 Chromium 生命周期、并发、网络限制、分页调用和 PDF 输出。
|
||||||
@@ -962,7 +973,7 @@ ECharts 第二阶段浏览器与 PDF 验证结果:
|
|||||||
- 阶段 2:已冻结 Pandoc `3.9.0.2`、许可证、Docker 与 Desktop
|
- 阶段 2:已冻结 Pandoc `3.9.0.2`、许可证、Docker 与 Desktop
|
||||||
分发方式;
|
分发方式;
|
||||||
- 阶段 3:已实现 DOCX 共享模型和跨端导出协议;
|
- 阶段 3:已实现 DOCX 共享模型和跨端导出协议;
|
||||||
- 阶段 4:实现 CodeMirror 6 与可扩展基础 Markdown 工具栏;
|
- 阶段 4:已实现 CodeMirror 6 与可扩展基础 Markdown 工具栏;
|
||||||
- 阶段 5~8:依次实现资源预处理、动态 reference.docx、Pandoc 转换服务
|
- 阶段 5~8:依次实现资源预处理、动态 reference.docx、Pandoc 转换服务
|
||||||
以及 Web/Desktop 导出交互;
|
以及 Web/Desktop 导出交互;
|
||||||
- 阶段 9~10:完成自动化和 Word/WPS 互操作验收,再构建正式发布产物。
|
- 阶段 9~10:完成自动化和 Word/WPS 互操作验收,再构建正式发布产物。
|
||||||
|
|||||||
+22
-7
@@ -1,6 +1,7 @@
|
|||||||
# v0.6.0 DOCX 导出与 Markdown 工具栏设计
|
# v0.6.0 DOCX 导出与 Markdown 工具栏设计
|
||||||
|
|
||||||
状态:阶段 1~3 已完成,DOCX 架构、Pandoc 分发及共享协议已经冻结。
|
状态:阶段 1~4 已完成,DOCX 共享协议和 Markdown 编辑器基础能力已经
|
||||||
|
落地。
|
||||||
|
|
||||||
## 1. 版本目标
|
## 1. 版本目标
|
||||||
|
|
||||||
@@ -211,8 +212,8 @@ Desktop 只提供平台运行时、传输与保存能力。
|
|||||||
|
|
||||||
左侧编辑器采用:
|
左侧编辑器采用:
|
||||||
|
|
||||||
- `@uiw/react-codemirror`;
|
- `@uiw/react-codemirror` `4.25.11`;
|
||||||
- `@codemirror/lang-markdown`;
|
- `@codemirror/lang-markdown` `6.5.1`;
|
||||||
- CodeMirror 6 的状态、选区、事务、历史和快捷键扩展。
|
- CodeMirror 6 的状态、选区、事务、历史和快捷键扩展。
|
||||||
|
|
||||||
不引入带独立 Markdown 预览的完整编辑器,避免复制现有安全渲染和统一
|
不引入带独立 Markdown 预览的完整编辑器,避免复制现有安全渲染和统一
|
||||||
@@ -245,6 +246,11 @@ interface MarkdownEditorCommand {
|
|||||||
- 根据编辑器或平台状态禁用;
|
- 根据编辑器或平台状态禁用;
|
||||||
- 注册快捷键。
|
- 注册快捷键。
|
||||||
|
|
||||||
|
当前实现在 `apps/web/src/markdown-editor-commands.ts` 维护项目自有命令
|
||||||
|
注册表和纯文本变换函数,`MarkdownEditor.tsx` 只负责 CodeMirror 适配,
|
||||||
|
`MarkdownToolbar.tsx` 只负责 React 工具栏呈现。后续 Front Matter 或
|
||||||
|
ECharts 工具可以增加命令与面板入口,不需要修改编辑器内核。
|
||||||
|
|
||||||
### 5.3 v0.6.0 基础命令
|
### 5.3 v0.6.0 基础命令
|
||||||
|
|
||||||
- 撤销、重做;
|
- 撤销、重做;
|
||||||
@@ -253,12 +259,16 @@ interface MarkdownEditorCommand {
|
|||||||
- 行内代码、代码块;
|
- 行内代码、代码块;
|
||||||
- 引用;
|
- 引用;
|
||||||
- 有序列表、无序列表和任务列表;
|
- 有序列表、无序列表和任务列表;
|
||||||
- 链接、图片;
|
- 通过尺寸网格快速插入 `1~10` 列、`2~10` 行的表格;
|
||||||
- 表格;
|
|
||||||
- 分隔线。
|
- 分隔线。
|
||||||
|
|
||||||
Front Matter 表单工具和 ECharts YAML 工具不在 `v0.6.0` 实现,但必须
|
链接、图片、Front Matter 表单工具和 ECharts YAML 工具不在 `v0.6.0`
|
||||||
能够在后续版本中以命令和面板扩展接入,不再次更换编辑器。
|
实现,但必须能够在后续版本中以命令和面板扩展接入,不再次更换编辑器。
|
||||||
|
|
||||||
|
选中多行后执行有序列表、无序列表或任务列表,应在一次事务中为每个非空
|
||||||
|
行生成对应 Markdown 标记。有序列表按非空行连续编号。表格尺寸中的行数
|
||||||
|
包含表头,不包含 Markdown 分隔语法行;例如 `3 × 4` 生成 3 列、1 行
|
||||||
|
表头和 3 行内容。
|
||||||
|
|
||||||
### 5.4 兼容性要求
|
### 5.4 兼容性要求
|
||||||
|
|
||||||
@@ -278,6 +288,11 @@ Front Matter 表单工具和 ECharts YAML 工具不在 `v0.6.0` 实现,但必
|
|||||||
外部文件加载不得污染用户的撤销历史;工具栏的一次操作应形成一次清晰
|
外部文件加载不得污染用户的撤销历史;工具栏的一次操作应形成一次清晰
|
||||||
的撤销步骤。
|
的撤销步骤。
|
||||||
|
|
||||||
|
当前通过独立的文档版本标识重建 CodeMirror 状态:用户键入和工具栏操作
|
||||||
|
保留在当前撤销历史中;新建、打开、重载、教程及主题示例切换会开始新的
|
||||||
|
历史。编辑器适配层同时暴露真实滚动容器和聚焦能力,继续复用原有预览
|
||||||
|
滚动同步与折叠逻辑。
|
||||||
|
|
||||||
## 6. API、安全与运行约束
|
## 6. API、安全与运行约束
|
||||||
|
|
||||||
### 6.1 API 与 IPC
|
### 6.1 API 与 IPC
|
||||||
|
|||||||
Generated
+319
@@ -538,9 +538,14 @@
|
|||||||
"name": "@md-to-pdf/web",
|
"name": "@md-to-pdf/web",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@codemirror/commands": "^6.10.4",
|
||||||
|
"@codemirror/lang-markdown": "^6.5.1",
|
||||||
|
"@codemirror/state": "^6.7.1",
|
||||||
|
"@codemirror/view": "^6.43.7",
|
||||||
"@md-to-pdf/core": "0.1.0",
|
"@md-to-pdf/core": "0.1.0",
|
||||||
"@md-to-pdf/markdown-echarts": "0.1.0",
|
"@md-to-pdf/markdown-echarts": "0.1.0",
|
||||||
"@md-to-pdf/preview-engine": "0.1.0",
|
"@md-to-pdf/preview-engine": "0.1.0",
|
||||||
|
"@uiw/react-codemirror": "^4.25.11",
|
||||||
"highlight.js": "^11.11.1",
|
"highlight.js": "^11.11.1",
|
||||||
"katex": "^0.16.28",
|
"katex": "^0.16.28",
|
||||||
"pdfjs-dist": "6.1.200",
|
"pdfjs-dist": "6.1.200",
|
||||||
@@ -899,6 +904,160 @@
|
|||||||
"integrity": "sha512-U+HFai5+zmJCkK86QsaJtoITlboZHBqrVketcO2ROv865xfCMSFpELQoz1GkX5GzME8pTa+3kbKrZHQtI0gdbw==",
|
"integrity": "sha512-U+HFai5+zmJCkK86QsaJtoITlboZHBqrVketcO2ROv865xfCMSFpELQoz1GkX5GzME8pTa+3kbKrZHQtI0gdbw==",
|
||||||
"license": "Apache-2.0"
|
"license": "Apache-2.0"
|
||||||
},
|
},
|
||||||
|
"node_modules/@codemirror/autocomplete": {
|
||||||
|
"version": "6.20.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.20.3.tgz",
|
||||||
|
"integrity": "sha512-tlosUqb+3BbxCxZdu4tKeRghPFC+QM7q4X5YhKV2eCmPG+1r2F3f4AaSz5sCrFqUtX4Jh20VFTKecl16MgiV9g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@codemirror/language": "^6.0.0",
|
||||||
|
"@codemirror/state": "^6.0.0",
|
||||||
|
"@codemirror/view": "^6.17.0",
|
||||||
|
"@lezer/common": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@codemirror/commands": {
|
||||||
|
"version": "6.10.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.10.4.tgz",
|
||||||
|
"integrity": "sha512-Ryk9y9T0FFVF0cUGhAknveAyUOl/A1qReTFi+qPKtOh2Z9F4AUBz3XOrYD4ZEgZirdugVzHvd/2/Wcwy5OliTg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@codemirror/language": "^6.0.0",
|
||||||
|
"@codemirror/state": "^6.7.0",
|
||||||
|
"@codemirror/view": "^6.27.0",
|
||||||
|
"@lezer/common": "^1.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@codemirror/lang-css": {
|
||||||
|
"version": "6.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@codemirror/lang-css/-/lang-css-6.3.1.tgz",
|
||||||
|
"integrity": "sha512-kr5fwBGiGtmz6l0LSJIbno9QrifNMUusivHbnA1H6Dmqy4HZFte3UAICix1VuKo0lMPKQr2rqB+0BkKi/S3Ejg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@codemirror/autocomplete": "^6.0.0",
|
||||||
|
"@codemirror/language": "^6.0.0",
|
||||||
|
"@codemirror/state": "^6.0.0",
|
||||||
|
"@lezer/common": "^1.0.2",
|
||||||
|
"@lezer/css": "^1.1.7"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@codemirror/lang-html": {
|
||||||
|
"version": "6.4.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/@codemirror/lang-html/-/lang-html-6.4.11.tgz",
|
||||||
|
"integrity": "sha512-9NsXp7Nwp891pQchI7gPdTwBuSuT3K65NGTHWHNJ55HjYcHLllr0rbIZNdOzas9ztc1EUVBlHou85FFZS4BNnw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@codemirror/autocomplete": "^6.0.0",
|
||||||
|
"@codemirror/lang-css": "^6.0.0",
|
||||||
|
"@codemirror/lang-javascript": "^6.0.0",
|
||||||
|
"@codemirror/language": "^6.4.0",
|
||||||
|
"@codemirror/state": "^6.0.0",
|
||||||
|
"@codemirror/view": "^6.17.0",
|
||||||
|
"@lezer/common": "^1.0.0",
|
||||||
|
"@lezer/css": "^1.1.0",
|
||||||
|
"@lezer/html": "^1.3.12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@codemirror/lang-javascript": {
|
||||||
|
"version": "6.2.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@codemirror/lang-javascript/-/lang-javascript-6.2.5.tgz",
|
||||||
|
"integrity": "sha512-zD4e5mS+50htS7F+TYjBPsiIFGanfVqg4HyUz6WNFikgOPf2BgKlx+TQedI1w6n/IqRBVBbBWmGFdLB/7uxO4A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@codemirror/autocomplete": "^6.0.0",
|
||||||
|
"@codemirror/language": "^6.6.0",
|
||||||
|
"@codemirror/lint": "^6.0.0",
|
||||||
|
"@codemirror/state": "^6.0.0",
|
||||||
|
"@codemirror/view": "^6.17.0",
|
||||||
|
"@lezer/common": "^1.0.0",
|
||||||
|
"@lezer/javascript": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@codemirror/lang-markdown": {
|
||||||
|
"version": "6.5.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@codemirror/lang-markdown/-/lang-markdown-6.5.1.tgz",
|
||||||
|
"integrity": "sha512-6re5avCNfyRMIoi3XNjbEfQM1vTeVD3JS3g/Fyegyso/eoANFM71Cyvbb66LDyYtQLMEcRFlzioywCqDo9SlLA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@codemirror/autocomplete": "^6.7.1",
|
||||||
|
"@codemirror/lang-html": "^6.0.0",
|
||||||
|
"@codemirror/language": "^6.3.0",
|
||||||
|
"@codemirror/state": "^6.0.0",
|
||||||
|
"@codemirror/view": "^6.0.0",
|
||||||
|
"@lezer/common": "^1.2.1",
|
||||||
|
"@lezer/markdown": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@codemirror/language": {
|
||||||
|
"version": "6.12.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.12.4.tgz",
|
||||||
|
"integrity": "sha512-1q4PaT+o6PbgpkJt4Q8Fv5XJxTy4FUZ4MWETtyiDw3J0Pyr9E2vqcKL+k9wcvjNTIsauxvE7OfmWj3FRPHQ76A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@codemirror/state": "^6.0.0",
|
||||||
|
"@codemirror/view": "^6.23.0",
|
||||||
|
"@lezer/common": "^1.5.0",
|
||||||
|
"@lezer/highlight": "^1.0.0",
|
||||||
|
"@lezer/lr": "^1.0.0",
|
||||||
|
"style-mod": "^4.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@codemirror/lint": {
|
||||||
|
"version": "6.9.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.9.7.tgz",
|
||||||
|
"integrity": "sha512-28/+iWLYxKxsvGYhSYL7zaCZqLz5+FFFDq9tVsvGv9kv8RY4fFAchJ5WX9M3YrrRlTIsECjsXPqeNgnSmNP2dg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@codemirror/state": "^6.0.0",
|
||||||
|
"@codemirror/view": "^6.42.0",
|
||||||
|
"crelt": "^1.0.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@codemirror/search": {
|
||||||
|
"version": "6.7.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@codemirror/search/-/search-6.7.1.tgz",
|
||||||
|
"integrity": "sha512-uMe5UO6PamJtSHrXhhHOzSX3ReWtiJrva6GnPMwSOrZtiExb5X5eExhr2OUZQVvdxPsKpY3Ro2mFbQadpPWmHA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@codemirror/state": "^6.0.0",
|
||||||
|
"@codemirror/view": "^6.37.0",
|
||||||
|
"crelt": "^1.0.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@codemirror/state": {
|
||||||
|
"version": "6.7.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.7.1.tgz",
|
||||||
|
"integrity": "sha512-9QzNDgE4EYDnAHfrTlR2lwiPciiOymLtwKK+8yHQzCc7GXhAP9xdEbEJFy2IWB1j9UGUl9BsgMmTo/ImA02T7A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@marijn/find-cluster-break": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@codemirror/theme-one-dark": {
|
||||||
|
"version": "6.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@codemirror/theme-one-dark/-/theme-one-dark-6.1.3.tgz",
|
||||||
|
"integrity": "sha512-NzBdIvEJmx6fjeremiGp3t/okrLPYT0d9orIc7AFun8oZcRk58aejkqhv6spnz4MLAevrKNPMQYXEWMg4s+sKA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@codemirror/language": "^6.0.0",
|
||||||
|
"@codemirror/state": "^6.0.0",
|
||||||
|
"@codemirror/view": "^6.0.0",
|
||||||
|
"@lezer/highlight": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@codemirror/view": {
|
||||||
|
"version": "6.43.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.43.7.tgz",
|
||||||
|
"integrity": "sha512-FZsExxkoxnAN+d9TgqXLg5g4A1oQwzX9WlkOT5i2PKkcW7xx3Bmu0vs90g6fo9Mpdsb/l96dnAraQ8932aO4/g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@codemirror/state": "^6.7.0",
|
||||||
|
"crelt": "^1.0.6",
|
||||||
|
"style-mod": "^4.1.0",
|
||||||
|
"w3c-keyname": "^2.2.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@electron-internal/extract-zip": {
|
"node_modules/@electron-internal/extract-zip": {
|
||||||
"version": "1.0.4",
|
"version": "1.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/@electron-internal/extract-zip/-/extract-zip-1.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/@electron-internal/extract-zip/-/extract-zip-1.0.4.tgz",
|
||||||
@@ -1884,6 +2043,73 @@
|
|||||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@lezer/common": {
|
||||||
|
"version": "1.5.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.5.2.tgz",
|
||||||
|
"integrity": "sha512-sxQE460fPZyU3sdc8lafxiPwJHBzZRy/udNFynGQky1SePYBdhkBl1kOagA9uT3pxR8K09bOrmTUqA9wb/PjSQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@lezer/css": {
|
||||||
|
"version": "1.3.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@lezer/css/-/css-1.3.4.tgz",
|
||||||
|
"integrity": "sha512-N+tn9tej2hPvyKgHEApMOQfHczDJCwxrRFS3SPn9QjYN+uwHvEDnCgKRrb3mxDYxRS8sKMM8fhC3+lc04Abz5Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@lezer/common": "^1.2.0",
|
||||||
|
"@lezer/highlight": "^1.0.0",
|
||||||
|
"@lezer/lr": "^1.3.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@lezer/highlight": {
|
||||||
|
"version": "1.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.3.tgz",
|
||||||
|
"integrity": "sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@lezer/common": "^1.3.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@lezer/html": {
|
||||||
|
"version": "1.3.13",
|
||||||
|
"resolved": "https://registry.npmjs.org/@lezer/html/-/html-1.3.13.tgz",
|
||||||
|
"integrity": "sha512-oI7n6NJml729m7pjm9lvLvmXbdoMoi2f+1pwSDJkl9d68zGr7a9Btz8NdHTGQZtW2DA25ybeuv/SyDb9D5tseg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@lezer/common": "^1.2.0",
|
||||||
|
"@lezer/highlight": "^1.0.0",
|
||||||
|
"@lezer/lr": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@lezer/javascript": {
|
||||||
|
"version": "1.5.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@lezer/javascript/-/javascript-1.5.4.tgz",
|
||||||
|
"integrity": "sha512-vvYx3MhWqeZtGPwDStM2dwgljd5smolYD2lR2UyFcHfxbBQebqx8yjmFmxtJ/E6nN6u1D9srOiVWm3Rb4tmcUA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@lezer/common": "^1.2.0",
|
||||||
|
"@lezer/highlight": "^1.1.3",
|
||||||
|
"@lezer/lr": "^1.3.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@lezer/lr": {
|
||||||
|
"version": "1.4.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.10.tgz",
|
||||||
|
"integrity": "sha512-rnCpTIBafOx4mRp43xOxDJbFipJm/c0cia/V5TiGlhmMa+wsSdoGmUN3w5Bqrks/09Q/D4tNAmWaT8p6NRi77A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@lezer/common": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@lezer/markdown": {
|
||||||
|
"version": "1.7.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@lezer/markdown/-/markdown-1.7.2.tgz",
|
||||||
|
"integrity": "sha512-iTkYvoVcKt3WkeL7qUDyXHONZEwLio4wj8KTNi2dnjQEXBZKMV63BpQrPqfsM+OkvuRbiSTAcycYAsQzLhRNoQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@lezer/common": "^1.5.0",
|
||||||
|
"@lezer/highlight": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@malept/cross-spawn-promise": {
|
"node_modules/@malept/cross-spawn-promise": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/@malept/cross-spawn-promise/-/cross-spawn-promise-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/@malept/cross-spawn-promise/-/cross-spawn-promise-2.0.0.tgz",
|
||||||
@@ -1939,6 +2165,12 @@
|
|||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@marijn/find-cluster-break": {
|
||||||
|
"version": "1.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@marijn/find-cluster-break/-/find-cluster-break-1.0.3.tgz",
|
||||||
|
"integrity": "sha512-FY+MKLBoTsLNJF/eLWaOsXGdz6uh3Iu1axjPf6TUq92IYumcTcXWHoS747JARLkcdlJ/Waiaxc5wQfFO8jC6NA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@md-to-pdf/application": {
|
"node_modules/@md-to-pdf/application": {
|
||||||
"resolved": "packages/application",
|
"resolved": "packages/application",
|
||||||
"link": true
|
"link": true
|
||||||
@@ -3297,6 +3529,59 @@
|
|||||||
"@types/node": "*"
|
"@types/node": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@uiw/codemirror-extensions-basic-setup": {
|
||||||
|
"version": "4.25.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/@uiw/codemirror-extensions-basic-setup/-/codemirror-extensions-basic-setup-4.25.11.tgz",
|
||||||
|
"integrity": "sha512-otyFa+n9IOYtEjaKOxPedHkj15fTPUF21wdR9pv0GpZPfuGl27cvmcv6+tognbRu9VvEcsHKE+ESoszeo3KfTw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@codemirror/autocomplete": "^6.0.0",
|
||||||
|
"@codemirror/commands": "^6.0.0",
|
||||||
|
"@codemirror/language": "^6.0.0",
|
||||||
|
"@codemirror/lint": "^6.0.0",
|
||||||
|
"@codemirror/search": "^6.0.0",
|
||||||
|
"@codemirror/state": "^6.0.0",
|
||||||
|
"@codemirror/view": "^6.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://jaywcjlove.github.io/#/sponsor"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@codemirror/autocomplete": ">=6.0.0",
|
||||||
|
"@codemirror/commands": ">=6.0.0",
|
||||||
|
"@codemirror/language": ">=6.0.0",
|
||||||
|
"@codemirror/lint": ">=6.0.0",
|
||||||
|
"@codemirror/search": ">=6.0.0",
|
||||||
|
"@codemirror/state": ">=6.0.0",
|
||||||
|
"@codemirror/view": ">=6.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@uiw/react-codemirror": {
|
||||||
|
"version": "4.25.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/@uiw/react-codemirror/-/react-codemirror-4.25.11.tgz",
|
||||||
|
"integrity": "sha512-DYVFAKLX+F/4JS9N/7xexh+TICrlncwkX9HKKInrP1bwO0tSfc3k0GB6oawTYhelVKh20cX3TuRx+NJSkVXuMw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.18.6",
|
||||||
|
"@codemirror/commands": "^6.1.0",
|
||||||
|
"@codemirror/state": "^6.1.1",
|
||||||
|
"@codemirror/theme-one-dark": "^6.0.0",
|
||||||
|
"@uiw/codemirror-extensions-basic-setup": "4.25.11",
|
||||||
|
"codemirror": "^6.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://jaywcjlove.github.io/#/sponsor"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@babel/runtime": ">=7.11.0",
|
||||||
|
"@codemirror/state": ">=6.0.0",
|
||||||
|
"@codemirror/theme-one-dark": ">=6.0.0",
|
||||||
|
"@codemirror/view": ">=6.0.0",
|
||||||
|
"codemirror": ">=6.0.0",
|
||||||
|
"react": ">=17.0.0",
|
||||||
|
"react-dom": ">=17.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@upsetjs/venn.js": {
|
"node_modules/@upsetjs/venn.js": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/@upsetjs/venn.js/-/venn.js-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/@upsetjs/venn.js/-/venn.js-2.0.0.tgz",
|
||||||
@@ -4441,6 +4726,21 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/codemirror": {
|
||||||
|
"version": "6.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/codemirror/-/codemirror-6.0.2.tgz",
|
||||||
|
"integrity": "sha512-VhydHotNW5w1UGK0Qj96BwSk/Zqbp9WbnyK2W/eVMv4QyF41INRGpjUhFJY7/uDNuudSc33a/PKr4iDqRduvHw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@codemirror/autocomplete": "^6.0.0",
|
||||||
|
"@codemirror/commands": "^6.0.0",
|
||||||
|
"@codemirror/language": "^6.0.0",
|
||||||
|
"@codemirror/lint": "^6.0.0",
|
||||||
|
"@codemirror/search": "^6.0.0",
|
||||||
|
"@codemirror/state": "^6.0.0",
|
||||||
|
"@codemirror/view": "^6.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/color-convert": {
|
"node_modules/color-convert": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||||
@@ -4569,6 +4869,12 @@
|
|||||||
"layout-base": "^1.0.0"
|
"layout-base": "^1.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/crelt": {
|
||||||
|
"version": "1.0.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.7.tgz",
|
||||||
|
"integrity": "sha512-aK6BbWfhf4U/wCcLHKPJl/xa6VkVstRaPywWtMKGwuOLc/wZTyQYuoxgvZnNsBvv7Kg3YTBQYYBCggcviQczuA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/cross-dirname": {
|
"node_modules/cross-dirname": {
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/cross-dirname/-/cross-dirname-0.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/cross-dirname/-/cross-dirname-0.1.0.tgz",
|
||||||
@@ -8077,6 +8383,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.8.tgz",
|
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.8.tgz",
|
||||||
"integrity": "sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ==",
|
"integrity": "sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"scheduler": "^0.27.0"
|
"scheduler": "^0.27.0"
|
||||||
},
|
},
|
||||||
@@ -8751,6 +9058,12 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/style-mod": {
|
||||||
|
"version": "4.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.3.tgz",
|
||||||
|
"integrity": "sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/stylis": {
|
"node_modules/stylis": {
|
||||||
"version": "4.4.0",
|
"version": "4.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/stylis/-/stylis-4.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/stylis/-/stylis-4.4.0.tgz",
|
||||||
@@ -9338,6 +9651,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/w3c-keyname": {
|
||||||
|
"version": "2.2.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz",
|
||||||
|
"integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/wait-on": {
|
"node_modules/wait-on": {
|
||||||
"version": "9.1.0",
|
"version": "9.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/wait-on/-/wait-on-9.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/wait-on/-/wait-on-9.1.0.tgz",
|
||||||
|
|||||||
Reference in New Issue
Block a user