mardora 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +113 -0
- package/dist/chunk-3OCUX4OO.js +7690 -0
- package/dist/chunk-3OCUX4OO.js.map +1 -0
- package/dist/chunk-3ZOCCFDL.cjs +74 -0
- package/dist/chunk-3ZOCCFDL.cjs.map +1 -0
- package/dist/chunk-7JOEPNEV.cjs +7740 -0
- package/dist/chunk-7JOEPNEV.cjs.map +1 -0
- package/dist/chunk-BIKZQZ6W.js +33 -0
- package/dist/chunk-BIKZQZ6W.js.map +1 -0
- package/dist/chunk-EQJESPP2.js +234 -0
- package/dist/chunk-EQJESPP2.js.map +1 -0
- package/dist/chunk-G4SE26YY.js +70 -0
- package/dist/chunk-G4SE26YY.js.map +1 -0
- package/dist/chunk-KNDWF2DP.cjs +35 -0
- package/dist/chunk-KNDWF2DP.cjs.map +1 -0
- package/dist/chunk-MLBEBFHB.cjs +2971 -0
- package/dist/chunk-MLBEBFHB.cjs.map +1 -0
- package/dist/chunk-P7JFCYU3.js +905 -0
- package/dist/chunk-P7JFCYU3.js.map +1 -0
- package/dist/chunk-SWFUKJDO.cjs +243 -0
- package/dist/chunk-SWFUKJDO.cjs.map +1 -0
- package/dist/chunk-WFVCG4LD.cjs +926 -0
- package/dist/chunk-WFVCG4LD.cjs.map +1 -0
- package/dist/chunk-XL6WFGJT.js +2901 -0
- package/dist/chunk-XL6WFGJT.js.map +1 -0
- package/dist/editor/index.cjs +277 -0
- package/dist/editor/index.cjs.map +1 -0
- package/dist/editor/index.d.cts +186 -0
- package/dist/editor/index.d.ts +186 -0
- package/dist/editor/index.js +4 -0
- package/dist/editor/index.js.map +1 -0
- package/dist/index.cjs +405 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +13 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/index.cjs +12 -0
- package/dist/lib/index.cjs.map +1 -0
- package/dist/lib/index.d.cts +16 -0
- package/dist/lib/index.d.ts +16 -0
- package/dist/lib/index.js +3 -0
- package/dist/lib/index.js.map +1 -0
- package/dist/mardora-DCwjomil.d.cts +640 -0
- package/dist/mardora-DCwjomil.d.ts +640 -0
- package/dist/plugins/index.cjs +104 -0
- package/dist/plugins/index.cjs.map +1 -0
- package/dist/plugins/index.d.cts +740 -0
- package/dist/plugins/index.d.ts +740 -0
- package/dist/plugins/index.js +7 -0
- package/dist/plugins/index.js.map +1 -0
- package/dist/preview/index.cjs +38 -0
- package/dist/preview/index.cjs.map +1 -0
- package/dist/preview/index.d.cts +101 -0
- package/dist/preview/index.d.ts +101 -0
- package/dist/preview/index.js +5 -0
- package/dist/preview/index.js.map +1 -0
- package/dist/types-NBsaxl4d.d.cts +71 -0
- package/dist/types-Pw2SWWAR.d.ts +71 -0
- package/package.json +92 -0
- package/src/editor/attachments/extension.ts +181 -0
- package/src/editor/attachments/format.ts +63 -0
- package/src/editor/attachments/index.ts +3 -0
- package/src/editor/attachments/types.ts +37 -0
- package/src/editor/heading-fold/config.ts +25 -0
- package/src/editor/heading-fold/extension.ts +268 -0
- package/src/editor/heading-fold/extract.ts +88 -0
- package/src/editor/heading-fold/index.ts +5 -0
- package/src/editor/heading-fold/theme.ts +85 -0
- package/src/editor/heading-fold/types.ts +24 -0
- package/src/editor/i18n.ts +13 -0
- package/src/editor/icons/index.ts +367 -0
- package/src/editor/index.ts +16 -0
- package/src/editor/mardora.ts +257 -0
- package/src/editor/media-lightbox-theme.ts +146 -0
- package/src/editor/media-lightbox.ts +125 -0
- package/src/editor/plugin.ts +294 -0
- package/src/editor/selection-toolbar/activation.ts +123 -0
- package/src/editor/selection-toolbar/commands.ts +279 -0
- package/src/editor/selection-toolbar/extension.ts +564 -0
- package/src/editor/selection-toolbar/i18n.ts +164 -0
- package/src/editor/selection-toolbar/index.ts +7 -0
- package/src/editor/selection-toolbar/menu.ts +252 -0
- package/src/editor/selection-toolbar/position.ts +43 -0
- package/src/editor/selection-toolbar/theme.ts +195 -0
- package/src/editor/selection-toolbar/types.ts +155 -0
- package/src/editor/slash/default-commands.ts +190 -0
- package/src/editor/slash/extension.ts +319 -0
- package/src/editor/slash/index.ts +7 -0
- package/src/editor/slash/insertions.ts +26 -0
- package/src/editor/slash/menu.ts +123 -0
- package/src/editor/slash/position.ts +61 -0
- package/src/editor/slash/query.ts +33 -0
- package/src/editor/slash/theme.ts +113 -0
- package/src/editor/slash/types.ts +40 -0
- package/src/editor/table-of-contents/extension.ts +202 -0
- package/src/editor/table-of-contents/extract.ts +53 -0
- package/src/editor/table-of-contents/index.ts +7 -0
- package/src/editor/table-of-contents/panel.ts +83 -0
- package/src/editor/table-of-contents/slug.ts +50 -0
- package/src/editor/table-of-contents/storage.ts +35 -0
- package/src/editor/table-of-contents/theme.ts +153 -0
- package/src/editor/table-of-contents/types.ts +44 -0
- package/src/editor/theme.ts +72 -0
- package/src/editor/utils.ts +176 -0
- package/src/editor/view-plugin.ts +189 -0
- package/src/index.ts +5 -0
- package/src/lib/index.ts +2 -0
- package/src/lib/input-handler.ts +47 -0
- package/src/plugins/code-plugin.theme.ts +545 -0
- package/src/plugins/code-plugin.ts +1892 -0
- package/src/plugins/emoji-plugin.ts +140 -0
- package/src/plugins/heading-plugin.ts +194 -0
- package/src/plugins/hr-plugin.ts +102 -0
- package/src/plugins/html-plugin.ts +353 -0
- package/src/plugins/image-plugin.ts +806 -0
- package/src/plugins/index.ts +71 -0
- package/src/plugins/inline-plugin.ts +311 -0
- package/src/plugins/link-plugin.ts +509 -0
- package/src/plugins/list-plugin.ts +492 -0
- package/src/plugins/math-plugin.ts +526 -0
- package/src/plugins/mermaid-plugin.ts +513 -0
- package/src/plugins/paragraph-plugin.ts +38 -0
- package/src/plugins/quote-plugin.ts +733 -0
- package/src/plugins/table-controls-theme.ts +126 -0
- package/src/plugins/table-controls.ts +423 -0
- package/src/plugins/table-model.ts +661 -0
- package/src/plugins/table-plugin.ts +2111 -0
- package/src/preview/context.ts +45 -0
- package/src/preview/css-generator.ts +64 -0
- package/src/preview/default-renderers.ts +29 -0
- package/src/preview/index.ts +29 -0
- package/src/preview/preview.ts +41 -0
- package/src/preview/renderer.ts +184 -0
- package/src/preview/syntax-theme.ts +112 -0
- package/src/preview/toc.ts +23 -0
- package/src/preview/types.ts +89 -0
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { MardoraAttachmentKind, MardoraAttachmentUploadResult, MardoraFileLike } from "./types";
|
|
2
|
+
|
|
3
|
+
export type MardoraUploadMarkerState = "uploading" | "failed";
|
|
4
|
+
|
|
5
|
+
export type MardoraUploadMarkerInput = {
|
|
6
|
+
taskId: string;
|
|
7
|
+
kind: MardoraAttachmentKind;
|
|
8
|
+
name: string;
|
|
9
|
+
state: MardoraUploadMarkerState;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export function detectAttachmentKind(file: MardoraFileLike): MardoraAttachmentKind {
|
|
13
|
+
if (file.type.startsWith("image/")) return "image";
|
|
14
|
+
if (file.type.startsWith("video/")) return "video";
|
|
15
|
+
if (file.type.startsWith("audio/")) return "audio";
|
|
16
|
+
return "file";
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function isAcceptedAttachment(file: MardoraFileLike, acceptRules: readonly string[]): boolean {
|
|
20
|
+
if (acceptRules.includes("*/*")) return true;
|
|
21
|
+
|
|
22
|
+
return acceptRules.some((rule) => {
|
|
23
|
+
if (rule.endsWith("/*")) {
|
|
24
|
+
return file.type.startsWith(rule.slice(0, -1));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (rule.startsWith(".")) {
|
|
28
|
+
return file.name.toLocaleLowerCase().endsWith(rule.toLocaleLowerCase());
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return file.type === rule;
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function createUploadMarker(input: MardoraUploadMarkerInput): string {
|
|
36
|
+
if (input.state === "failed") {
|
|
37
|
+
return `[Upload failed: ${input.name}](mardora-upload://${input.taskId})`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (input.kind === "image") {
|
|
41
|
+
return ``;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return `[Uploading ${input.name}](mardora-upload://${input.taskId})`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function formatAttachmentMarkdown(kind: MardoraAttachmentKind, result: MardoraAttachmentUploadResult): string {
|
|
48
|
+
const name = result.name || "attachment";
|
|
49
|
+
|
|
50
|
+
if (kind === "image") {
|
|
51
|
+
return result.title ? `` : ``;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (kind === "video") {
|
|
55
|
+
return `<video src="${result.url}" controls></video>`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (kind === "audio") {
|
|
59
|
+
return `<audio src="${result.url}" controls></audio>`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return `[${name}](${result.url})`;
|
|
63
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export type MardoraAttachmentKind = "image" | "video" | "audio" | "file";
|
|
2
|
+
|
|
3
|
+
export type MardoraAttachmentUploadSource = "slash" | "paste" | "drop" | "api";
|
|
4
|
+
|
|
5
|
+
export type MardoraAttachmentUploadContext = {
|
|
6
|
+
kind: MardoraAttachmentKind;
|
|
7
|
+
source: MardoraAttachmentUploadSource;
|
|
8
|
+
documentText: string;
|
|
9
|
+
selection: { from: number; to: number };
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export type MardoraAttachmentUploadResult = {
|
|
13
|
+
url: string;
|
|
14
|
+
name?: string;
|
|
15
|
+
title?: string;
|
|
16
|
+
mimeType?: string;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export type MardoraAttachmentUploader = (
|
|
20
|
+
file: File,
|
|
21
|
+
context: MardoraAttachmentUploadContext
|
|
22
|
+
) => Promise<MardoraAttachmentUploadResult>;
|
|
23
|
+
|
|
24
|
+
export type MardoraAttachmentAccept = Partial<Record<MardoraAttachmentKind, string[]>>;
|
|
25
|
+
|
|
26
|
+
export type MardoraAttachmentsConfig = {
|
|
27
|
+
enabled?: boolean;
|
|
28
|
+
uploader?: MardoraAttachmentUploader;
|
|
29
|
+
accept?: MardoraAttachmentAccept;
|
|
30
|
+
enablePaste?: boolean;
|
|
31
|
+
enableDrop?: boolean;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export type MardoraFileLike = {
|
|
35
|
+
name: string;
|
|
36
|
+
type: string;
|
|
37
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
MardoraHeadingFoldConfig,
|
|
3
|
+
MardoraHeadingFoldLevel,
|
|
4
|
+
ResolvedMardoraHeadingFoldConfig,
|
|
5
|
+
} from "./types";
|
|
6
|
+
|
|
7
|
+
const minSupportedLevel = 2;
|
|
8
|
+
const maxSupportedLevel = 5;
|
|
9
|
+
|
|
10
|
+
function clampLevel(level: MardoraHeadingFoldLevel): MardoraHeadingFoldLevel {
|
|
11
|
+
return Math.min(Math.max(level, minSupportedLevel), maxSupportedLevel) as MardoraHeadingFoldLevel;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function resolveHeadingFoldConfig(config: MardoraHeadingFoldConfig = {}): ResolvedMardoraHeadingFoldConfig {
|
|
15
|
+
const requestedMinLevel = clampLevel(config.minLevel ?? minSupportedLevel);
|
|
16
|
+
const requestedMaxLevel = clampLevel(config.maxLevel ?? maxSupportedLevel);
|
|
17
|
+
const minLevel = Math.min(requestedMinLevel, requestedMaxLevel) as MardoraHeadingFoldLevel;
|
|
18
|
+
const maxLevel = Math.max(requestedMinLevel, requestedMaxLevel) as MardoraHeadingFoldLevel;
|
|
19
|
+
|
|
20
|
+
return {
|
|
21
|
+
enabled: config.enabled !== false,
|
|
22
|
+
minLevel,
|
|
23
|
+
maxLevel,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Facet,
|
|
3
|
+
StateEffect,
|
|
4
|
+
StateField,
|
|
5
|
+
type EditorState,
|
|
6
|
+
type Extension,
|
|
7
|
+
type Range,
|
|
8
|
+
type Transaction,
|
|
9
|
+
} from "@codemirror/state";
|
|
10
|
+
import { Decoration, type DecorationSet, EditorView, ViewPlugin, type ViewUpdate, WidgetType } from "@codemirror/view";
|
|
11
|
+
import { extractHeadingFoldRangesFromState } from "./extract";
|
|
12
|
+
import { headingFoldTheme } from "./theme";
|
|
13
|
+
import type { MardoraHeadingFoldConfig, MardoraHeadingFoldRange } from "./types";
|
|
14
|
+
|
|
15
|
+
const toggleHeadingFoldEffect = StateEffect.define<number>();
|
|
16
|
+
|
|
17
|
+
const headingFoldConfigFacet = Facet.define<MardoraHeadingFoldConfig, MardoraHeadingFoldConfig>({
|
|
18
|
+
combine: (values) => values[values.length - 1] ?? {},
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
interface HeadingFoldState {
|
|
22
|
+
foldedHeadings: readonly number[];
|
|
23
|
+
decorations: DecorationSet;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function nextFoldedHeadings(value: readonly number[], transaction: Transaction): readonly number[] {
|
|
27
|
+
const next = new Set<number>();
|
|
28
|
+
|
|
29
|
+
if (transaction.docChanged) {
|
|
30
|
+
for (const position of value) {
|
|
31
|
+
next.add(transaction.changes.mapPos(position, 1));
|
|
32
|
+
}
|
|
33
|
+
} else {
|
|
34
|
+
for (const position of value) next.add(position);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
for (const effect of transaction.effects) {
|
|
38
|
+
if (!effect.is(toggleHeadingFoldEffect)) continue;
|
|
39
|
+
if (next.has(effect.value)) {
|
|
40
|
+
next.delete(effect.value);
|
|
41
|
+
} else {
|
|
42
|
+
next.add(effect.value);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return Array.from(next).sort((a, b) => a - b);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
class HeadingFoldWidget extends WidgetType {
|
|
50
|
+
constructor(
|
|
51
|
+
private readonly range: MardoraHeadingFoldRange,
|
|
52
|
+
private readonly folded: boolean
|
|
53
|
+
) {
|
|
54
|
+
super();
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
override eq(other: HeadingFoldWidget): boolean {
|
|
58
|
+
return other.range.headingFrom === this.range.headingFrom && other.folded === this.folded;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
override toDOM(): HTMLElement {
|
|
62
|
+
const button = document.createElement("button");
|
|
63
|
+
button.type = "button";
|
|
64
|
+
button.className = "cm-mardora-heading-fold-toggle";
|
|
65
|
+
button.setAttribute("aria-label", `${this.folded ? "Expand" : "Collapse"} H${this.range.level} section`);
|
|
66
|
+
button.dataset.mardoraHeadingFoldFolded = String(this.folded);
|
|
67
|
+
button.dataset.mardoraHeadingFoldFrom = String(this.range.headingFrom);
|
|
68
|
+
|
|
69
|
+
const level = document.createElement("span");
|
|
70
|
+
level.className = "cm-mardora-heading-fold-level";
|
|
71
|
+
level.textContent = `H${this.range.level}`;
|
|
72
|
+
|
|
73
|
+
const arrow = document.createElement("span");
|
|
74
|
+
arrow.className = "cm-mardora-heading-fold-arrow";
|
|
75
|
+
arrow.setAttribute("aria-hidden", "true");
|
|
76
|
+
|
|
77
|
+
button.append(level, arrow);
|
|
78
|
+
|
|
79
|
+
return button;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
override ignoreEvent(event: Event): boolean {
|
|
83
|
+
return event.type === "mousedown" || event.type === "click";
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
class FoldPlaceholderWidget extends WidgetType {
|
|
88
|
+
override eq(other: FoldPlaceholderWidget): boolean {
|
|
89
|
+
return other instanceof FoldPlaceholderWidget;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
override toDOM(): HTMLElement {
|
|
93
|
+
const placeholder = document.createElement("span");
|
|
94
|
+
placeholder.className = "cm-mardora-heading-fold-placeholder";
|
|
95
|
+
placeholder.setAttribute("aria-hidden", "true");
|
|
96
|
+
placeholder.textContent = "...";
|
|
97
|
+
return placeholder;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function selectionTouchesHeading(state: EditorState, range: MardoraHeadingFoldRange): boolean {
|
|
102
|
+
return state.selection.ranges.some((selection) => {
|
|
103
|
+
const line = state.doc.lineAt(selection.head);
|
|
104
|
+
return line.from === range.headingLineFrom;
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function readHeadingFoldButtonPosition(target: EventTarget | null, view: EditorView): number | null {
|
|
109
|
+
if (!(target instanceof Element)) return null;
|
|
110
|
+
const button = target.closest(".cm-mardora-heading-fold-toggle");
|
|
111
|
+
if (!(button instanceof HTMLElement) || !view.dom.contains(button)) return null;
|
|
112
|
+
const position = Number(button.dataset.mardoraHeadingFoldFrom);
|
|
113
|
+
return Number.isFinite(position) ? position : null;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function toggleHeadingFoldAt(view: EditorView, position: number): void {
|
|
117
|
+
view.dispatch({ effects: toggleHeadingFoldEffect.of(position) });
|
|
118
|
+
view.focus();
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const headingFoldDomHandlers = EditorView.domEventHandlers({
|
|
122
|
+
mousedown: (event, view) => {
|
|
123
|
+
if (event.button !== 0) return false;
|
|
124
|
+
const position = readHeadingFoldButtonPosition(event.target, view);
|
|
125
|
+
if (position === null) return false;
|
|
126
|
+
event.preventDefault();
|
|
127
|
+
event.stopPropagation();
|
|
128
|
+
toggleHeadingFoldAt(view, position);
|
|
129
|
+
return true;
|
|
130
|
+
},
|
|
131
|
+
click: (event, view) => {
|
|
132
|
+
if (event.detail !== 0) return false;
|
|
133
|
+
const position = readHeadingFoldButtonPosition(event.target, view);
|
|
134
|
+
if (position === null) return false;
|
|
135
|
+
event.preventDefault();
|
|
136
|
+
event.stopPropagation();
|
|
137
|
+
toggleHeadingFoldAt(view, position);
|
|
138
|
+
return true;
|
|
139
|
+
},
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
export function __buildHeadingFoldDecorations(
|
|
143
|
+
state: EditorState,
|
|
144
|
+
config: MardoraHeadingFoldConfig,
|
|
145
|
+
foldedHeadings: readonly number[]
|
|
146
|
+
): DecorationSet {
|
|
147
|
+
const folded = new Set(foldedHeadings);
|
|
148
|
+
const decorations: Range<Decoration>[] = [];
|
|
149
|
+
|
|
150
|
+
for (const range of extractHeadingFoldRangesFromState(state, config)) {
|
|
151
|
+
const isFolded = folded.has(range.headingFrom);
|
|
152
|
+
const active = selectionTouchesHeading(state, range);
|
|
153
|
+
const lineClass = [
|
|
154
|
+
"cm-mardora-heading-fold-line",
|
|
155
|
+
active ? "cm-mardora-heading-fold-line-active" : "",
|
|
156
|
+
isFolded ? "cm-mardora-heading-fold-line-folded" : "",
|
|
157
|
+
]
|
|
158
|
+
.filter(Boolean)
|
|
159
|
+
.join(" ");
|
|
160
|
+
|
|
161
|
+
decorations.push(Decoration.line({ class: lineClass }).range(range.headingLineFrom));
|
|
162
|
+
decorations.push(
|
|
163
|
+
Decoration.widget({
|
|
164
|
+
widget: new HeadingFoldWidget(range, isFolded),
|
|
165
|
+
side: -1,
|
|
166
|
+
}).range(range.headingFrom)
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
if (isFolded) {
|
|
170
|
+
decorations.push(
|
|
171
|
+
Decoration.widget({
|
|
172
|
+
widget: new FoldPlaceholderWidget(),
|
|
173
|
+
side: 1,
|
|
174
|
+
mardoraHeadingFoldRole: "placeholder",
|
|
175
|
+
}).range(range.headingTo)
|
|
176
|
+
);
|
|
177
|
+
decorations.push(
|
|
178
|
+
Decoration.replace({
|
|
179
|
+
mardoraHeadingFoldRole: "hidden-content",
|
|
180
|
+
}).range(range.foldFrom, range.foldTo)
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return Decoration.set(decorations, true);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const headingFoldStateField = StateField.define<HeadingFoldState>({
|
|
189
|
+
create: (state) => {
|
|
190
|
+
const foldedHeadings: readonly number[] = [];
|
|
191
|
+
return {
|
|
192
|
+
foldedHeadings,
|
|
193
|
+
decorations: __buildHeadingFoldDecorations(state, state.facet(headingFoldConfigFacet), foldedHeadings),
|
|
194
|
+
};
|
|
195
|
+
},
|
|
196
|
+
update: (value, transaction) => {
|
|
197
|
+
const foldedHeadings = nextFoldedHeadings(value.foldedHeadings, transaction);
|
|
198
|
+
return {
|
|
199
|
+
foldedHeadings,
|
|
200
|
+
decorations: __buildHeadingFoldDecorations(transaction.state, transaction.state.facet(headingFoldConfigFacet), foldedHeadings),
|
|
201
|
+
};
|
|
202
|
+
},
|
|
203
|
+
provide: (field) => EditorView.decorations.from(field, (value) => value.decorations),
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
class HeadingFoldViewPlugin {
|
|
207
|
+
private suppressNextMouseDown = false;
|
|
208
|
+
|
|
209
|
+
constructor(
|
|
210
|
+
private readonly view: EditorView,
|
|
211
|
+
_config: MardoraHeadingFoldConfig
|
|
212
|
+
) {
|
|
213
|
+
void _config;
|
|
214
|
+
this.view.dom.addEventListener("pointerdown", this.handlePointerDown, true);
|
|
215
|
+
this.view.dom.addEventListener("mousedown", this.handleMouseDown, true);
|
|
216
|
+
this.view.dom.addEventListener("click", this.handleClick, true);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
update(_update: ViewUpdate): void {
|
|
220
|
+
void _update;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
destroy(): void {
|
|
224
|
+
this.view.dom.removeEventListener("pointerdown", this.handlePointerDown, true);
|
|
225
|
+
this.view.dom.removeEventListener("mousedown", this.handleMouseDown, true);
|
|
226
|
+
this.view.dom.removeEventListener("click", this.handleClick, true);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
private readonly handlePointerDown = (event: PointerEvent): void => {
|
|
230
|
+
if (event.button !== 0) return;
|
|
231
|
+
if (readHeadingFoldButtonPosition(event.target, this.view) !== null) this.suppressNextMouseDown = true;
|
|
232
|
+
this.handleToggleEvent(event);
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
private readonly handleMouseDown = (event: MouseEvent): void => {
|
|
236
|
+
if (event.button !== 0) return;
|
|
237
|
+
if (this.suppressNextMouseDown) {
|
|
238
|
+
this.suppressNextMouseDown = false;
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
this.handleToggleEvent(event);
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
private readonly handleClick = (event: MouseEvent): void => {
|
|
245
|
+
if (event.detail !== 0) return;
|
|
246
|
+
this.handleToggleEvent(event);
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
private handleToggleEvent(event: MouseEvent | PointerEvent): void {
|
|
250
|
+
const position = readHeadingFoldButtonPosition(event.target, this.view);
|
|
251
|
+
if (position === null) return;
|
|
252
|
+
event.preventDefault();
|
|
253
|
+
event.stopPropagation();
|
|
254
|
+
toggleHeadingFoldAt(this.view, position);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
export function headingFold(config: MardoraHeadingFoldConfig = {}): Extension[] {
|
|
259
|
+
if (config.enabled === false) return [];
|
|
260
|
+
|
|
261
|
+
return [
|
|
262
|
+
headingFoldConfigFacet.of(config),
|
|
263
|
+
headingFoldStateField,
|
|
264
|
+
headingFoldTheme,
|
|
265
|
+
headingFoldDomHandlers,
|
|
266
|
+
ViewPlugin.define((view) => new HeadingFoldViewPlugin(view, config)),
|
|
267
|
+
];
|
|
268
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import type { EditorState } from "@codemirror/state";
|
|
2
|
+
import { ensureSyntaxTree, syntaxTree } from "@codemirror/language";
|
|
3
|
+
import type { SyntaxNodeRef } from "@lezer/common";
|
|
4
|
+
import { stripMarkdownHeadingText } from "../table-of-contents";
|
|
5
|
+
import { resolveHeadingFoldConfig } from "./config";
|
|
6
|
+
import type { MardoraHeadingFoldConfig, MardoraHeadingFoldLevel, MardoraHeadingFoldRange } from "./types";
|
|
7
|
+
|
|
8
|
+
const headingPattern = /^ATXHeading([1-6])$/;
|
|
9
|
+
const headingFoldParseTimeout = 100;
|
|
10
|
+
|
|
11
|
+
interface ParsedHeading {
|
|
12
|
+
level: number;
|
|
13
|
+
text: string;
|
|
14
|
+
from: number;
|
|
15
|
+
to: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function headingLevel(node: SyntaxNodeRef): number | null {
|
|
19
|
+
const match = headingPattern.exec(node.name);
|
|
20
|
+
return match ? Number(match[1]) : null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function canFoldLevel(level: number, minLevel: MardoraHeadingFoldLevel, maxLevel: MardoraHeadingFoldLevel): boolean {
|
|
24
|
+
return level >= minLevel && level <= maxLevel;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function collectHeadings(state: EditorState): ParsedHeading[] {
|
|
28
|
+
const headings: ParsedHeading[] = [];
|
|
29
|
+
const tree = ensureSyntaxTree(state, state.doc.length, headingFoldParseTimeout) ?? syntaxTree(state);
|
|
30
|
+
|
|
31
|
+
tree.iterate({
|
|
32
|
+
enter: (node) => {
|
|
33
|
+
const level = headingLevel(node);
|
|
34
|
+
if (!level) return;
|
|
35
|
+
const text = stripMarkdownHeadingText(state.sliceDoc(node.from, node.to));
|
|
36
|
+
if (!text) return;
|
|
37
|
+
headings.push({ level, text, from: node.from, to: node.to });
|
|
38
|
+
},
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
return headings;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function findFoldEnd(state: EditorState, headings: ParsedHeading[], index: number): number {
|
|
45
|
+
const heading = headings[index]!;
|
|
46
|
+
const nextBoundary = headings.slice(index + 1).find((candidate) => candidate.level <= heading.level);
|
|
47
|
+
if (!nextBoundary) return state.doc.length;
|
|
48
|
+
return state.doc.lineAt(nextBoundary.from).from;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function findFoldStart(state: EditorState, heading: ParsedHeading): number {
|
|
52
|
+
const headingLine = state.doc.lineAt(heading.from);
|
|
53
|
+
if (headingLine.number >= state.doc.lines) return headingLine.to;
|
|
54
|
+
return state.doc.line(headingLine.number + 1).from;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function extractHeadingFoldRangesFromState(
|
|
58
|
+
state: EditorState,
|
|
59
|
+
config: MardoraHeadingFoldConfig = {}
|
|
60
|
+
): MardoraHeadingFoldRange[] {
|
|
61
|
+
const resolved = resolveHeadingFoldConfig(config);
|
|
62
|
+
if (!resolved.enabled) return [];
|
|
63
|
+
|
|
64
|
+
const headings = collectHeadings(state);
|
|
65
|
+
const ranges: MardoraHeadingFoldRange[] = [];
|
|
66
|
+
|
|
67
|
+
headings.forEach((heading, index) => {
|
|
68
|
+
if (!canFoldLevel(heading.level, resolved.minLevel, resolved.maxLevel)) return;
|
|
69
|
+
|
|
70
|
+
const line = state.doc.lineAt(heading.from);
|
|
71
|
+
const foldFrom = findFoldStart(state, heading);
|
|
72
|
+
const foldTo = findFoldEnd(state, headings, index);
|
|
73
|
+
if (foldTo <= foldFrom) return;
|
|
74
|
+
|
|
75
|
+
ranges.push({
|
|
76
|
+
level: heading.level as MardoraHeadingFoldLevel,
|
|
77
|
+
text: heading.text,
|
|
78
|
+
headingFrom: heading.from,
|
|
79
|
+
headingTo: heading.to,
|
|
80
|
+
headingLineFrom: line.from,
|
|
81
|
+
headingLineTo: line.to,
|
|
82
|
+
foldFrom,
|
|
83
|
+
foldTo,
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
return ranges;
|
|
88
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { EditorView } from "@codemirror/view";
|
|
2
|
+
|
|
3
|
+
export const headingFoldTheme = EditorView.baseTheme({
|
|
4
|
+
".cm-mardora-heading-fold-line": {
|
|
5
|
+
position: "relative",
|
|
6
|
+
},
|
|
7
|
+
|
|
8
|
+
".cm-mardora-heading-fold-toggle": {
|
|
9
|
+
alignItems: "center",
|
|
10
|
+
background: "transparent",
|
|
11
|
+
border: "0",
|
|
12
|
+
borderRadius: "4px",
|
|
13
|
+
color: "var(--mardora-heading-fold-muted, #a1a1aa)",
|
|
14
|
+
cursor: "pointer",
|
|
15
|
+
display: "inline-flex",
|
|
16
|
+
font: "600 0.7rem/1 var(--font-sans, sans-serif)",
|
|
17
|
+
height: "1rem",
|
|
18
|
+
justifyContent: "center",
|
|
19
|
+
marginLeft: "-2.55rem",
|
|
20
|
+
marginRight: "0.45rem",
|
|
21
|
+
padding: "0",
|
|
22
|
+
transform: "translateY(-0.52em)",
|
|
23
|
+
verticalAlign: "middle",
|
|
24
|
+
width: "2rem",
|
|
25
|
+
},
|
|
26
|
+
|
|
27
|
+
".cm-mardora-heading-fold-toggle:hover, .cm-mardora-heading-fold-toggle:focus-visible": {
|
|
28
|
+
color: "var(--mardora-heading-fold-active, #52525b)",
|
|
29
|
+
},
|
|
30
|
+
|
|
31
|
+
".cm-mardora-heading-fold-toggle:focus-visible": {
|
|
32
|
+
outline: "2px solid var(--mardora-heading-fold-focus, #a1a1aa)",
|
|
33
|
+
outlineOffset: "2px",
|
|
34
|
+
},
|
|
35
|
+
|
|
36
|
+
".cm-mardora-heading-fold-level": {
|
|
37
|
+
display: "inline-flex",
|
|
38
|
+
},
|
|
39
|
+
|
|
40
|
+
".cm-mardora-heading-fold-arrow": {
|
|
41
|
+
display: "none",
|
|
42
|
+
height: "0.55rem",
|
|
43
|
+
position: "relative",
|
|
44
|
+
width: "0.55rem",
|
|
45
|
+
},
|
|
46
|
+
|
|
47
|
+
".cm-mardora-heading-fold-arrow::before": {
|
|
48
|
+
borderBottom: "1.5px solid currentColor",
|
|
49
|
+
borderRight: "1.5px solid currentColor",
|
|
50
|
+
content: '""',
|
|
51
|
+
display: "block",
|
|
52
|
+
height: "0.28rem",
|
|
53
|
+
left: "0.08rem",
|
|
54
|
+
position: "absolute",
|
|
55
|
+
top: "0.03rem",
|
|
56
|
+
transform: "rotate(45deg)",
|
|
57
|
+
width: "0.28rem",
|
|
58
|
+
},
|
|
59
|
+
|
|
60
|
+
".cm-mardora-heading-fold-toggle[data-mardora-heading-fold-folded='true'] .cm-mardora-heading-fold-arrow::before": {
|
|
61
|
+
left: "0.03rem",
|
|
62
|
+
top: "0.11rem",
|
|
63
|
+
transform: "rotate(-45deg)",
|
|
64
|
+
},
|
|
65
|
+
|
|
66
|
+
".cm-mardora-heading-fold-line:hover .cm-mardora-heading-fold-level, .cm-mardora-heading-fold-line-active .cm-mardora-heading-fold-level, .cm-mardora-heading-fold-toggle:hover .cm-mardora-heading-fold-level, .cm-mardora-heading-fold-toggle:focus-visible .cm-mardora-heading-fold-level, .cm-mardora-heading-fold-toggle[data-mardora-heading-fold-folded='true'] .cm-mardora-heading-fold-level": {
|
|
67
|
+
display: "none",
|
|
68
|
+
},
|
|
69
|
+
|
|
70
|
+
".cm-mardora-heading-fold-line:hover .cm-mardora-heading-fold-arrow, .cm-mardora-heading-fold-line-active .cm-mardora-heading-fold-arrow, .cm-mardora-heading-fold-toggle:hover .cm-mardora-heading-fold-arrow, .cm-mardora-heading-fold-toggle:focus-visible .cm-mardora-heading-fold-arrow, .cm-mardora-heading-fold-toggle[data-mardora-heading-fold-folded='true'] .cm-mardora-heading-fold-arrow": {
|
|
71
|
+
display: "inline-block",
|
|
72
|
+
},
|
|
73
|
+
|
|
74
|
+
".cm-mardora-heading-fold-placeholder": {
|
|
75
|
+
color: "var(--mardora-heading-fold-muted, #a1a1aa)",
|
|
76
|
+
font: "600 0.9rem/1.4 var(--font-sans, sans-serif)",
|
|
77
|
+
padding: "0.1rem 0 0.35rem 0.25rem",
|
|
78
|
+
},
|
|
79
|
+
|
|
80
|
+
"&dark .cm-mardora-heading-fold-toggle, &dark .cm-mardora-heading-fold-placeholder": {
|
|
81
|
+
"--mardora-heading-fold-muted": "#71717a",
|
|
82
|
+
"--mardora-heading-fold-active": "#d4d4d8",
|
|
83
|
+
"--mardora-heading-fold-focus": "#71717a",
|
|
84
|
+
},
|
|
85
|
+
});
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export type MardoraHeadingFoldLevel = 2 | 3 | 4 | 5;
|
|
2
|
+
|
|
3
|
+
export interface MardoraHeadingFoldConfig {
|
|
4
|
+
enabled?: boolean;
|
|
5
|
+
minLevel?: MardoraHeadingFoldLevel;
|
|
6
|
+
maxLevel?: MardoraHeadingFoldLevel;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface ResolvedMardoraHeadingFoldConfig {
|
|
10
|
+
enabled: boolean;
|
|
11
|
+
minLevel: MardoraHeadingFoldLevel;
|
|
12
|
+
maxLevel: MardoraHeadingFoldLevel;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface MardoraHeadingFoldRange {
|
|
16
|
+
level: MardoraHeadingFoldLevel;
|
|
17
|
+
text: string;
|
|
18
|
+
headingFrom: number;
|
|
19
|
+
headingTo: number;
|
|
20
|
+
headingLineFrom: number;
|
|
21
|
+
headingLineTo: number;
|
|
22
|
+
foldFrom: number;
|
|
23
|
+
foldTo: number;
|
|
24
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export type MardoraLocale = "zh-CN" | "en-US";
|
|
2
|
+
|
|
3
|
+
export type MardoraI18nConfig = {
|
|
4
|
+
locale?: MardoraLocale;
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
export const defaultMardoraLocale: MardoraLocale = "zh-CN";
|
|
8
|
+
|
|
9
|
+
const supportedMardoraLocales = new Set<MardoraLocale>(["zh-CN", "en-US"]);
|
|
10
|
+
|
|
11
|
+
export function resolveMardoraLocale(locale?: MardoraLocale): MardoraLocale {
|
|
12
|
+
return locale && supportedMardoraLocales.has(locale) ? locale : defaultMardoraLocale;
|
|
13
|
+
}
|