@moraya/core 0.1.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/CHANGELOG.md +344 -0
- package/LICENSE +85 -0
- package/README.md +82 -0
- package/dist/adapters/browser-media-resolver.d.ts +21 -0
- package/dist/adapters/browser-media-resolver.js +24 -0
- package/dist/adapters/browser-media-resolver.js.map +1 -0
- package/dist/commands.d.ts +35 -0
- package/dist/commands.js +976 -0
- package/dist/commands.js.map +1 -0
- package/dist/doc-cache.d.ts +29 -0
- package/dist/doc-cache.js +50 -0
- package/dist/doc-cache.js.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +4534 -0
- package/dist/index.js.map +1 -0
- package/dist/markdown.d.ts +46 -0
- package/dist/markdown.js +1553 -0
- package/dist/markdown.js.map +1 -0
- package/dist/plugins/code-block-view.d.ts +52 -0
- package/dist/plugins/code-block-view.js +686 -0
- package/dist/plugins/code-block-view.js.map +1 -0
- package/dist/plugins/cursor-syntax.d.ts +27 -0
- package/dist/plugins/cursor-syntax.js +122 -0
- package/dist/plugins/cursor-syntax.js.map +1 -0
- package/dist/plugins/definition-list.d.ts +23 -0
- package/dist/plugins/definition-list.js +12 -0
- package/dist/plugins/definition-list.js.map +1 -0
- package/dist/plugins/editor-props-plugin.d.ts +36 -0
- package/dist/plugins/editor-props-plugin.js +1963 -0
- package/dist/plugins/editor-props-plugin.js.map +1 -0
- package/dist/plugins/emoji.d.ts +21 -0
- package/dist/plugins/emoji.js +42 -0
- package/dist/plugins/emoji.js.map +1 -0
- package/dist/plugins/enter-handler.d.ts +26 -0
- package/dist/plugins/enter-handler.js +193 -0
- package/dist/plugins/enter-handler.js.map +1 -0
- package/dist/plugins/highlight.d.ts +39 -0
- package/dist/plugins/highlight.js +283 -0
- package/dist/plugins/highlight.js.map +1 -0
- package/dist/plugins/inline-code-convert.d.ts +32 -0
- package/dist/plugins/inline-code-convert.js +173 -0
- package/dist/plugins/inline-code-convert.js.map +1 -0
- package/dist/plugins/link-text-plugin.d.ts +22 -0
- package/dist/plugins/link-text-plugin.js +194 -0
- package/dist/plugins/link-text-plugin.js.map +1 -0
- package/dist/plugins/mermaid-renderer.d.ts +24 -0
- package/dist/plugins/mermaid-renderer.js +80 -0
- package/dist/plugins/mermaid-renderer.js.map +1 -0
- package/dist/schema.d.ts +48 -0
- package/dist/schema.js +847 -0
- package/dist/schema.js.map +1 -0
- package/dist/setup.d.ts +104 -0
- package/dist/setup.js +4393 -0
- package/dist/setup.js.map +1 -0
- package/dist/types.d.ts +107 -0
- package/dist/types.js +10 -0
- package/dist/types.js.map +1 -0
- package/package.json +121 -0
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
// src/plugins/inline-code-convert.ts
|
|
2
|
+
import { Plugin, PluginKey } from "prosemirror-state";
|
|
3
|
+
var pluginKey = new PluginKey("moraya-inline-code-convert");
|
|
4
|
+
var ZWSP = "\u200B";
|
|
5
|
+
var ZWSP_MARK_NAMES = ["code", "strong", "em", "strike_through"];
|
|
6
|
+
function hasZwspTargetMark(marks, state) {
|
|
7
|
+
return ZWSP_MARK_NAMES.some((name) => {
|
|
8
|
+
const mt = state.schema.marks[name];
|
|
9
|
+
return mt && mt.isInSet(marks);
|
|
10
|
+
});
|
|
11
|
+
}
|
|
12
|
+
var CODE_PATTERN = /`([^`]+)`/g;
|
|
13
|
+
function findCodePatternsInBlock(state, pos) {
|
|
14
|
+
const matches = [];
|
|
15
|
+
const codeType = state.schema.marks.code;
|
|
16
|
+
let resolved;
|
|
17
|
+
try {
|
|
18
|
+
resolved = state.doc.resolve(pos);
|
|
19
|
+
} catch {
|
|
20
|
+
return matches;
|
|
21
|
+
}
|
|
22
|
+
const parent = resolved.parent;
|
|
23
|
+
if (!parent.isTextblock) return matches;
|
|
24
|
+
if (parent.type.spec.code) return matches;
|
|
25
|
+
const base = resolved.start();
|
|
26
|
+
let nodePos = base;
|
|
27
|
+
for (let i = 0; i < parent.childCount; i++) {
|
|
28
|
+
const child = parent.child(i);
|
|
29
|
+
if (child.isText && child.text && !(codeType && codeType.isInSet(child.marks))) {
|
|
30
|
+
CODE_PATTERN.lastIndex = 0;
|
|
31
|
+
let m;
|
|
32
|
+
while ((m = CODE_PATTERN.exec(child.text)) !== null) {
|
|
33
|
+
matches.push({
|
|
34
|
+
from: nodePos + m.index,
|
|
35
|
+
to: nodePos + m.index + m[0].length,
|
|
36
|
+
content: m[1] ?? ""
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
nodePos += child.nodeSize;
|
|
41
|
+
}
|
|
42
|
+
return matches;
|
|
43
|
+
}
|
|
44
|
+
function needsCursorTarget(state) {
|
|
45
|
+
const { $head } = state.selection;
|
|
46
|
+
if (!$head) return -1;
|
|
47
|
+
const parent = $head.parent;
|
|
48
|
+
if (!parent.isTextblock || parent.type.spec.code || parent.childCount === 0) return -1;
|
|
49
|
+
const lastChild = parent.lastChild;
|
|
50
|
+
if (!lastChild?.isText) return -1;
|
|
51
|
+
if (!hasZwspTargetMark(lastChild.marks, state) && lastChild.text?.endsWith(ZWSP)) return -1;
|
|
52
|
+
for (let i = parent.childCount - 1; i >= 0; i--) {
|
|
53
|
+
const child = parent.child(i);
|
|
54
|
+
if (child.isText && !hasZwspTargetMark(child.marks, state) && child.text === ZWSP) continue;
|
|
55
|
+
if (child.isText && hasZwspTargetMark(child.marks, state)) {
|
|
56
|
+
return $head.start() + parent.content.size;
|
|
57
|
+
}
|
|
58
|
+
break;
|
|
59
|
+
}
|
|
60
|
+
return -1;
|
|
61
|
+
}
|
|
62
|
+
function createInlineCodeConvertPlugin() {
|
|
63
|
+
return new Plugin({
|
|
64
|
+
key: pluginKey,
|
|
65
|
+
appendTransaction(transactions, oldState, newState) {
|
|
66
|
+
if (transactions.some((tr) => tr.getMeta(pluginKey))) return null;
|
|
67
|
+
if (transactions.some((tr) => tr.getMeta("full-delete"))) return null;
|
|
68
|
+
const selChanged = transactions.some((tr) => tr.selectionSet);
|
|
69
|
+
const docChanged = transactions.some((tr) => tr.docChanged);
|
|
70
|
+
if (!selChanged && !docChanged) return null;
|
|
71
|
+
if (!newState.selection.empty) return null;
|
|
72
|
+
const newPos = newState.selection.from;
|
|
73
|
+
const oldPos = oldState.selection.from;
|
|
74
|
+
const codeType = newState.schema.marks.code;
|
|
75
|
+
const oldMatches = findCodePatternsInBlock(oldState, oldPos);
|
|
76
|
+
const wasIn = oldMatches.find((m) => oldPos > m.from && oldPos < m.to);
|
|
77
|
+
if (wasIn && codeType) {
|
|
78
|
+
let mappedFrom = wasIn.from;
|
|
79
|
+
if (docChanged) {
|
|
80
|
+
for (const t of transactions) {
|
|
81
|
+
mappedFrom = t.mapping.map(mappedFrom);
|
|
82
|
+
}
|
|
83
|
+
if (mappedFrom < 0 || mappedFrom > newState.doc.content.size) return null;
|
|
84
|
+
}
|
|
85
|
+
const newMatches = findCodePatternsInBlock(newState, mappedFrom);
|
|
86
|
+
const isStillIn = newMatches.find((m) => newPos > m.from && newPos < m.to);
|
|
87
|
+
if (!isStillIn) {
|
|
88
|
+
const target = newMatches.find(
|
|
89
|
+
(m) => Math.abs(m.from - mappedFrom) < 3
|
|
90
|
+
);
|
|
91
|
+
if (target?.content) {
|
|
92
|
+
const codeNode = newState.schema.text(target.content, [codeType.create()]);
|
|
93
|
+
const tr = newState.tr.replaceWith(target.from, target.to, codeNode);
|
|
94
|
+
tr.setMeta(pluginKey, "collapse");
|
|
95
|
+
tr.setMeta("addToHistory", false);
|
|
96
|
+
return tr;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
const insertPos = needsCursorTarget(newState);
|
|
101
|
+
if (insertPos >= 0) {
|
|
102
|
+
const tr = newState.tr.insertText(ZWSP, insertPos);
|
|
103
|
+
tr.setMeta(pluginKey, "cursor-target");
|
|
104
|
+
tr.setMeta("addToHistory", false);
|
|
105
|
+
if (newState.selection.from === insertPos) {
|
|
106
|
+
const { $head: $h } = newState.selection;
|
|
107
|
+
if ($h?.nodeBefore) {
|
|
108
|
+
const hasInclusive = ZWSP_MARK_NAMES.filter((n) => n !== "code").some((name) => {
|
|
109
|
+
const mt = newState.schema.marks[name];
|
|
110
|
+
return mt && $h.nodeBefore.marks.some((m) => m.type === mt);
|
|
111
|
+
});
|
|
112
|
+
if (hasInclusive) {
|
|
113
|
+
const filtered = $h.marks().filter(
|
|
114
|
+
(m) => !ZWSP_MARK_NAMES.filter((n) => n !== "code").some((name) => {
|
|
115
|
+
const mt = newState.schema.marks[name];
|
|
116
|
+
return mt && m.type === mt;
|
|
117
|
+
})
|
|
118
|
+
);
|
|
119
|
+
tr.setStoredMarks(filtered);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return tr;
|
|
124
|
+
}
|
|
125
|
+
if (transactions.some((tr) => tr.getMeta("code-escape"))) return null;
|
|
126
|
+
const { $head } = newState.selection;
|
|
127
|
+
if ($head && newState.selection.empty && codeType) {
|
|
128
|
+
const nodeBefore = $head.nodeBefore;
|
|
129
|
+
const nodeAfter = $head.nodeAfter;
|
|
130
|
+
if (nodeBefore?.marks.some((m) => m.type === codeType) && nodeAfter?.isText && !codeType.isInSet(nodeAfter.marks) && nodeAfter.text?.startsWith(ZWSP)) {
|
|
131
|
+
const stored = newState.storedMarks;
|
|
132
|
+
if (stored && stored.some((m) => m.type === codeType)) return null;
|
|
133
|
+
const marks = [...$head.marks(), codeType.create()];
|
|
134
|
+
const tr = newState.tr.setStoredMarks(marks);
|
|
135
|
+
tr.setMeta(pluginKey, "boundary-marks");
|
|
136
|
+
tr.setMeta("addToHistory", false);
|
|
137
|
+
return tr;
|
|
138
|
+
}
|
|
139
|
+
const inclusiveMarkNames = ZWSP_MARK_NAMES.filter((n) => n !== "code");
|
|
140
|
+
const hasInclusiveBefore = nodeBefore != null && inclusiveMarkNames.some((name) => {
|
|
141
|
+
const mt = newState.schema.marks[name];
|
|
142
|
+
return mt && nodeBefore.marks.some((m) => m.type === mt);
|
|
143
|
+
});
|
|
144
|
+
if (hasInclusiveBefore && nodeAfter?.isText && nodeAfter.text?.startsWith(ZWSP)) {
|
|
145
|
+
const stored = newState.storedMarks;
|
|
146
|
+
const storedHasInclusive = stored?.some(
|
|
147
|
+
(m) => inclusiveMarkNames.some((name) => {
|
|
148
|
+
const mt = newState.schema.marks[name];
|
|
149
|
+
return mt && m.type === mt;
|
|
150
|
+
})
|
|
151
|
+
);
|
|
152
|
+
if (stored !== null && !storedHasInclusive) return null;
|
|
153
|
+
const filtered = $head.marks().filter(
|
|
154
|
+
(m) => !inclusiveMarkNames.some((name) => {
|
|
155
|
+
const mt = newState.schema.marks[name];
|
|
156
|
+
return mt && m.type === mt;
|
|
157
|
+
})
|
|
158
|
+
);
|
|
159
|
+
const tr = newState.tr.setStoredMarks(filtered);
|
|
160
|
+
tr.setMeta(pluginKey, "boundary-marks-inclusive");
|
|
161
|
+
tr.setMeta("addToHistory", false);
|
|
162
|
+
return tr;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
export {
|
|
170
|
+
ZWSP,
|
|
171
|
+
createInlineCodeConvertPlugin
|
|
172
|
+
};
|
|
173
|
+
//# sourceMappingURL=inline-code-convert.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/plugins/inline-code-convert.ts"],"sourcesContent":["/**\n * Inline mark convert plugin — three responsibilities:\n *\n * 1. **Backtick collapse**: Auto-converts `` `text` `` patterns to code marks\n * when the cursor leaves the backtick pair. Handles the workflow where the\n * user types two backticks first, moves the cursor between them, types\n * content, then leaves.\n *\n * 2. **Cursor target**: Inserts a zero-width space (U+200B) after formatting\n * marks (`code`, `strong`, `em`, `strike_through`) at the end of textblocks.\n * WebKit can't position the caret after certain inline elements when there\n * is no subsequent text node, so the ZWSP provides a DOM target for both\n * keyboard navigation and mouse clicks.\n *\n * 3. **Stored marks at code–ZWSP boundary**: code is `inclusive: false` so\n * `marks()` at the boundary excludes it. The plugin proactively sets stored\n * marks so typing at the boundary still extends code. ArrowRight clears the\n * stored marks (handled in `editor-props-plugin.ts` `'code-escape'` meta).\n * `strong` / `em` / `strike_through` are `inclusive: true` so `marks()`\n * already includes them at the boundary — no `storedMarks` manipulation\n * needed for those.\n *\n * The U+200B is stripped during markdown serialization (see `serializeMarkdown`).\n */\n\nimport { Plugin, PluginKey } from 'prosemirror-state'\nimport type { EditorState } from 'prosemirror-state'\n\nconst pluginKey = new PluginKey('moraya-inline-code-convert')\n\n/** Zero-width space used as cursor anchor after trailing formatting marks. */\nexport const ZWSP = ''\n\n/**\n * Marks that get a ZWSP cursor target when they are the last content in a\n * textblock. Includes non-inclusive marks (code) and inclusive formatting\n * marks (strong, em, strike_through) — all need an escape position at end\n * of paragraph so ArrowRight doesn't jump straight to the next block.\n */\nconst ZWSP_MARK_NAMES = ['code', 'strong', 'em', 'strike_through'] as const\n\nfunction hasZwspTargetMark(\n marks: readonly import('prosemirror-model').Mark[],\n state: EditorState,\n): boolean {\n return ZWSP_MARK_NAMES.some(name => {\n const mt = state.schema.marks[name]\n return mt && mt.isInSet(marks)\n })\n}\n\n/** Matches `` `text` `` (backtick-delimited) for conversion — requires non-empty content. */\nconst CODE_PATTERN = /`([^`]+)`/g\n\ninterface CodeMatch {\n from: number\n to: number\n content: string\n}\n\n/**\n * Find `` `text` `` patterns in the textblock containing `pos`.\n * Only scans unmarked text nodes (skips text already marked as code).\n */\nfunction findCodePatternsInBlock(state: EditorState, pos: number): CodeMatch[] {\n const matches: CodeMatch[] = []\n const codeType = state.schema.marks.code\n\n let resolved\n try { resolved = state.doc.resolve(pos) } catch { return matches }\n const parent = resolved.parent\n if (!parent.isTextblock) return matches\n\n // Skip code blocks — backticks are literal there\n if (parent.type.spec.code) return matches\n\n const base = resolved.start()\n let nodePos = base\n for (let i = 0; i < parent.childCount; i++) {\n const child = parent.child(i)\n if (child.isText && child.text && !(codeType && codeType.isInSet(child.marks))) {\n CODE_PATTERN.lastIndex = 0\n let m: RegExpExecArray | null\n while ((m = CODE_PATTERN.exec(child.text)) !== null) {\n matches.push({\n from: nodePos + m.index,\n to: nodePos + m.index + m[0].length,\n content: m[1] ?? '',\n })\n }\n }\n nodePos += child.nodeSize\n }\n return matches\n}\n\n/**\n * Check if a textblock's last content is a formatting mark that needs a\n * trailing cursor target (U+200B). Returns the insert position, or -1.\n */\nfunction needsCursorTarget(state: EditorState): number {\n const { $head } = state.selection\n if (!$head) return -1\n const parent = $head.parent\n if (!parent.isTextblock || parent.type.spec.code || parent.childCount === 0) return -1\n\n const lastChild = parent.lastChild\n if (!lastChild?.isText) return -1\n\n // Already has a trailing ZWSP without any target mark — no action needed\n if (!hasZwspTargetMark(lastChild.marks, state) && lastChild.text?.endsWith(ZWSP)) return -1\n\n // Walk backwards skipping existing ZWSP-only unmarked nodes.\n // If the last meaningful child has a target mark → insert ZWSP at end.\n for (let i = parent.childCount - 1; i >= 0; i--) {\n const child = parent.child(i)\n if (child.isText && !hasZwspTargetMark(child.marks, state) && child.text === ZWSP) continue\n if (child.isText && hasZwspTargetMark(child.marks, state)) {\n return $head.start() + parent.content.size // insert at end of textblock content\n }\n break // non-target-mark, non-ZWSP content found — no target needed\n }\n return -1\n}\n\nexport function createInlineCodeConvertPlugin(): Plugin {\n return new Plugin({\n key: pluginKey,\n\n appendTransaction(transactions, oldState, newState) {\n // Skip if this plugin already produced a transaction\n if (transactions.some((tr) => tr.getMeta(pluginKey))) return null\n if (transactions.some((tr) => tr.getMeta('full-delete'))) return null\n\n const selChanged = transactions.some((tr) => tr.selectionSet)\n const docChanged = transactions.some((tr) => tr.docChanged)\n if (!selChanged && !docChanged) return null\n if (!newState.selection.empty) return null\n\n const newPos = newState.selection.from\n const oldPos = oldState.selection.from\n const codeType = newState.schema.marks.code\n\n // ── 1. Backtick pair collapse ──\n const oldMatches = findCodePatternsInBlock(oldState, oldPos)\n const wasIn = oldMatches.find((m) => oldPos > m.from && oldPos < m.to)\n if (wasIn && codeType) {\n let mappedFrom = wasIn.from\n if (docChanged) {\n for (const t of transactions) {\n mappedFrom = t.mapping.map(mappedFrom)\n }\n if (mappedFrom < 0 || mappedFrom > newState.doc.content.size) return null\n }\n\n const newMatches = findCodePatternsInBlock(newState, mappedFrom)\n const isStillIn = newMatches.find((m) => newPos > m.from && newPos < m.to)\n if (!isStillIn) {\n const target = newMatches.find(\n (m) => Math.abs(m.from - mappedFrom) < 3,\n )\n if (target?.content) {\n const codeNode = newState.schema.text(target.content, [codeType.create()])\n const tr = newState.tr.replaceWith(target.from, target.to, codeNode)\n tr.setMeta(pluginKey, 'collapse')\n tr.setMeta('addToHistory', false)\n return tr\n }\n }\n }\n\n // ── 2. Cursor target: ensure U+200B after trailing formatting marks ──\n const insertPos = needsCursorTarget(newState)\n if (insertPos >= 0) {\n const tr = newState.tr.insertText(ZWSP, insertPos)\n tr.setMeta(pluginKey, 'cursor-target')\n tr.setMeta('addToHistory', false)\n\n // For inclusive marks (strong/em/strike_through): if cursor is exactly at\n // the insertion point (the right boundary of the mark), clear those marks\n // from storedMarks so that typing immediately after the input rule produces\n // plain text (Typora-style: completing **bold** exits bold).\n if (newState.selection.from === insertPos) {\n const { $head: $h } = newState.selection\n if ($h?.nodeBefore) {\n const hasInclusive = ZWSP_MARK_NAMES\n .filter((n) => n !== 'code')\n .some((name) => {\n const mt = newState.schema.marks[name]\n return mt && $h.nodeBefore!.marks.some((m) => m.type === mt)\n })\n if (hasInclusive) {\n const filtered = $h.marks().filter((m) =>\n !ZWSP_MARK_NAMES.filter((n) => n !== 'code').some((name) => {\n const mt = newState.schema.marks[name]\n return mt && m.type === mt\n }),\n )\n tr.setStoredMarks(filtered)\n }\n }\n }\n\n return tr\n }\n\n // ── 3. Stored marks at code–ZWSP boundary ──\n // With inclusive:false, marks() at the right boundary of code excludes\n // the code mark. But the user expects typing at the end of code text to\n // extend the code (they visually see the cursor inside the gray background).\n // Proactively set stored marks to include code at this position.\n // ArrowRight handler sets 'code-escape' meta to opt out.\n if (transactions.some((tr) => tr.getMeta('code-escape'))) return null\n\n const { $head } = newState.selection\n if ($head && newState.selection.empty && codeType) {\n const nodeBefore = $head.nodeBefore\n const nodeAfter = $head.nodeAfter\n\n if (\n nodeBefore?.marks.some((m) => m.type === codeType) &&\n nodeAfter?.isText &&\n !codeType.isInSet(nodeAfter.marks) &&\n nodeAfter.text?.startsWith(ZWSP)\n ) {\n // Already has code in stored marks — nothing to do\n const stored = newState.storedMarks\n if (stored && stored.some((m) => m.type === codeType)) return null\n\n const marks = [...$head.marks(), codeType.create()]\n const tr = newState.tr.setStoredMarks(marks)\n tr.setMeta(pluginKey, 'boundary-marks')\n tr.setMeta('addToHistory', false)\n return tr\n }\n\n // ── 3b. Clear inclusive marks at mark–ZWSP boundary ──\n // strong/em/strike_through are inclusive:true, so marks() at the right\n // boundary includes them. When cursor navigates here (via ← or click),\n // clear those marks from storedMarks so typing is plain text.\n const inclusiveMarkNames = ZWSP_MARK_NAMES.filter((n) => n !== 'code')\n const hasInclusiveBefore = nodeBefore != null && inclusiveMarkNames.some((name) => {\n const mt = newState.schema.marks[name]\n return mt && nodeBefore.marks.some((m) => m.type === mt)\n })\n if (hasInclusiveBefore && nodeAfter?.isText && nodeAfter.text?.startsWith(ZWSP)) {\n // If storedMarks is already null or already excludes inclusive marks, bail\n const stored = newState.storedMarks\n const storedHasInclusive = stored?.some((m) =>\n inclusiveMarkNames.some((name) => {\n const mt = newState.schema.marks[name]\n return mt && m.type === mt\n }),\n )\n if (stored !== null && !storedHasInclusive) return null\n\n const filtered = $head.marks().filter((m) =>\n !inclusiveMarkNames.some((name) => {\n const mt = newState.schema.marks[name]\n return mt && m.type === mt\n }),\n )\n const tr = newState.tr.setStoredMarks(filtered)\n tr.setMeta(pluginKey, 'boundary-marks-inclusive')\n tr.setMeta('addToHistory', false)\n return tr\n }\n }\n\n return null\n },\n })\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { Plugin } from 'prosemirror-state';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Link text plugin — Typora-style inline link editing:
|
|
5
|
+
*
|
|
6
|
+
* 1. **Decoration**: Scans text nodes for `[...](...)` literal patterns and
|
|
7
|
+
* applies a muted CSS class so users can distinguish link syntax from text.
|
|
8
|
+
*
|
|
9
|
+
* 2. **Collapse**: When cursor leaves a `[text](url)` pattern (both non-empty),
|
|
10
|
+
* auto-convert to a ProseMirror link mark (rendered as clickable link).
|
|
11
|
+
*
|
|
12
|
+
* 3. **Expand**: When cursor enters a rendered link mark, replace the mark
|
|
13
|
+
* with literal text `[text](url)` so the user can edit both text and URL
|
|
14
|
+
* directly inline.
|
|
15
|
+
*
|
|
16
|
+
* Schema-agnostic: uses `state.schema.marks.link` rather than an imported
|
|
17
|
+
* singleton, so the plugin works against any consumer-injected schema.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
declare function createLinkTextPlugin(): Plugin;
|
|
21
|
+
|
|
22
|
+
export { createLinkTextPlugin };
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
// src/plugins/link-text-plugin.ts
|
|
2
|
+
import { Plugin, PluginKey, TextSelection } from "prosemirror-state";
|
|
3
|
+
import { Decoration, DecorationSet } from "prosemirror-view";
|
|
4
|
+
var pluginKey = new PluginKey("moraya-link-text");
|
|
5
|
+
var LINK_PATTERN_DECO = /\[([^\]]*)\]\(([^)]*)\)/g;
|
|
6
|
+
var LINK_PATTERN_CONVERT = /\[([^\]]+)\]\(([^)]+)\)/g;
|
|
7
|
+
function findLinkPatterns(state, regex) {
|
|
8
|
+
const matches = [];
|
|
9
|
+
const linkType = state.schema.marks.link;
|
|
10
|
+
state.doc.descendants((node, pos) => {
|
|
11
|
+
if (!node.isText || !node.text) return;
|
|
12
|
+
if (linkType && linkType.isInSet(node.marks)) return;
|
|
13
|
+
regex.lastIndex = 0;
|
|
14
|
+
let m;
|
|
15
|
+
while ((m = regex.exec(node.text)) !== null) {
|
|
16
|
+
matches.push({
|
|
17
|
+
from: pos + m.index,
|
|
18
|
+
to: pos + m.index + m[0].length,
|
|
19
|
+
text: m[1] ?? "",
|
|
20
|
+
url: m[2] ?? ""
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
return matches;
|
|
25
|
+
}
|
|
26
|
+
function findLinkPatternsInBlock(state, pos, regex) {
|
|
27
|
+
const matches = [];
|
|
28
|
+
const linkType = state.schema.marks.link;
|
|
29
|
+
let resolved;
|
|
30
|
+
try {
|
|
31
|
+
resolved = state.doc.resolve(pos);
|
|
32
|
+
} catch {
|
|
33
|
+
return matches;
|
|
34
|
+
}
|
|
35
|
+
const parent = resolved.parent;
|
|
36
|
+
if (!parent.isTextblock) return matches;
|
|
37
|
+
const base = resolved.start();
|
|
38
|
+
let nodePos = base;
|
|
39
|
+
for (let i = 0; i < parent.childCount; i++) {
|
|
40
|
+
const child = parent.child(i);
|
|
41
|
+
if (child.isText && child.text && !(linkType && linkType.isInSet(child.marks))) {
|
|
42
|
+
regex.lastIndex = 0;
|
|
43
|
+
let m;
|
|
44
|
+
while ((m = regex.exec(child.text)) !== null) {
|
|
45
|
+
matches.push({
|
|
46
|
+
from: nodePos + m.index,
|
|
47
|
+
to: nodePos + m.index + m[0].length,
|
|
48
|
+
text: m[1] ?? "",
|
|
49
|
+
url: m[2] ?? ""
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
nodePos += child.nodeSize;
|
|
54
|
+
}
|
|
55
|
+
return matches;
|
|
56
|
+
}
|
|
57
|
+
function buildDecorations(state) {
|
|
58
|
+
const matches = findLinkPatterns(state, LINK_PATTERN_DECO);
|
|
59
|
+
if (matches.length === 0) return DecorationSet.empty;
|
|
60
|
+
const decorations = matches.map(
|
|
61
|
+
(m) => Decoration.inline(m.from, m.to, { class: "link-text-syntax" })
|
|
62
|
+
);
|
|
63
|
+
return DecorationSet.create(state.doc, decorations);
|
|
64
|
+
}
|
|
65
|
+
function cursorInsidePattern(pos, matches) {
|
|
66
|
+
return matches.some((m) => pos >= m.from && pos <= m.to);
|
|
67
|
+
}
|
|
68
|
+
function findLinkMarkAtPos(state, pos) {
|
|
69
|
+
const linkType = state.schema.marks.link;
|
|
70
|
+
if (!linkType) return null;
|
|
71
|
+
let resolved;
|
|
72
|
+
try {
|
|
73
|
+
resolved = state.doc.resolve(pos);
|
|
74
|
+
} catch {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
const parent = resolved.parent;
|
|
78
|
+
if (!parent.isTextblock) return null;
|
|
79
|
+
const base = resolved.start();
|
|
80
|
+
let runFrom = -1;
|
|
81
|
+
let runTo = -1;
|
|
82
|
+
let href = "";
|
|
83
|
+
const textParts = [];
|
|
84
|
+
let nodePos = base;
|
|
85
|
+
for (let i = 0; i < parent.childCount; i++) {
|
|
86
|
+
const child = parent.child(i);
|
|
87
|
+
const childEnd = nodePos + child.nodeSize;
|
|
88
|
+
const lm = linkType.isInSet(child.marks);
|
|
89
|
+
if (lm) {
|
|
90
|
+
if (runFrom === -1) {
|
|
91
|
+
runFrom = nodePos;
|
|
92
|
+
href = lm.attrs.href || "";
|
|
93
|
+
textParts.length = 0;
|
|
94
|
+
}
|
|
95
|
+
textParts.push(child.text || "");
|
|
96
|
+
runTo = childEnd;
|
|
97
|
+
} else {
|
|
98
|
+
if (runFrom !== -1 && pos >= runFrom && pos <= runTo) {
|
|
99
|
+
return { from: runFrom, to: runTo, text: textParts.join(""), href };
|
|
100
|
+
}
|
|
101
|
+
runFrom = -1;
|
|
102
|
+
runTo = -1;
|
|
103
|
+
href = "";
|
|
104
|
+
textParts.length = 0;
|
|
105
|
+
}
|
|
106
|
+
nodePos = childEnd;
|
|
107
|
+
}
|
|
108
|
+
if (runFrom !== -1 && pos >= runFrom && pos <= runTo) {
|
|
109
|
+
return { from: runFrom, to: runTo, text: textParts.join(""), href };
|
|
110
|
+
}
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
function createLinkTextPlugin() {
|
|
114
|
+
return new Plugin({
|
|
115
|
+
key: pluginKey,
|
|
116
|
+
state: {
|
|
117
|
+
init(_, state) {
|
|
118
|
+
return buildDecorations(state);
|
|
119
|
+
},
|
|
120
|
+
apply(tr, old, _, newState) {
|
|
121
|
+
if (!tr.docChanged) return old;
|
|
122
|
+
if (tr.getMeta("full-delete")) return DecorationSet.empty;
|
|
123
|
+
return buildDecorations(newState);
|
|
124
|
+
}
|
|
125
|
+
},
|
|
126
|
+
props: {
|
|
127
|
+
decorations(state) {
|
|
128
|
+
return this.getState(state);
|
|
129
|
+
}
|
|
130
|
+
},
|
|
131
|
+
appendTransaction(transactions, oldState, newState) {
|
|
132
|
+
if (transactions.some((tr2) => tr2.getMeta(pluginKey))) return null;
|
|
133
|
+
if (transactions.some((tr2) => tr2.getMeta("full-delete"))) return null;
|
|
134
|
+
const selChanged = transactions.some((tr2) => tr2.selectionSet);
|
|
135
|
+
const docChanged = transactions.some((tr2) => tr2.docChanged);
|
|
136
|
+
if (!selChanged && !docChanged) return null;
|
|
137
|
+
const linkType = newState.schema.marks.link;
|
|
138
|
+
if (!linkType) return null;
|
|
139
|
+
if (!newState.selection.empty) return null;
|
|
140
|
+
const newPos = newState.selection.from;
|
|
141
|
+
const oldPos = oldState.selection.from;
|
|
142
|
+
const linkInfo = findLinkMarkAtPos(newState, newPos);
|
|
143
|
+
if (linkInfo) {
|
|
144
|
+
const oldLinkInfo = findLinkMarkAtPos(oldState, oldPos);
|
|
145
|
+
if (!oldLinkInfo) {
|
|
146
|
+
const { from, to, text, href } = linkInfo;
|
|
147
|
+
const literal = `[${text}](${href})`;
|
|
148
|
+
const textNode = newState.schema.text(literal);
|
|
149
|
+
const tr2 = newState.tr.replaceWith(from, to, textNode);
|
|
150
|
+
tr2.setMeta(pluginKey, "expand");
|
|
151
|
+
tr2.setMeta("addToHistory", false);
|
|
152
|
+
const relPos = Math.max(0, Math.min(newPos - from, text.length));
|
|
153
|
+
const cursorPos = from + 1 + relPos;
|
|
154
|
+
try {
|
|
155
|
+
tr2.setSelection(TextSelection.create(tr2.doc, cursorPos));
|
|
156
|
+
} catch {
|
|
157
|
+
}
|
|
158
|
+
return tr2;
|
|
159
|
+
}
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
162
|
+
const oldBlockMatches = findLinkPatternsInBlock(oldState, oldPos, LINK_PATTERN_CONVERT);
|
|
163
|
+
if (oldBlockMatches.length === 0) return null;
|
|
164
|
+
const wasIn = oldBlockMatches.find((m) => oldPos >= m.from && oldPos <= m.to);
|
|
165
|
+
if (!wasIn) return null;
|
|
166
|
+
let target;
|
|
167
|
+
if (docChanged) {
|
|
168
|
+
let mappedFrom = wasIn.from;
|
|
169
|
+
for (const t of transactions) {
|
|
170
|
+
mappedFrom = t.mapping.map(mappedFrom);
|
|
171
|
+
}
|
|
172
|
+
if (mappedFrom < 0 || mappedFrom > newState.doc.content.size) return null;
|
|
173
|
+
const newBlockMatches = findLinkPatternsInBlock(newState, mappedFrom, LINK_PATTERN_CONVERT);
|
|
174
|
+
if (cursorInsidePattern(newPos, newBlockMatches)) return null;
|
|
175
|
+
target = newBlockMatches.find((m) => Math.abs(m.from - mappedFrom) < 3);
|
|
176
|
+
} else {
|
|
177
|
+
const newBlockMatches = findLinkPatternsInBlock(newState, wasIn.from, LINK_PATTERN_CONVERT);
|
|
178
|
+
if (cursorInsidePattern(newPos, newBlockMatches)) return null;
|
|
179
|
+
target = newBlockMatches.find((m) => m.from === wasIn.from && m.to === wasIn.to);
|
|
180
|
+
}
|
|
181
|
+
if (!target || !target.text || !target.url) return null;
|
|
182
|
+
const mark = linkType.create({ href: target.url });
|
|
183
|
+
const linkNode = newState.schema.text(target.text, [mark]);
|
|
184
|
+
const tr = newState.tr.replaceWith(target.from, target.to, linkNode);
|
|
185
|
+
tr.setMeta(pluginKey, "collapse");
|
|
186
|
+
tr.setMeta("addToHistory", false);
|
|
187
|
+
return tr;
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
export {
|
|
192
|
+
createLinkTextPlugin
|
|
193
|
+
};
|
|
194
|
+
//# sourceMappingURL=link-text-plugin.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/plugins/link-text-plugin.ts"],"sourcesContent":["/**\n * Link text plugin — Typora-style inline link editing:\n *\n * 1. **Decoration**: Scans text nodes for `[...](...)` literal patterns and\n * applies a muted CSS class so users can distinguish link syntax from text.\n *\n * 2. **Collapse**: When cursor leaves a `[text](url)` pattern (both non-empty),\n * auto-convert to a ProseMirror link mark (rendered as clickable link).\n *\n * 3. **Expand**: When cursor enters a rendered link mark, replace the mark\n * with literal text `[text](url)` so the user can edit both text and URL\n * directly inline.\n *\n * Schema-agnostic: uses `state.schema.marks.link` rather than an imported\n * singleton, so the plugin works against any consumer-injected schema.\n */\n\nimport { Plugin, PluginKey, TextSelection } from 'prosemirror-state'\nimport type { EditorState } from 'prosemirror-state'\nimport { Decoration, DecorationSet } from 'prosemirror-view'\n\nconst pluginKey = new PluginKey('moraya-link-text')\n\n/** Matches [text](url) for decoration — allows empty text or url. */\nconst LINK_PATTERN_DECO = /\\[([^\\]]*)\\]\\(([^)]*)\\)/g\n\n/** Matches [text](url) for conversion — requires non-empty text AND url. */\nconst LINK_PATTERN_CONVERT = /\\[([^\\]]+)\\]\\(([^)]+)\\)/g\n\ninterface LinkMatch {\n from: number\n to: number\n text: string\n url: string\n}\n\ninterface LinkMarkInfo {\n from: number\n to: number\n text: string\n href: string\n}\n\n/**\n * Find all [text](url) literal text patterns NOT inside a link mark.\n */\nfunction findLinkPatterns(state: EditorState, regex: RegExp): LinkMatch[] {\n const matches: LinkMatch[] = []\n const linkType = state.schema.marks.link\n\n state.doc.descendants((node, pos) => {\n if (!node.isText || !node.text) return\n if (linkType && linkType.isInSet(node.marks)) return\n\n regex.lastIndex = 0\n let m: RegExpExecArray | null\n while ((m = regex.exec(node.text)) !== null) {\n matches.push({\n from: pos + m.index,\n to: pos + m.index + m[0].length,\n text: m[1] ?? '',\n url: m[2] ?? '',\n })\n }\n })\n\n return matches\n}\n\n/**\n * Find [text](url) patterns only within the textblock containing `pos`.\n * Much cheaper than full-doc scan for appendTransaction checks.\n */\nfunction findLinkPatternsInBlock(state: EditorState, pos: number, regex: RegExp): LinkMatch[] {\n const matches: LinkMatch[] = []\n const linkType = state.schema.marks.link\n let resolved\n try { resolved = state.doc.resolve(pos) } catch { return matches }\n const parent = resolved.parent\n if (!parent.isTextblock) return matches\n\n const base = resolved.start()\n let nodePos = base\n for (let i = 0; i < parent.childCount; i++) {\n const child = parent.child(i)\n if (child.isText && child.text && !(linkType && linkType.isInSet(child.marks))) {\n regex.lastIndex = 0\n let m: RegExpExecArray | null\n while ((m = regex.exec(child.text)) !== null) {\n matches.push({\n from: nodePos + m.index,\n to: nodePos + m.index + m[0].length,\n text: m[1] ?? '',\n url: m[2] ?? '',\n })\n }\n }\n nodePos += child.nodeSize\n }\n return matches\n}\n\nfunction buildDecorations(state: EditorState): DecorationSet {\n const matches = findLinkPatterns(state, LINK_PATTERN_DECO)\n if (matches.length === 0) return DecorationSet.empty\n\n const decorations = matches.map((m) =>\n Decoration.inline(m.from, m.to, { class: 'link-text-syntax' }),\n )\n return DecorationSet.create(state.doc, decorations)\n}\n\nfunction cursorInsidePattern(pos: number, matches: LinkMatch[]): boolean {\n return matches.some((m) => pos >= m.from && pos <= m.to)\n}\n\n/**\n * Find link mark range containing `pos`. Returns mark info or null.\n */\nfunction findLinkMarkAtPos(state: EditorState, pos: number): LinkMarkInfo | null {\n const linkType = state.schema.marks.link\n if (!linkType) return null\n\n let resolved\n try { resolved = state.doc.resolve(pos) } catch { return null }\n const parent = resolved.parent\n if (!parent.isTextblock) return null\n\n const base = resolved.start()\n let runFrom = -1\n let runTo = -1\n let href = ''\n const textParts: string[] = []\n let nodePos = base\n\n for (let i = 0; i < parent.childCount; i++) {\n const child = parent.child(i)\n const childEnd = nodePos + child.nodeSize\n const lm = linkType.isInSet(child.marks)\n\n if (lm) {\n if (runFrom === -1) {\n runFrom = nodePos\n href = (lm.attrs.href as string) || ''\n textParts.length = 0\n }\n textParts.push(child.text || '')\n runTo = childEnd\n } else {\n if (runFrom !== -1 && pos >= runFrom && pos <= runTo) {\n return { from: runFrom, to: runTo, text: textParts.join(''), href }\n }\n runFrom = -1\n runTo = -1\n href = ''\n textParts.length = 0\n }\n nodePos = childEnd\n }\n\n if (runFrom !== -1 && pos >= runFrom && pos <= runTo) {\n return { from: runFrom, to: runTo, text: textParts.join(''), href }\n }\n return null\n}\n\nexport function createLinkTextPlugin(): Plugin {\n return new Plugin({\n key: pluginKey,\n\n state: {\n init(_, state) { return buildDecorations(state) },\n apply(tr, old, _, newState) {\n // Decorations depend only on document content, not cursor position.\n // Selection-only changes can reuse existing decorations via mapping.\n if (!tr.docChanged) return old\n // Full-delete: new doc is tiny, rebuild directly (skip mapping old decos)\n if (tr.getMeta('full-delete')) return DecorationSet.empty\n return buildDecorations(newState)\n },\n },\n\n props: {\n decorations(state) { return this.getState(state) },\n },\n\n appendTransaction(transactions, oldState, newState) {\n // Skip if this plugin already produced a transaction in this batch\n if (transactions.some((tr) => tr.getMeta(pluginKey))) return null\n\n // Skip for full-delete transactions (entire document replaced)\n if (transactions.some((tr) => tr.getMeta('full-delete'))) return null\n\n const selChanged = transactions.some((tr) => tr.selectionSet)\n const docChanged = transactions.some((tr) => tr.docChanged)\n if (!selChanged && !docChanged) return null\n\n const linkType = newState.schema.marks.link\n if (!linkType) return null\n if (!newState.selection.empty) return null\n\n const newPos = newState.selection.from\n const oldPos = oldState.selection.from\n\n // ── EXPAND: cursor just entered a link mark ──\n const linkInfo = findLinkMarkAtPos(newState, newPos)\n if (linkInfo) {\n // Only expand if cursor was NOT in a link mark in old state\n const oldLinkInfo = findLinkMarkAtPos(oldState, oldPos)\n if (!oldLinkInfo) {\n const { from, to, text, href } = linkInfo\n const literal = `[${text}](${href})`\n const textNode = newState.schema.text(literal)\n const tr = newState.tr.replaceWith(from, to, textNode)\n tr.setMeta(pluginKey, 'expand')\n tr.setMeta('addToHistory', false)\n\n // Place cursor at same relative offset within text portion\n const relPos = Math.max(0, Math.min(newPos - from, text.length))\n const cursorPos = from + 1 + relPos // +1 for the `[`\n try {\n tr.setSelection(TextSelection.create(tr.doc, cursorPos))\n } catch { /* ignore */ }\n return tr\n }\n return null\n }\n\n // ── COLLAPSE: cursor left a [text](url) pattern ──\n // Use block-local scan instead of full-doc scan for performance.\n const oldBlockMatches = findLinkPatternsInBlock(oldState, oldPos, LINK_PATTERN_CONVERT)\n if (oldBlockMatches.length === 0) return null\n\n const wasIn = oldBlockMatches.find((m) => oldPos >= m.from && oldPos <= m.to)\n if (!wasIn) return null\n\n // Find the pattern in the new state (scan only the affected block)\n let target: LinkMatch | undefined\n if (docChanged) {\n let mappedFrom = wasIn.from\n for (const t of transactions) {\n mappedFrom = t.mapping.map(mappedFrom)\n }\n // After large deletes, the mapped position may be invalid — bail out\n if (mappedFrom < 0 || mappedFrom > newState.doc.content.size) return null\n const newBlockMatches = findLinkPatternsInBlock(newState, mappedFrom, LINK_PATTERN_CONVERT)\n if (cursorInsidePattern(newPos, newBlockMatches)) return null\n target = newBlockMatches.find((m) => Math.abs(m.from - mappedFrom) < 3)\n } else {\n const newBlockMatches = findLinkPatternsInBlock(newState, wasIn.from, LINK_PATTERN_CONVERT)\n if (cursorInsidePattern(newPos, newBlockMatches)) return null\n target = newBlockMatches.find((m) => m.from === wasIn.from && m.to === wasIn.to)\n }\n\n if (!target || !target.text || !target.url) return null\n\n const mark = linkType.create({ href: target.url })\n const linkNode = newState.schema.text(target.text, [mark])\n const tr = newState.tr.replaceWith(target.from, target.to, linkNode)\n tr.setMeta(pluginKey, 'collapse')\n tr.setMeta('addToHistory', false)\n return tr\n },\n })\n}\n"],"mappings":"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","names":["tr"]}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mermaid renderer — lazy-loads the mermaid library and provides a render API.
|
|
3
|
+
*
|
|
4
|
+
* This is a utility module imported on-demand by `code-block-view.ts`,
|
|
5
|
+
* NOT itself a ProseMirror plugin. The mermaid library (~2.4 MB) is loaded
|
|
6
|
+
* only when the first mermaid code block is encountered, via dynamic
|
|
7
|
+
* `import('mermaid')`. Consumers that want mermaid support must install
|
|
8
|
+
* `mermaid` as a peer dependency.
|
|
9
|
+
*
|
|
10
|
+
* IMPORTANT: `mermaid.render()` manipulates global DOM state and is NOT safe
|
|
11
|
+
* to call concurrently. All renders go through a serial queue.
|
|
12
|
+
*/
|
|
13
|
+
declare function ensureMermaidLoaded(): Promise<void>;
|
|
14
|
+
declare function renderMermaid(code: string): Promise<{
|
|
15
|
+
svg: string;
|
|
16
|
+
} | {
|
|
17
|
+
error: string;
|
|
18
|
+
}>;
|
|
19
|
+
/**
|
|
20
|
+
* Re-initialize mermaid with updated theme. Called when theme changes.
|
|
21
|
+
*/
|
|
22
|
+
declare function updateMermaidTheme(): void;
|
|
23
|
+
|
|
24
|
+
export { ensureMermaidLoaded, renderMermaid, updateMermaidTheme };
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// src/plugins/mermaid-renderer.ts
|
|
2
|
+
var mermaidModule = null;
|
|
3
|
+
var loadingPromise = null;
|
|
4
|
+
var renderCounter = 0;
|
|
5
|
+
var renderQueue = Promise.resolve();
|
|
6
|
+
function isDark() {
|
|
7
|
+
if (typeof document === "undefined") return false;
|
|
8
|
+
const dt = document.documentElement.getAttribute("data-theme");
|
|
9
|
+
if (dt === "dark") return true;
|
|
10
|
+
if (dt === "light") return false;
|
|
11
|
+
if (typeof window === "undefined" || !window.matchMedia) return false;
|
|
12
|
+
return window.matchMedia("(prefers-color-scheme: dark)").matches;
|
|
13
|
+
}
|
|
14
|
+
function resolveThemeColors() {
|
|
15
|
+
if (typeof document === "undefined" || typeof getComputedStyle === "undefined") {
|
|
16
|
+
return {
|
|
17
|
+
primaryColor: "#4a90d9",
|
|
18
|
+
primaryTextColor: "#333",
|
|
19
|
+
primaryBorderColor: "#ccc",
|
|
20
|
+
lineColor: "#666",
|
|
21
|
+
secondaryColor: "#f5f5f5",
|
|
22
|
+
tertiaryColor: "#eee"
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
const s = getComputedStyle(document.documentElement);
|
|
26
|
+
return {
|
|
27
|
+
primaryColor: s.getPropertyValue("--accent-color").trim() || "#4a90d9",
|
|
28
|
+
primaryTextColor: s.getPropertyValue("--text-primary").trim() || "#333",
|
|
29
|
+
primaryBorderColor: s.getPropertyValue("--border-color").trim() || "#ccc",
|
|
30
|
+
lineColor: s.getPropertyValue("--text-secondary").trim() || "#666",
|
|
31
|
+
secondaryColor: s.getPropertyValue("--bg-secondary").trim() || "#f5f5f5",
|
|
32
|
+
tertiaryColor: s.getPropertyValue("--bg-hover").trim() || "#eee"
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
async function ensureMermaidLoaded() {
|
|
36
|
+
if (mermaidModule) return;
|
|
37
|
+
if (loadingPromise) return loadingPromise;
|
|
38
|
+
loadingPromise = (async () => {
|
|
39
|
+
const mod = await import(
|
|
40
|
+
/* @vite-ignore */
|
|
41
|
+
"mermaid"
|
|
42
|
+
);
|
|
43
|
+
mermaidModule = mod.default;
|
|
44
|
+
mermaidModule.initialize({
|
|
45
|
+
startOnLoad: false,
|
|
46
|
+
theme: isDark() ? "dark" : "default",
|
|
47
|
+
themeVariables: resolveThemeColors()
|
|
48
|
+
});
|
|
49
|
+
})();
|
|
50
|
+
return loadingPromise;
|
|
51
|
+
}
|
|
52
|
+
async function renderMermaid(code) {
|
|
53
|
+
await ensureMermaidLoaded();
|
|
54
|
+
const result = new Promise((resolve) => {
|
|
55
|
+
renderQueue = renderQueue.then(async () => {
|
|
56
|
+
const id = `mermaid-${++renderCounter}`;
|
|
57
|
+
try {
|
|
58
|
+
const { svg } = await mermaidModule.render(id, code);
|
|
59
|
+
resolve({ svg });
|
|
60
|
+
} catch (e) {
|
|
61
|
+
resolve({ error: e instanceof Error ? e.message : "Render failed" });
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
return result;
|
|
66
|
+
}
|
|
67
|
+
function updateMermaidTheme() {
|
|
68
|
+
if (!mermaidModule) return;
|
|
69
|
+
mermaidModule.initialize({
|
|
70
|
+
startOnLoad: false,
|
|
71
|
+
theme: isDark() ? "dark" : "default",
|
|
72
|
+
themeVariables: resolveThemeColors()
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
export {
|
|
76
|
+
ensureMermaidLoaded,
|
|
77
|
+
renderMermaid,
|
|
78
|
+
updateMermaidTheme
|
|
79
|
+
};
|
|
80
|
+
//# sourceMappingURL=mermaid-renderer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/plugins/mermaid-renderer.ts"],"sourcesContent":["/**\n * Mermaid renderer — lazy-loads the mermaid library and provides a render API.\n *\n * This is a utility module imported on-demand by `code-block-view.ts`,\n * NOT itself a ProseMirror plugin. The mermaid library (~2.4 MB) is loaded\n * only when the first mermaid code block is encountered, via dynamic\n * `import('mermaid')`. Consumers that want mermaid support must install\n * `mermaid` as a peer dependency.\n *\n * IMPORTANT: `mermaid.render()` manipulates global DOM state and is NOT safe\n * to call concurrently. All renders go through a serial queue.\n */\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nlet mermaidModule: any = null\nlet loadingPromise: Promise<void> | null = null\nlet renderCounter = 0\n\n// ── Serial render queue ──────────────────────────\n// Mermaid.render() creates a temp SVG in the DOM, measures text, then removes\n// it. If two renders overlap, the second corrupts the first's temp element,\n// causing \"Render failed\". This queue ensures only one render runs at a time.\nlet renderQueue: Promise<void> = Promise.resolve()\n\nfunction isDark(): boolean {\n if (typeof document === 'undefined') return false\n const dt = document.documentElement.getAttribute('data-theme')\n if (dt === 'dark') return true\n if (dt === 'light') return false\n if (typeof window === 'undefined' || !window.matchMedia) return false\n return window.matchMedia('(prefers-color-scheme: dark)').matches\n}\n\n/** Read resolved CSS custom property values from :root */\nfunction resolveThemeColors() {\n if (typeof document === 'undefined' || typeof getComputedStyle === 'undefined') {\n return {\n primaryColor: '#4a90d9',\n primaryTextColor: '#333',\n primaryBorderColor: '#ccc',\n lineColor: '#666',\n secondaryColor: '#f5f5f5',\n tertiaryColor: '#eee',\n }\n }\n const s = getComputedStyle(document.documentElement)\n return {\n primaryColor: s.getPropertyValue('--accent-color').trim() || '#4a90d9',\n primaryTextColor: s.getPropertyValue('--text-primary').trim() || '#333',\n primaryBorderColor: s.getPropertyValue('--border-color').trim() || '#ccc',\n lineColor: s.getPropertyValue('--text-secondary').trim() || '#666',\n secondaryColor: s.getPropertyValue('--bg-secondary').trim() || '#f5f5f5',\n tertiaryColor: s.getPropertyValue('--bg-hover').trim() || '#eee',\n }\n}\n\nexport async function ensureMermaidLoaded(): Promise<void> {\n if (mermaidModule) return\n if (loadingPromise) return loadingPromise\n\n loadingPromise = (async () => {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const mod: any = await import(/* @vite-ignore */ 'mermaid')\n mermaidModule = mod.default\n mermaidModule.initialize({\n startOnLoad: false,\n theme: isDark() ? 'dark' : 'default',\n themeVariables: resolveThemeColors(),\n })\n })()\n\n return loadingPromise\n}\n\nexport async function renderMermaid(\n code: string,\n): Promise<{ svg: string } | { error: string }> {\n await ensureMermaidLoaded()\n\n // Enqueue: wait for previous render to finish before starting this one\n const result = new Promise<{ svg: string } | { error: string }>((resolve) => {\n renderQueue = renderQueue.then(async () => {\n const id = `mermaid-${++renderCounter}`\n try {\n const { svg } = await mermaidModule.render(id, code)\n resolve({ svg })\n } catch (e) {\n resolve({ error: e instanceof Error ? e.message : 'Render failed' })\n }\n })\n })\n\n return result\n}\n\n/**\n * Re-initialize mermaid with updated theme. Called when theme changes.\n */\nexport function updateMermaidTheme(): void {\n if (!mermaidModule) return\n mermaidModule.initialize({\n startOnLoad: false,\n theme: isDark() ? 'dark' : 'default',\n themeVariables: resolveThemeColors(),\n })\n}\n"],"mappings":";AAcA,IAAI,gBAAqB;AACzB,IAAI,iBAAuC;AAC3C,IAAI,gBAAgB;AAMpB,IAAI,cAA6B,QAAQ,QAAQ;AAEjD,SAAS,SAAkB;AACzB,MAAI,OAAO,aAAa,YAAa,QAAO;AAC5C,QAAM,KAAK,SAAS,gBAAgB,aAAa,YAAY;AAC7D,MAAI,OAAO,OAAQ,QAAO;AAC1B,MAAI,OAAO,QAAS,QAAO;AAC3B,MAAI,OAAO,WAAW,eAAe,CAAC,OAAO,WAAY,QAAO;AAChE,SAAO,OAAO,WAAW,8BAA8B,EAAE;AAC3D;AAGA,SAAS,qBAAqB;AAC5B,MAAI,OAAO,aAAa,eAAe,OAAO,qBAAqB,aAAa;AAC9E,WAAO;AAAA,MACL,cAAc;AAAA,MACd,kBAAkB;AAAA,MAClB,oBAAoB;AAAA,MACpB,WAAW;AAAA,MACX,gBAAgB;AAAA,MAChB,eAAe;AAAA,IACjB;AAAA,EACF;AACA,QAAM,IAAI,iBAAiB,SAAS,eAAe;AACnD,SAAO;AAAA,IACL,cAAc,EAAE,iBAAiB,gBAAgB,EAAE,KAAK,KAAK;AAAA,IAC7D,kBAAkB,EAAE,iBAAiB,gBAAgB,EAAE,KAAK,KAAK;AAAA,IACjE,oBAAoB,EAAE,iBAAiB,gBAAgB,EAAE,KAAK,KAAK;AAAA,IACnE,WAAW,EAAE,iBAAiB,kBAAkB,EAAE,KAAK,KAAK;AAAA,IAC5D,gBAAgB,EAAE,iBAAiB,gBAAgB,EAAE,KAAK,KAAK;AAAA,IAC/D,eAAe,EAAE,iBAAiB,YAAY,EAAE,KAAK,KAAK;AAAA,EAC5D;AACF;AAEA,eAAsB,sBAAqC;AACzD,MAAI,cAAe;AACnB,MAAI,eAAgB,QAAO;AAE3B,oBAAkB,YAAY;AAE5B,UAAM,MAAW,MAAM;AAAA;AAAA,MAA0B;AAAA,IAAS;AAC1D,oBAAgB,IAAI;AACpB,kBAAc,WAAW;AAAA,MACvB,aAAa;AAAA,MACb,OAAO,OAAO,IAAI,SAAS;AAAA,MAC3B,gBAAgB,mBAAmB;AAAA,IACrC,CAAC;AAAA,EACH,GAAG;AAEH,SAAO;AACT;AAEA,eAAsB,cACpB,MAC8C;AAC9C,QAAM,oBAAoB;AAG1B,QAAM,SAAS,IAAI,QAA6C,CAAC,YAAY;AAC3E,kBAAc,YAAY,KAAK,YAAY;AACzC,YAAM,KAAK,WAAW,EAAE,aAAa;AACrC,UAAI;AACF,cAAM,EAAE,IAAI,IAAI,MAAM,cAAc,OAAO,IAAI,IAAI;AACnD,gBAAQ,EAAE,IAAI,CAAC;AAAA,MACjB,SAAS,GAAG;AACV,gBAAQ,EAAE,OAAO,aAAa,QAAQ,EAAE,UAAU,gBAAgB,CAAC;AAAA,MACrE;AAAA,IACF,CAAC;AAAA,EACH,CAAC;AAED,SAAO;AACT;AAKO,SAAS,qBAA2B;AACzC,MAAI,CAAC,cAAe;AACpB,gBAAc,WAAW;AAAA,IACvB,aAAa;AAAA,IACb,OAAO,OAAO,IAAI,SAAS;AAAA,IAC3B,gBAAgB,mBAAmB;AAAA,EACrC,CAAC;AACH;","names":[]}
|
package/dist/schema.d.ts
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { Schema } from 'prosemirror-model';
|
|
2
|
+
export { Node as PmNode } from 'prosemirror-model';
|
|
3
|
+
import { SchemaConfig } from './types.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Unified ProseMirror Schema for `@moraya/core`.
|
|
7
|
+
*
|
|
8
|
+
* Faithful 1:1 migration from Moraya desktop `src/lib/editor/schema.ts`
|
|
9
|
+
* with the following DI changes (v0.60.0-pre §F2.5):
|
|
10
|
+
* - All Tauri IPC `read_file_binary` / `plugin-http` calls in image / media
|
|
11
|
+
* loaders are replaced by consumer-injected `MediaResolver` methods.
|
|
12
|
+
* - Schema NodeSpecs that depend on the resolver (image, html_inline) are
|
|
13
|
+
* built inside `createSchema(config)` factory body, capturing `config`
|
|
14
|
+
* in closures for `toDOM`. Other NodeSpecs are pure data.
|
|
15
|
+
* - Module-level `documentBaseDir` + `setDocumentBaseDir` is preserved
|
|
16
|
+
* (pure string state, not Tauri-coupled).
|
|
17
|
+
* - Per §6.1.1: this module does NOT export the default schema. It is
|
|
18
|
+
* used internally by parseMarkdown / serializeMarkdown only.
|
|
19
|
+
*
|
|
20
|
+
* Nodes (23): doc, text, paragraph, heading, blockquote, code_block,
|
|
21
|
+
* horizontal_rule, bullet_list, ordered_list, list_item, image,
|
|
22
|
+
* hardbreak, html_block, html_inline, table, table_header_row, table_row,
|
|
23
|
+
* table_header, table_cell, math_inline, math_block,
|
|
24
|
+
* defList, defListTerm, defListDescription
|
|
25
|
+
*
|
|
26
|
+
* Marks (6): html_mark, strong, em, code, link, strike_through
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
/** Update the base directory used to resolve relative image paths. */
|
|
30
|
+
declare function setDocumentBaseDir(dir: string): void;
|
|
31
|
+
/** Read the current base dir. Exposed for consumers that need to coordinate (e.g. tests). */
|
|
32
|
+
declare function getDocumentBaseDir(): string;
|
|
33
|
+
/**
|
|
34
|
+
* Internal default schema (uses {@link nullMediaResolver}).
|
|
35
|
+
* Used by parseMarkdown / serializeMarkdown when no real consumer schema
|
|
36
|
+
* is available. Per §6.1.1 NOT exported via index.ts — consumers must call
|
|
37
|
+
* createSchema(config) with a real MediaResolver.
|
|
38
|
+
*/
|
|
39
|
+
declare const defaultSchema: Schema<string, string>;
|
|
40
|
+
/**
|
|
41
|
+
* Create a ProseMirror Schema with consumer-injected dependencies.
|
|
42
|
+
*
|
|
43
|
+
* @throws TypeError if `config.mediaResolver` is missing or is the internal
|
|
44
|
+
* nullMediaResolver sentinel.
|
|
45
|
+
*/
|
|
46
|
+
declare function createSchema(config: SchemaConfig): Schema;
|
|
47
|
+
|
|
48
|
+
export { SchemaConfig, createSchema, defaultSchema, getDocumentBaseDir, setDocumentBaseDir };
|