@segnavia/markdown-editor 0.0.0-stage → 0.42.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/dist/editor-css.js +18 -0
- package/dist/editor-style.js +8 -0
- package/dist/markdown-editor-font-registration.js +32 -0
- package/dist/markdown-editor-fonts.js +56 -0
- package/dist/markdown-editor-link.js +9 -0
- package/dist/markdown-editor-model.d.ts +3 -0
- package/dist/markdown-editor-model.js +76 -0
- package/dist/markdown-editor-options.js +38 -0
- package/dist/markdown-editor-policy.d.ts +83 -0
- package/dist/markdown-editor-policy.js +117 -0
- package/dist/markdown-editor-types.d.ts +28 -0
- package/dist/markdown-editor.d.ts +12 -0
- package/dist/markdown-editor.js +348 -0
- package/dist/markdown-rich-commands.js +114 -0
- package/dist/markdown-rich-editor.js +79 -0
- package/dist/markdown-rich-extensions.js +61 -0
- package/dist/markdown-rich-locked.js +97 -0
- package/dist/markdown-rich-save.js +182 -0
- package/dist/markdown-source.js +119 -0
- package/dist/markdown-title.d.ts +13 -0
- package/dist/markdown-title.js +43 -0
- package/package.json +72 -4
- package/README.md +0 -3
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { isDiagramFence, isGithubAlertBlock } from "@segnavia/html";
|
|
2
|
+
import { Node } from "@tiptap/core";
|
|
3
|
+
//#region src/markdown-rich-locked.ts
|
|
4
|
+
const LOCKED_BLOCK_NAME = "lockedBlock";
|
|
5
|
+
function lockedFenceLabel(lockedFences, language) {
|
|
6
|
+
return lockedFences[language?.toLowerCase() ?? ""];
|
|
7
|
+
}
|
|
8
|
+
function matchFrontmatter(src) {
|
|
9
|
+
return /^---[ \t]*\n[\s\S]*?\n---[ \t]*(?:\n|$)/.exec(src)?.[0];
|
|
10
|
+
}
|
|
11
|
+
function matchDirectiveContainer(src) {
|
|
12
|
+
const firstNl = src.indexOf("\n");
|
|
13
|
+
if (firstNl < 0) return void 0;
|
|
14
|
+
const open = /^(:{3,})[^\n]*$/.exec(src.slice(0, firstNl));
|
|
15
|
+
if (!open) return void 0;
|
|
16
|
+
const fenceLen = open[1]?.length ?? 0;
|
|
17
|
+
let pos = firstNl + 1;
|
|
18
|
+
while (pos <= src.length) {
|
|
19
|
+
const nextNl = src.indexOf("\n", pos);
|
|
20
|
+
const lineEnd = nextNl < 0 ? src.length : nextNl;
|
|
21
|
+
if ((/^(:{3,})[ \t]*$/.exec(src.slice(pos, lineEnd))?.[1]?.length ?? 0) >= fenceLen) return src.slice(0, nextNl < 0 ? src.length : nextNl + 1);
|
|
22
|
+
if (nextNl < 0) return void 0;
|
|
23
|
+
pos = nextNl + 1;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
function isHtmlComment(raw) {
|
|
27
|
+
return /^<!--[\s\S]*-->$/.test(raw.replace(/\n+$/, ""));
|
|
28
|
+
}
|
|
29
|
+
function isLockedToken(token, lockedFences = {}) {
|
|
30
|
+
if (token.type === "frontmatter" || token.type === "directive") return true;
|
|
31
|
+
if (token.type === "code" && (isDiagramFence(token.lang) || Boolean(lockedFenceLabel(lockedFences, token.lang)))) return true;
|
|
32
|
+
if (token.type === "blockquote" && isGithubAlertBlock(token.raw)) return true;
|
|
33
|
+
return token.type === "html" && !isHtmlComment(token.raw);
|
|
34
|
+
}
|
|
35
|
+
function isSilentToken(token) {
|
|
36
|
+
return token.type === "def" || token.type === "html" && isHtmlComment(token.raw);
|
|
37
|
+
}
|
|
38
|
+
function lockedNode(source, label) {
|
|
39
|
+
return {
|
|
40
|
+
type: LOCKED_BLOCK_NAME,
|
|
41
|
+
attrs: {
|
|
42
|
+
source,
|
|
43
|
+
label: label ?? null,
|
|
44
|
+
dir: "auto"
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
const LockedBlock = Node.create({
|
|
49
|
+
name: LOCKED_BLOCK_NAME,
|
|
50
|
+
group: "block",
|
|
51
|
+
atom: true,
|
|
52
|
+
selectable: true,
|
|
53
|
+
draggable: false,
|
|
54
|
+
addAttributes() {
|
|
55
|
+
return {
|
|
56
|
+
source: { default: "" },
|
|
57
|
+
label: { default: null },
|
|
58
|
+
dir: { default: "auto" }
|
|
59
|
+
};
|
|
60
|
+
},
|
|
61
|
+
parseHTML() {
|
|
62
|
+
return [{ tag: "div[data-locked-block]" }];
|
|
63
|
+
},
|
|
64
|
+
renderHTML({ HTMLAttributes }) {
|
|
65
|
+
return ["div", {
|
|
66
|
+
...HTMLAttributes,
|
|
67
|
+
"data-locked-block": "",
|
|
68
|
+
dir: "auto"
|
|
69
|
+
}];
|
|
70
|
+
},
|
|
71
|
+
renderMarkdown(node) {
|
|
72
|
+
return typeof node.attrs?.source === "string" ? node.attrs.source : "";
|
|
73
|
+
},
|
|
74
|
+
markdownTokenizer: {
|
|
75
|
+
name: "lockedFence",
|
|
76
|
+
level: "block",
|
|
77
|
+
start(src) {
|
|
78
|
+
return src.startsWith("---") || src.startsWith(":::") ? 0 : -1;
|
|
79
|
+
},
|
|
80
|
+
tokenize(src, tokens) {
|
|
81
|
+
if (tokens.length === 0) {
|
|
82
|
+
const frontmatter = matchFrontmatter(src);
|
|
83
|
+
if (frontmatter) return {
|
|
84
|
+
type: "frontmatter",
|
|
85
|
+
raw: frontmatter
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
const directive = matchDirectiveContainer(src);
|
|
89
|
+
return directive ? {
|
|
90
|
+
type: "directive",
|
|
91
|
+
raw: directive
|
|
92
|
+
} : void 0;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
//#endregion
|
|
97
|
+
export { LOCKED_BLOCK_NAME, LockedBlock, isLockedToken, isSilentToken, lockedFenceLabel, lockedNode };
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { isLockedToken, isSilentToken, lockedFenceLabel, lockedNode } from "./markdown-rich-locked.js";
|
|
2
|
+
import { richMarkdownManager, richSchema } from "./markdown-rich-extensions.js";
|
|
3
|
+
import { Node } from "@tiptap/pm/model";
|
|
4
|
+
//#region src/markdown-rich-save.ts
|
|
5
|
+
let cachedOriginal;
|
|
6
|
+
const objectKeys = /* @__PURE__ */ new WeakMap();
|
|
7
|
+
function isRecord(value) {
|
|
8
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
9
|
+
}
|
|
10
|
+
function keptAttrs(attrs) {
|
|
11
|
+
const kept = Object.fromEntries(Object.entries(attrs).filter(([, value]) => value !== null && value !== void 0 && (!Array.isArray(value) || value.length > 0)));
|
|
12
|
+
return Object.keys(kept).length ? kept : void 0;
|
|
13
|
+
}
|
|
14
|
+
function canonical(node) {
|
|
15
|
+
const next = { type: node.type };
|
|
16
|
+
if (typeof node.text === "string") next.text = node.text;
|
|
17
|
+
const attrs = node.attrs && isRecord(node.attrs) ? keptAttrs(node.attrs) : void 0;
|
|
18
|
+
if (attrs) next.attrs = attrs;
|
|
19
|
+
if (node.marks?.length) next.marks = node.marks.map((mark) => canonical(mark));
|
|
20
|
+
if (node.content?.length) next.content = node.content.map(canonical);
|
|
21
|
+
return next;
|
|
22
|
+
}
|
|
23
|
+
function asJson(node) {
|
|
24
|
+
return node instanceof Node ? node.toJSON() : node;
|
|
25
|
+
}
|
|
26
|
+
function nodeKey(node) {
|
|
27
|
+
const cached = objectKeys.get(node);
|
|
28
|
+
if (cached !== void 0) return cached;
|
|
29
|
+
const key = JSON.stringify(canonical(asJson(node)));
|
|
30
|
+
objectKeys.set(node, key);
|
|
31
|
+
return key;
|
|
32
|
+
}
|
|
33
|
+
const isEmptyParagraph = (node) => {
|
|
34
|
+
const json = asJson(node);
|
|
35
|
+
return json.type === "paragraph" && !json.content?.length && !json.text;
|
|
36
|
+
};
|
|
37
|
+
const dropTrailingEmptyParagraph = (nodes) => {
|
|
38
|
+
const last = nodes.at(-1);
|
|
39
|
+
return last && isEmptyParagraph(last) ? nodes.slice(0, -1) : nodes;
|
|
40
|
+
};
|
|
41
|
+
const restoreAmpersands = (markdown) => markdown.replace(/&(?![a-zA-Z][a-zA-Z0-9]*;)/g, "&");
|
|
42
|
+
function parseBlock(token, raw, lockedFences) {
|
|
43
|
+
if (isSilentToken(token)) return [];
|
|
44
|
+
if (isLockedToken(token, lockedFences)) return [lockedNode(raw, lockedFenceLabel(lockedFences, token.lang))];
|
|
45
|
+
const parsed = richMarkdownManager().parse(raw);
|
|
46
|
+
return Node.fromJSON(richSchema(), parsed).toJSON().content ?? [];
|
|
47
|
+
}
|
|
48
|
+
function blocks(markdown, lockedFences = {}) {
|
|
49
|
+
const out = [];
|
|
50
|
+
const tokens = richMarkdownManager().instance.lexer(markdown);
|
|
51
|
+
let at = 0;
|
|
52
|
+
let prevEnd = 0;
|
|
53
|
+
for (const token of tokens) {
|
|
54
|
+
const start = at;
|
|
55
|
+
at += token.raw.length;
|
|
56
|
+
if (token.type === "space") continue;
|
|
57
|
+
const raw = token.raw.replace(/\n+$/, "");
|
|
58
|
+
const nodes = parseBlock(token, raw, lockedFences);
|
|
59
|
+
out.push({
|
|
60
|
+
raw,
|
|
61
|
+
sep: markdown.slice(prevEnd, start),
|
|
62
|
+
nodes,
|
|
63
|
+
keys: nodes.map(nodeKey)
|
|
64
|
+
});
|
|
65
|
+
prevEnd = start + raw.length;
|
|
66
|
+
}
|
|
67
|
+
return out;
|
|
68
|
+
}
|
|
69
|
+
function fencesKey(lockedFences) {
|
|
70
|
+
return JSON.stringify(Object.entries(lockedFences ?? {}).sort(([left], [right]) => left.localeCompare(right)));
|
|
71
|
+
}
|
|
72
|
+
function originalEntry(original, options) {
|
|
73
|
+
const nextFencesKey = fencesKey(options.lockedFences);
|
|
74
|
+
if (cachedOriginal?.original === original && cachedOriginal.fencesKey === nextFencesKey && cachedOriginal.bodyTransform === options.bodyTransform) return cachedOriginal;
|
|
75
|
+
const split = options.bodyTransform?.split(original);
|
|
76
|
+
const editableBlocks = blocks(split?.editable ?? original, options.lockedFences);
|
|
77
|
+
const nodes = editableBlocks.flatMap((block) => block.nodes);
|
|
78
|
+
const keys = editableBlocks.flatMap((block) => block.keys);
|
|
79
|
+
const indicesByFirstKey = /* @__PURE__ */ new Map();
|
|
80
|
+
for (const [index, block] of editableBlocks.entries()) {
|
|
81
|
+
const first = block.keys[0];
|
|
82
|
+
if (first === void 0) continue;
|
|
83
|
+
const indices = indicesByFirstKey.get(first) ?? [];
|
|
84
|
+
indices.push(index);
|
|
85
|
+
indicesByFirstKey.set(first, indices);
|
|
86
|
+
}
|
|
87
|
+
cachedOriginal = {
|
|
88
|
+
original,
|
|
89
|
+
fencesKey: nextFencesKey,
|
|
90
|
+
bodyTransform: options.bodyTransform,
|
|
91
|
+
split,
|
|
92
|
+
blocks: editableBlocks,
|
|
93
|
+
nodes,
|
|
94
|
+
keys,
|
|
95
|
+
indicesByFirstKey
|
|
96
|
+
};
|
|
97
|
+
return cachedOriginal;
|
|
98
|
+
}
|
|
99
|
+
function firstAtOrAfter(indices, minimum) {
|
|
100
|
+
let low = 0;
|
|
101
|
+
let high = indices.length;
|
|
102
|
+
while (low < high) {
|
|
103
|
+
const middle = low + high >>> 1;
|
|
104
|
+
if ((indices[middle] ?? Number.POSITIVE_INFINITY) < minimum) low = middle + 1;
|
|
105
|
+
else high = middle;
|
|
106
|
+
}
|
|
107
|
+
return indices[low] ?? -1;
|
|
108
|
+
}
|
|
109
|
+
function serializeNodes(nodes) {
|
|
110
|
+
return restoreAmpersands(richMarkdownManager().serialize({
|
|
111
|
+
type: "doc",
|
|
112
|
+
content: nodes.map(asJson)
|
|
113
|
+
}).trim());
|
|
114
|
+
}
|
|
115
|
+
function matchingBlockIndex(entry, keys, keyIndex, blockIndex) {
|
|
116
|
+
const currentKey = keys[keyIndex];
|
|
117
|
+
if (currentKey === void 0) return -1;
|
|
118
|
+
const candidates = entry.indicesByFirstKey.get(currentKey) ?? [];
|
|
119
|
+
let hit = firstAtOrAfter(candidates, blockIndex);
|
|
120
|
+
let block = entry.blocks[hit];
|
|
121
|
+
while (hit >= 0 && !block?.keys.every((key, offset) => keys[keyIndex + offset] === key)) {
|
|
122
|
+
hit = firstAtOrAfter(candidates, hit + 1);
|
|
123
|
+
block = entry.blocks[hit];
|
|
124
|
+
}
|
|
125
|
+
return hit;
|
|
126
|
+
}
|
|
127
|
+
function stitch(entry, edited, keys) {
|
|
128
|
+
const bs = entry.blocks;
|
|
129
|
+
let out = "";
|
|
130
|
+
let last = -1;
|
|
131
|
+
let blockIndex = 0;
|
|
132
|
+
let keyIndex = 0;
|
|
133
|
+
const pending = [];
|
|
134
|
+
const emitNew = () => {
|
|
135
|
+
if (!pending.length) return;
|
|
136
|
+
out += (out ? "\n\n" : bs[blockIndex]?.sep ?? "") + serializeNodes(pending.splice(0));
|
|
137
|
+
last = -1;
|
|
138
|
+
};
|
|
139
|
+
const emitOld = (hit) => {
|
|
140
|
+
const block = bs[hit];
|
|
141
|
+
if (!block) throw new Error(`Cannot stitch missing rich Markdown block ${hit}.`);
|
|
142
|
+
out += out ? last === hit - 1 ? block.sep : "\n\n" : block.sep;
|
|
143
|
+
out += block.raw;
|
|
144
|
+
last = hit;
|
|
145
|
+
};
|
|
146
|
+
const emitReplacement = (end) => {
|
|
147
|
+
let firstEditable = blockIndex;
|
|
148
|
+
while (firstEditable < end && bs[firstEditable]?.nodes.length === 0) {
|
|
149
|
+
emitOld(firstEditable);
|
|
150
|
+
firstEditable++;
|
|
151
|
+
}
|
|
152
|
+
emitNew();
|
|
153
|
+
for (let index = firstEditable; index < end; index++) if (bs[index]?.nodes.length === 0) emitOld(index);
|
|
154
|
+
};
|
|
155
|
+
while (keyIndex < keys.length) {
|
|
156
|
+
const hit = matchingBlockIndex(entry, keys, keyIndex, blockIndex);
|
|
157
|
+
if (hit < 0) {
|
|
158
|
+
const node = edited[keyIndex++];
|
|
159
|
+
if (node) pending.push(node);
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
emitReplacement(hit);
|
|
163
|
+
emitOld(hit);
|
|
164
|
+
keyIndex += bs[hit]?.nodes.length ?? 0;
|
|
165
|
+
blockIndex = hit + 1;
|
|
166
|
+
}
|
|
167
|
+
emitReplacement(bs.length);
|
|
168
|
+
return `${out.replace(/\n+$/, "")}\n`;
|
|
169
|
+
}
|
|
170
|
+
function parseRichDocument(markdown, options = {}) {
|
|
171
|
+
return dropTrailingEmptyParagraph(originalEntry(markdown, options).nodes);
|
|
172
|
+
}
|
|
173
|
+
function saveRichDocument(original, edited, options = {}) {
|
|
174
|
+
const prepared = dropTrailingEmptyParagraph(edited);
|
|
175
|
+
const entry = originalEntry(original, options);
|
|
176
|
+
const keys = prepared.map(nodeKey);
|
|
177
|
+
if (entry.keys.length === keys.length && entry.keys.every((key, index) => key === keys[index])) return original;
|
|
178
|
+
const saved = stitch(entry, prepared, keys);
|
|
179
|
+
return entry.split && options.bodyTransform ? options.bodyTransform.join(saved, entry.split.retained) : saved;
|
|
180
|
+
}
|
|
181
|
+
//#endregion
|
|
182
|
+
export { parseRichDocument, saveRichDocument };
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { MARKDOWN_EDITOR_KEYS } from "./markdown-editor-policy.js";
|
|
2
|
+
import { defaultKeymap, history, historyKeymap, redoDepth, undoDepth } from "@codemirror/commands";
|
|
3
|
+
import { Decoration, EditorView, keymap } from "@codemirror/view";
|
|
4
|
+
import { markdown } from "@codemirror/lang-markdown";
|
|
5
|
+
import { HighlightStyle, LanguageDescription, syntaxHighlighting } from "@codemirror/language";
|
|
6
|
+
import { languages } from "@codemirror/language-data";
|
|
7
|
+
import { EditorState, Prec } from "@codemirror/state";
|
|
8
|
+
import { Tag, styleTags, tags } from "@lezer/highlight";
|
|
9
|
+
import { GFM } from "@lezer/markdown";
|
|
10
|
+
//#region src/markdown-source.ts
|
|
11
|
+
const mermaidLanguage = LanguageDescription.of({
|
|
12
|
+
name: "Mermaid",
|
|
13
|
+
alias: ["mermaid"],
|
|
14
|
+
load: () => import("codemirror-lang-mermaid").then((module) => module.mermaid())
|
|
15
|
+
});
|
|
16
|
+
function codeLanguage(info) {
|
|
17
|
+
if (info.toLowerCase() === "mermaid") return mermaidLanguage;
|
|
18
|
+
return LanguageDescription.matchLanguageName(languages, info, true);
|
|
19
|
+
}
|
|
20
|
+
const inlineCode = Tag.define(tags.monospace);
|
|
21
|
+
const autoDirectionLine = Decoration.line({ attributes: { dir: "auto" } });
|
|
22
|
+
const lineDirections = EditorView.decorations.of((view) => {
|
|
23
|
+
const ranges = [];
|
|
24
|
+
let previous = -1;
|
|
25
|
+
for (const visible of view.visibleRanges) for (let position = visible.from; position <= visible.to;) {
|
|
26
|
+
const line = view.state.doc.lineAt(position);
|
|
27
|
+
if (line.from !== previous) ranges.push(autoDirectionLine.range(line.from));
|
|
28
|
+
previous = line.from;
|
|
29
|
+
position = line.to + 1;
|
|
30
|
+
}
|
|
31
|
+
return Decoration.set(ranges);
|
|
32
|
+
});
|
|
33
|
+
const highlight = HighlightStyle.define([
|
|
34
|
+
{
|
|
35
|
+
tag: tags.comment,
|
|
36
|
+
color: "var(--segnavia-markdown-comment,inherit)"
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
tag: [
|
|
40
|
+
tags.keyword,
|
|
41
|
+
tags.processingInstruction,
|
|
42
|
+
tags.meta
|
|
43
|
+
],
|
|
44
|
+
color: "var(--segnavia-markdown-keyword,inherit)"
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
tag: [tags.string, tags.regexp],
|
|
48
|
+
color: "var(--segnavia-markdown-string,inherit)"
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
tag: [
|
|
52
|
+
tags.number,
|
|
53
|
+
tags.bool,
|
|
54
|
+
tags.atom
|
|
55
|
+
],
|
|
56
|
+
color: "var(--segnavia-markdown-literal,inherit)"
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
tag: [tags.heading, tags.strong],
|
|
60
|
+
color: "var(--segnavia-markdown-heading,inherit)"
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
tag: [tags.link, tags.url],
|
|
64
|
+
color: "var(--segnavia-markdown-link,inherit)"
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
tag: [inlineCode, tags.monospace],
|
|
68
|
+
color: "var(--segnavia-markdown-code,inherit)"
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
tag: [
|
|
72
|
+
tags.punctuation,
|
|
73
|
+
tags.bracket,
|
|
74
|
+
tags.labelName,
|
|
75
|
+
tags.contentSeparator
|
|
76
|
+
],
|
|
77
|
+
color: "var(--segnavia-markdown-mark,inherit)"
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
tag: tags.invalid,
|
|
81
|
+
color: "var(--segnavia-markdown-invalid,inherit)"
|
|
82
|
+
}
|
|
83
|
+
]);
|
|
84
|
+
function markdownSourceState(doc, callbacks) {
|
|
85
|
+
const actions = {
|
|
86
|
+
save: callbacks.save,
|
|
87
|
+
"toggle-mode": callbacks.toggleMode
|
|
88
|
+
};
|
|
89
|
+
return EditorState.create({
|
|
90
|
+
doc,
|
|
91
|
+
extensions: [
|
|
92
|
+
history(),
|
|
93
|
+
Prec.highest(keymap.of(MARKDOWN_EDITOR_KEYS.map((binding) => ({
|
|
94
|
+
key: binding.key,
|
|
95
|
+
preventDefault: true,
|
|
96
|
+
run: () => {
|
|
97
|
+
actions[binding.action]();
|
|
98
|
+
return true;
|
|
99
|
+
}
|
|
100
|
+
})))),
|
|
101
|
+
keymap.of([...defaultKeymap, ...historyKeymap]),
|
|
102
|
+
EditorView.lineWrapping,
|
|
103
|
+
lineDirections,
|
|
104
|
+
EditorView.perLineTextDirection.of(true),
|
|
105
|
+
markdown({
|
|
106
|
+
extensions: [GFM, { props: [styleTags({ InlineCode: inlineCode })] }],
|
|
107
|
+
codeLanguages: codeLanguage
|
|
108
|
+
}),
|
|
109
|
+
syntaxHighlighting(highlight),
|
|
110
|
+
EditorView.updateListener.of((update) => {
|
|
111
|
+
if (update.docChanged) callbacks.change(update.state.doc.toString());
|
|
112
|
+
if (update.transactions.length) callbacks.state(undoDepth(update.state) > 0, redoDepth(update.state) > 0);
|
|
113
|
+
}),
|
|
114
|
+
EditorView.contentAttributes.of({ "aria-label": "Markdown body" })
|
|
115
|
+
]
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
//#endregion
|
|
119
|
+
export { markdownSourceState };
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
//#region src/markdown-title.d.ts
|
|
2
|
+
export type LeadingHeadingSplit = Readonly<{
|
|
3
|
+
title: string | null;
|
|
4
|
+
editable: string;
|
|
5
|
+
retained: string;
|
|
6
|
+
}>;
|
|
7
|
+
/** Splits the first nonblank level-one ATX heading from the editable body. */
|
|
8
|
+
export declare function splitLeadingHeading(markdown: string): LeadingHeadingSplit;
|
|
9
|
+
/** Joins an editable body to the exact leading-heading source retained by the split. */
|
|
10
|
+
export declare function joinLeadingHeading(editable: string, retained: string): string;
|
|
11
|
+
/** A title field is always one line, including pasted text. */
|
|
12
|
+
export declare function normalizeMarkdownTitle(value: string): string;
|
|
13
|
+
//#endregion
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
//#region src/markdown-title.ts
|
|
2
|
+
const LEADING_HEADING = /^#(?!#)[ \t]+(.*?)(?:[ \t]+#+)?[ \t]*$/;
|
|
3
|
+
/** Splits the first nonblank level-one ATX heading from the editable body. */
|
|
4
|
+
function splitLeadingHeading(markdown) {
|
|
5
|
+
const lines = markdown.split("\n");
|
|
6
|
+
let index = 0;
|
|
7
|
+
while (index < lines.length && lines[index]?.trim() === "") index++;
|
|
8
|
+
const line = lines[index];
|
|
9
|
+
const title = (line === void 0 ? null : LEADING_HEADING.exec(line))?.[1] || null;
|
|
10
|
+
if (!title) return {
|
|
11
|
+
title: null,
|
|
12
|
+
editable: markdown,
|
|
13
|
+
retained: ""
|
|
14
|
+
};
|
|
15
|
+
const heading = lines.slice(0, index + 1).join("\n");
|
|
16
|
+
const retained = markdown[heading.length] === "\n" ? `${heading}\n` : heading;
|
|
17
|
+
return {
|
|
18
|
+
title,
|
|
19
|
+
editable: markdown.slice(retained.length),
|
|
20
|
+
retained
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
/** Joins an editable body to the exact leading-heading source retained by the split. */
|
|
24
|
+
function joinLeadingHeading(editable, retained) {
|
|
25
|
+
return `${retained}${retained && editable && !retained.endsWith("\n") ? "\n" : ""}${editable}`;
|
|
26
|
+
}
|
|
27
|
+
/** Replaces the retained heading while preserving blank lines before it and its line break. */
|
|
28
|
+
function retainedLeadingHeading(title, previous) {
|
|
29
|
+
if (title === null || title === "") return "";
|
|
30
|
+
if (!previous.retained) {
|
|
31
|
+
if (!previous.editable) return `# ${title}`;
|
|
32
|
+
return `# ${title}${previous.editable.startsWith("\n") ? "\n" : "\n\n"}`;
|
|
33
|
+
}
|
|
34
|
+
const retainedWithoutBreak = previous.retained.endsWith("\n") ? previous.retained.slice(0, -1) : previous.retained;
|
|
35
|
+
const headingStart = retainedWithoutBreak.lastIndexOf("\n") + 1;
|
|
36
|
+
return `${retainedWithoutBreak.slice(0, headingStart)}# ${title}${previous.retained.endsWith("\n") ? "\n" : ""}`;
|
|
37
|
+
}
|
|
38
|
+
/** A title field is always one line, including pasted text. */
|
|
39
|
+
function normalizeMarkdownTitle(value) {
|
|
40
|
+
return value.replace(/[\r\n]+/g, " ");
|
|
41
|
+
}
|
|
42
|
+
//#endregion
|
|
43
|
+
export { joinLeadingHeading, normalizeMarkdownTitle, retainedLeadingHeading, splitLeadingHeading };
|
package/package.json
CHANGED
|
@@ -1,6 +1,74 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@segnavia/markdown-editor",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.42.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/modstudio/segnavia.git",
|
|
9
|
+
"directory": "packages/markdown-editor"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"dist"
|
|
13
|
+
],
|
|
14
|
+
"sideEffects": [
|
|
15
|
+
"./src/markdown-editor.ts",
|
|
16
|
+
"./dist/markdown-editor.js",
|
|
17
|
+
"./src/markdown-rich-editor.ts",
|
|
18
|
+
"./dist/markdown-rich-editor.js"
|
|
19
|
+
],
|
|
20
|
+
"engines": {
|
|
21
|
+
"node": ">=22"
|
|
22
|
+
},
|
|
23
|
+
"publishConfig": {
|
|
24
|
+
"access": "public"
|
|
25
|
+
},
|
|
26
|
+
"exports": {
|
|
27
|
+
".": {
|
|
28
|
+
"@segnavia/source": "./src/markdown-editor.ts",
|
|
29
|
+
"types": "./dist/markdown-editor.d.ts",
|
|
30
|
+
"default": "./dist/markdown-editor.js"
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"@segnavia/format": "0.42.0",
|
|
35
|
+
"@segnavia/frame": "0.42.0",
|
|
36
|
+
"@segnavia/html": "0.42.0"
|
|
37
|
+
},
|
|
38
|
+
"peerDependencies": {
|
|
39
|
+
"@codemirror/commands": "^6.11.1",
|
|
40
|
+
"@codemirror/lang-markdown": "^6.5.2",
|
|
41
|
+
"@codemirror/language": "^6.13.1",
|
|
42
|
+
"@codemirror/language-data": "^6.5.2",
|
|
43
|
+
"@codemirror/state": "^6.7.6",
|
|
44
|
+
"@codemirror/view": "^6.43.15",
|
|
45
|
+
"@lezer/highlight": "^1.2.5",
|
|
46
|
+
"@lezer/markdown": "^1.8.1",
|
|
47
|
+
"@tiptap/core": "^3.32.0",
|
|
48
|
+
"@tiptap/extension-image": "^3.32.0",
|
|
49
|
+
"@tiptap/extension-list": "^3.32.0",
|
|
50
|
+
"@tiptap/extension-table": "^3.32.0",
|
|
51
|
+
"@tiptap/markdown": "^3.32.0",
|
|
52
|
+
"@tiptap/pm": "^3.32.0",
|
|
53
|
+
"@tiptap/starter-kit": "^3.32.0",
|
|
54
|
+
"codemirror-lang-mermaid": "^0.5.0"
|
|
55
|
+
},
|
|
56
|
+
"devDependencies": {
|
|
57
|
+
"@codemirror/commands": "6.11.1",
|
|
58
|
+
"@codemirror/lang-markdown": "6.5.2",
|
|
59
|
+
"@codemirror/language": "6.13.1",
|
|
60
|
+
"@codemirror/language-data": "6.5.2",
|
|
61
|
+
"@codemirror/state": "6.7.6",
|
|
62
|
+
"@codemirror/view": "6.43.15",
|
|
63
|
+
"@lezer/highlight": "1.2.5",
|
|
64
|
+
"@lezer/markdown": "1.8.1",
|
|
65
|
+
"@tiptap/core": "3.32.0",
|
|
66
|
+
"@tiptap/extension-image": "3.32.0",
|
|
67
|
+
"@tiptap/extension-list": "3.32.0",
|
|
68
|
+
"@tiptap/extension-table": "3.32.0",
|
|
69
|
+
"@tiptap/markdown": "3.32.0",
|
|
70
|
+
"@tiptap/pm": "3.32.0",
|
|
71
|
+
"@tiptap/starter-kit": "3.32.0",
|
|
72
|
+
"codemirror-lang-mermaid": "0.5.0"
|
|
73
|
+
}
|
|
74
|
+
}
|
package/README.md
DELETED