feat: 升级 CodeMirror 编辑器并新增 Markdown 工具栏
This commit is contained in:
@@ -10,9 +10,14 @@
|
||||
"typecheck": "tsc -b --pretty false"
|
||||
},
|
||||
"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/markdown-echarts": "0.1.0",
|
||||
"@md-to-pdf/preview-engine": "0.1.0",
|
||||
"@uiw/react-codemirror": "^4.25.11",
|
||||
"highlight.js": "^11.11.1",
|
||||
"katex": "^0.16.28",
|
||||
"pdfjs-dist": "6.1.200",
|
||||
|
||||
+40
-14
@@ -9,6 +9,7 @@ import {
|
||||
useRef,
|
||||
useState
|
||||
} from "react";
|
||||
import type { EditorView } from "@codemirror/view";
|
||||
import {
|
||||
createPagedDocumentPayload,
|
||||
getPaperDimensionsMm,
|
||||
@@ -71,6 +72,11 @@ import {
|
||||
applyThemeDefaults,
|
||||
selectTheme
|
||||
} from "./theme-margins";
|
||||
import {
|
||||
MarkdownEditor,
|
||||
type MarkdownEditorHandle
|
||||
} from "./MarkdownEditor";
|
||||
import { MarkdownToolbar } from "./MarkdownToolbar";
|
||||
|
||||
const PrecisePdfPreview = lazy(async () => {
|
||||
const module = await import("./PrecisePdfPreview");
|
||||
@@ -107,6 +113,11 @@ const initialDocumentKind: DocumentKind = startsWithBlankMarkdown
|
||||
|
||||
export function App() {
|
||||
const [markdown, setMarkdown] = useState(initialMarkdown);
|
||||
const [editorDocumentVersion, setEditorDocumentVersion] =
|
||||
useState(0);
|
||||
const [editorView, setEditorView] = useState<EditorView | null>(
|
||||
null
|
||||
);
|
||||
const [fileName, setFileName] = useState(initialFileName);
|
||||
const [documentKind, setDocumentKind] =
|
||||
useState<DocumentKind>(initialDocumentKind);
|
||||
@@ -141,7 +152,7 @@ export function App() {
|
||||
current: 0,
|
||||
total: 0
|
||||
});
|
||||
const editorRef = useRef<HTMLTextAreaElement>(null);
|
||||
const editorRef = useRef<MarkdownEditorHandle>(null);
|
||||
const markdownFileInputRef = useRef<HTMLInputElement>(null);
|
||||
const moreMenuRef = useRef<HTMLDivElement>(null);
|
||||
const previewScrollRef = useRef<HTMLDivElement>(null);
|
||||
@@ -158,6 +169,11 @@ export function App() {
|
||||
const themeRefreshTargetRef = useRef<number | undefined>(undefined);
|
||||
documentDirtyRef.current = documentDirty;
|
||||
|
||||
function replaceMarkdownDocument(content: string) {
|
||||
setMarkdown(content);
|
||||
setEditorDocumentVersion((version) => version + 1);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const bridge = window.mdToPdfDesktop;
|
||||
if (!bridge) {
|
||||
@@ -191,7 +207,7 @@ export function App() {
|
||||
return;
|
||||
}
|
||||
setAppError("");
|
||||
setMarkdown(opened.markdown);
|
||||
replaceMarkdownDocument(opened.markdown);
|
||||
setFileName(opened.fileName);
|
||||
setDocumentKind("file");
|
||||
setSavedMarkdown(opened.markdown);
|
||||
@@ -658,9 +674,13 @@ export function App() {
|
||||
|
||||
function synchronizeScroll(origin: "editor" | "preview") {
|
||||
const source =
|
||||
origin === "editor" ? editorRef.current : getPreviewScroller();
|
||||
origin === "editor"
|
||||
? editorRef.current?.getScrollElement()
|
||||
: getPreviewScroller();
|
||||
const target =
|
||||
origin === "editor" ? getPreviewScroller() : editorRef.current;
|
||||
origin === "editor"
|
||||
? getPreviewScroller()
|
||||
: editorRef.current?.getScrollElement();
|
||||
if (!source || !target) {
|
||||
return;
|
||||
}
|
||||
@@ -866,7 +886,7 @@ export function App() {
|
||||
try {
|
||||
setAppError("");
|
||||
const content = await file.text();
|
||||
setMarkdown(content);
|
||||
replaceMarkdownDocument(content);
|
||||
setFileName(file.name);
|
||||
setDocumentKind("file");
|
||||
setSavedMarkdown(content);
|
||||
@@ -885,7 +905,7 @@ export function App() {
|
||||
if (!opened) {
|
||||
return;
|
||||
}
|
||||
setMarkdown(opened.markdown);
|
||||
replaceMarkdownDocument(opened.markdown);
|
||||
setFileName(opened.fileName);
|
||||
setDocumentKind("file");
|
||||
setSavedMarkdown(opened.markdown);
|
||||
@@ -923,7 +943,7 @@ export function App() {
|
||||
try {
|
||||
setAppError("");
|
||||
await window.mdToPdfDesktop?.startNewMarkdown();
|
||||
setMarkdown("");
|
||||
replaceMarkdownDocument("");
|
||||
setFileName("");
|
||||
setDocumentKind("new");
|
||||
setSavedMarkdown("");
|
||||
@@ -1034,7 +1054,7 @@ export function App() {
|
||||
try {
|
||||
setAppError("");
|
||||
await window.mdToPdfDesktop?.startNewMarkdown();
|
||||
setMarkdown(sample.markdown);
|
||||
replaceMarkdownDocument(sample.markdown);
|
||||
setFileName(sample.fileName);
|
||||
setDocumentKind("sample");
|
||||
setSavedMarkdown(sample.markdown);
|
||||
@@ -1078,7 +1098,7 @@ export function App() {
|
||||
try {
|
||||
setAppError("");
|
||||
await window.mdToPdfDesktop?.startNewMarkdown();
|
||||
setMarkdown(tutorial.markdown);
|
||||
replaceMarkdownDocument(tutorial.markdown);
|
||||
setFileName(tutorial.fileName);
|
||||
setDocumentKind("sample");
|
||||
setSavedMarkdown(tutorial.markdown);
|
||||
@@ -1522,12 +1542,13 @@ export function App() {
|
||||
收起
|
||||
</button>
|
||||
</div>
|
||||
<textarea
|
||||
<MarkdownToolbar editorView={editorView} />
|
||||
<MarkdownEditor
|
||||
ref={editorRef}
|
||||
aria-label="Markdown 内容"
|
||||
value={markdown}
|
||||
spellCheck={false}
|
||||
onChange={(event) => setMarkdown(event.target.value)}
|
||||
documentVersion={editorDocumentVersion}
|
||||
onChange={setMarkdown}
|
||||
onEditorReady={setEditorView}
|
||||
onScroll={() => synchronizeScroll("editor")}
|
||||
/>
|
||||
</div>
|
||||
@@ -1537,7 +1558,12 @@ export function App() {
|
||||
className="source-panel-restore"
|
||||
aria-controls="markdown-source-content"
|
||||
aria-expanded="false"
|
||||
onClick={() => setSourcePanelCollapsed(false)}
|
||||
onClick={() => {
|
||||
setSourcePanelCollapsed(false);
|
||||
window.requestAnimationFrame(() =>
|
||||
editorRef.current?.focus()
|
||||
);
|
||||
}}
|
||||
>
|
||||
展开源文本
|
||||
</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;
|
||||
}
|
||||
|
||||
textarea {
|
||||
.markdown-editor {
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
min-height: 0;
|
||||
padding: 24px;
|
||||
border: 0;
|
||||
outline: 0;
|
||||
resize: none;
|
||||
background: #f6f7f5;
|
||||
color: #26312d;
|
||||
font-family: "Cascadia Code", "SFMono-Regular", Consolas, monospace;
|
||||
font-size: 0.86rem;
|
||||
line-height: 1.72;
|
||||
tab-size: 2;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
textarea:focus {
|
||||
box-shadow: inset 3px 0 #74a88f;
|
||||
.markdown-editor > div,
|
||||
.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 {
|
||||
@@ -1190,7 +1300,7 @@ textarea:focus {
|
||||
min-height: 650px;
|
||||
}
|
||||
|
||||
textarea {
|
||||
.markdown-editor {
|
||||
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
|
||||
引擎返回同一种准备结果。本阶段未新增 HTTP 路由、IPC 或 Pandoc 调用。
|
||||
|
||||
`v0.6.0` 阶段 4 已将左侧原生文本框升级为 CodeMirror 6,并建立项目
|
||||
自有 Markdown 命令注册层和响应式基础工具栏。当前支持撤销、重做、
|
||||
H1~H6、加粗、斜体、删除线、行内代码、代码块、引用、三类列表、尺寸
|
||||
表格和分隔线;链接、图片等功能化工具延后。现有实现保留受控 Markdown
|
||||
状态、防抖预览、文件工作流、文档脏状态、折叠恢复焦点及编辑/预览滚动
|
||||
同步。外部文档替换会重置历史,单次工具栏操作形成一个 CodeMirror
|
||||
事务。
|
||||
|
||||
## 2. 已完成
|
||||
|
||||
### 2.1 项目骨架
|
||||
@@ -919,6 +927,9 @@ ECharts 第二阶段浏览器与 PDF 验证结果:
|
||||
- `apps/web/src/PreviewPageControl.tsx` 和 `preview-page.ts` 负责当前页输入、
|
||||
页码边界、滚动页识别和快速跳转。
|
||||
- `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 协议、验证、安全过滤、
|
||||
Markdown-it 插件、浏览器 SVG 渲染和分页样式。
|
||||
- `apps/server/src/pdf-engine.ts` 负责 Chromium 生命周期、并发、网络限制、分页调用和 PDF 输出。
|
||||
@@ -962,7 +973,7 @@ ECharts 第二阶段浏览器与 PDF 验证结果:
|
||||
- 阶段 2:已冻结 Pandoc `3.9.0.2`、许可证、Docker 与 Desktop
|
||||
分发方式;
|
||||
- 阶段 3:已实现 DOCX 共享模型和跨端导出协议;
|
||||
- 阶段 4:实现 CodeMirror 6 与可扩展基础 Markdown 工具栏;
|
||||
- 阶段 4:已实现 CodeMirror 6 与可扩展基础 Markdown 工具栏;
|
||||
- 阶段 5~8:依次实现资源预处理、动态 reference.docx、Pandoc 转换服务
|
||||
以及 Web/Desktop 导出交互;
|
||||
- 阶段 9~10:完成自动化和 Word/WPS 互操作验收,再构建正式发布产物。
|
||||
|
||||
+22
-7
@@ -1,6 +1,7 @@
|
||||
# v0.6.0 DOCX 导出与 Markdown 工具栏设计
|
||||
|
||||
状态:阶段 1~3 已完成,DOCX 架构、Pandoc 分发及共享协议已经冻结。
|
||||
状态:阶段 1~4 已完成,DOCX 共享协议和 Markdown 编辑器基础能力已经
|
||||
落地。
|
||||
|
||||
## 1. 版本目标
|
||||
|
||||
@@ -211,8 +212,8 @@ Desktop 只提供平台运行时、传输与保存能力。
|
||||
|
||||
左侧编辑器采用:
|
||||
|
||||
- `@uiw/react-codemirror`;
|
||||
- `@codemirror/lang-markdown`;
|
||||
- `@uiw/react-codemirror` `4.25.11`;
|
||||
- `@codemirror/lang-markdown` `6.5.1`;
|
||||
- CodeMirror 6 的状态、选区、事务、历史和快捷键扩展。
|
||||
|
||||
不引入带独立 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 基础命令
|
||||
|
||||
- 撤销、重做;
|
||||
@@ -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 兼容性要求
|
||||
|
||||
@@ -278,6 +288,11 @@ Front Matter 表单工具和 ECharts YAML 工具不在 `v0.6.0` 实现,但必
|
||||
外部文件加载不得污染用户的撤销历史;工具栏的一次操作应形成一次清晰
|
||||
的撤销步骤。
|
||||
|
||||
当前通过独立的文档版本标识重建 CodeMirror 状态:用户键入和工具栏操作
|
||||
保留在当前撤销历史中;新建、打开、重载、教程及主题示例切换会开始新的
|
||||
历史。编辑器适配层同时暴露真实滚动容器和聚焦能力,继续复用原有预览
|
||||
滚动同步与折叠逻辑。
|
||||
|
||||
## 6. API、安全与运行约束
|
||||
|
||||
### 6.1 API 与 IPC
|
||||
|
||||
Generated
+319
@@ -538,9 +538,14 @@
|
||||
"name": "@md-to-pdf/web",
|
||||
"version": "0.1.0",
|
||||
"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/markdown-echarts": "0.1.0",
|
||||
"@md-to-pdf/preview-engine": "0.1.0",
|
||||
"@uiw/react-codemirror": "^4.25.11",
|
||||
"highlight.js": "^11.11.1",
|
||||
"katex": "^0.16.28",
|
||||
"pdfjs-dist": "6.1.200",
|
||||
@@ -899,6 +904,160 @@
|
||||
"integrity": "sha512-U+HFai5+zmJCkK86QsaJtoITlboZHBqrVketcO2ROv865xfCMSFpELQoz1GkX5GzME8pTa+3kbKrZHQtI0gdbw==",
|
||||
"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": {
|
||||
"version": "1.0.4",
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"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": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@malept/cross-spawn-promise/-/cross-spawn-promise-2.0.0.tgz",
|
||||
@@ -1939,6 +2165,12 @@
|
||||
"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": {
|
||||
"resolved": "packages/application",
|
||||
"link": true
|
||||
@@ -3297,6 +3529,59 @@
|
||||
"@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": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@upsetjs/venn.js/-/venn.js-2.0.0.tgz",
|
||||
@@ -4441,6 +4726,21 @@
|
||||
"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": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||
@@ -4569,6 +4869,12 @@
|
||||
"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": {
|
||||
"version": "0.1.0",
|
||||
"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",
|
||||
"integrity": "sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"scheduler": "^0.27.0"
|
||||
},
|
||||
@@ -8751,6 +9058,12 @@
|
||||
"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": {
|
||||
"version": "4.4.0",
|
||||
"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": {
|
||||
"version": "9.1.0",
|
||||
"resolved": "https://registry.npmjs.org/wait-on/-/wait-on-9.1.0.tgz",
|
||||
|
||||
Reference in New Issue
Block a user