@segnavia/markdown-editor 0.44.0 → 0.45.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/dist/editor-css.js +4 -2
- package/dist/markdown-editor-model.js +12 -7
- package/dist/markdown-rich-editor.js +19 -1
- package/dist/markdown-rich-extensions.js +168 -6
- package/dist/markdown-rich-locked.js +28 -2
- package/dist/markdown-rich-raw-inline.js +116 -0
- package/dist/markdown-rich-save.js +27 -7
- package/package.json +8 -6
package/dist/editor-css.js
CHANGED
|
@@ -9,10 +9,12 @@ const EDITOR_CSS = `
|
|
|
9
9
|
:where(:host(segnavia-markdown-editor:not([unstyled])) [part~="rich"] .ProseMirror){min-block-size:100%;outline:0}
|
|
10
10
|
:where(:host(segnavia-markdown-editor:not([unstyled])) [part~="rich"] table){border-collapse:collapse;inline-size:max-content;min-inline-size:100%}
|
|
11
11
|
:where(:host(segnavia-markdown-editor:not([unstyled])) [part~="rich"] th),:where(:host(segnavia-markdown-editor:not([unstyled])) [part~="rich"] td){min-inline-size:1em}
|
|
12
|
-
:where(:host(segnavia-markdown-editor:not([unstyled])) [part~="rich"] [data-
|
|
13
|
-
:where(:host(segnavia-markdown-editor:not([unstyled])) [part~="rich"] [data-
|
|
12
|
+
:where(:host(segnavia-markdown-editor:not([unstyled])) [part~="rich"] ul:has(> li[data-checked])){list-style:none;padding-inline-start:0}
|
|
13
|
+
:where(:host(segnavia-markdown-editor:not([unstyled])) [part~="rich"] li[data-checked]){display:grid;grid-template-columns:auto minmax(0,1fr);gap:var(--segnavia-markdown-task-gap,.5em);align-items:baseline}
|
|
14
|
+
:where(:host(segnavia-markdown-editor:not([unstyled])) [part~="rich"] li[data-checked] > div > p){margin:0}
|
|
14
15
|
:where(:host(segnavia-markdown-editor:not([unstyled])) [part~="locked-block"]){display:block;position:relative;min-block-size:1em}
|
|
15
16
|
:where(:host(segnavia-markdown-editor:not([unstyled])) [part~="locked-block"] segnavia-page){display:block}
|
|
17
|
+
:where(:host(segnavia-markdown-editor:not([unstyled])) [part~="rich"] [part~="raw-inline"]){unicode-bidi:isolate;color:var(--segnavia-markdown-raw-inline-color,inherit);background:var(--segnavia-markdown-raw-inline-background,transparent);border:var(--segnavia-markdown-raw-inline-border,none);border-radius:var(--segnavia-markdown-raw-inline-radius,0);padding-inline:var(--segnavia-markdown-raw-inline-padding,.15em)}
|
|
16
18
|
`;
|
|
17
19
|
//#endregion
|
|
18
20
|
export { EDITOR_CSS };
|
|
@@ -9,17 +9,21 @@ function markdownEditorModel() {
|
|
|
9
9
|
retained: ""
|
|
10
10
|
};
|
|
11
11
|
}
|
|
12
|
+
function normalizeMarkdownLineEndings(markdown) {
|
|
13
|
+
return markdown.replace(/\r\n?/g, "\n");
|
|
14
|
+
}
|
|
12
15
|
function setModelMarkdown(model, markdown) {
|
|
13
|
-
|
|
16
|
+
const normalized = normalizeMarkdownLineEndings(markdown);
|
|
17
|
+
if (normalized === model.markdown) return model;
|
|
14
18
|
if (model.titleSource === "separate") return {
|
|
15
19
|
...model,
|
|
16
|
-
markdown,
|
|
17
|
-
editable:
|
|
20
|
+
markdown: normalized,
|
|
21
|
+
editable: normalized
|
|
18
22
|
};
|
|
19
|
-
const split = splitLeadingHeading(
|
|
23
|
+
const split = splitLeadingHeading(normalized);
|
|
20
24
|
return {
|
|
21
25
|
...model,
|
|
22
|
-
markdown,
|
|
26
|
+
markdown: normalized,
|
|
23
27
|
title: split.title ?? "",
|
|
24
28
|
editable: split.editable,
|
|
25
29
|
retained: split.retained
|
|
@@ -63,7 +67,8 @@ function setModelTitle(model, value) {
|
|
|
63
67
|
retained
|
|
64
68
|
};
|
|
65
69
|
}
|
|
66
|
-
function setModelEditable(model,
|
|
70
|
+
function setModelEditable(model, edited) {
|
|
71
|
+
const editable = normalizeMarkdownLineEndings(edited);
|
|
67
72
|
const markdown = model.titleSource === "leading-heading" ? joinLeadingHeading(editable, model.retained) : editable;
|
|
68
73
|
if (markdown === model.markdown && editable === model.editable) return model;
|
|
69
74
|
return {
|
|
@@ -73,4 +78,4 @@ function setModelEditable(model, editable) {
|
|
|
73
78
|
};
|
|
74
79
|
}
|
|
75
80
|
//#endregion
|
|
76
|
-
export { markdownEditorModel, setModelEditable, setModelMarkdown, setModelTitle, setModelTitleSource };
|
|
81
|
+
export { markdownEditorModel, normalizeMarkdownLineEndings, setModelEditable, setModelMarkdown, setModelTitle, setModelTitleSource };
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { LockedBlock } from "./markdown-rich-locked.js";
|
|
2
2
|
import { richEditorState, richKeyboard } from "./markdown-rich-commands.js";
|
|
3
|
+
import { RawInline } from "./markdown-rich-raw-inline.js";
|
|
3
4
|
import { richExtensions } from "./markdown-rich-extensions.js";
|
|
4
5
|
import { safeMarkdownUrl } from "@segnavia/html";
|
|
5
6
|
import { HEADLESS_ELEMENTS, SegnaviaPage, createPageElement } from "@segnavia/frame";
|
|
@@ -37,8 +38,25 @@ function lockedView(options) {
|
|
|
37
38
|
};
|
|
38
39
|
} });
|
|
39
40
|
}
|
|
41
|
+
const rawInlineView = RawInline.extend({ addNodeView() {
|
|
42
|
+
return ({ node }) => {
|
|
43
|
+
const dom = document.createElement("span");
|
|
44
|
+
dom.part.add("raw-inline");
|
|
45
|
+
dom.dataset.rawInline = "";
|
|
46
|
+
dom.dataset.rawSource = String(node.attrs.source ?? "");
|
|
47
|
+
dom.dataset.rawDisplay = String(node.attrs.display ?? "");
|
|
48
|
+
dom.dir = "auto";
|
|
49
|
+
dom.contentEditable = "false";
|
|
50
|
+
dom.title = String(node.attrs.source ?? "");
|
|
51
|
+
dom.textContent = String(node.attrs.display ?? "");
|
|
52
|
+
return { dom };
|
|
53
|
+
};
|
|
54
|
+
} });
|
|
40
55
|
function extensions(options, keyboard) {
|
|
41
|
-
return [...richExtensions().map((extension) =>
|
|
56
|
+
return [...richExtensions().map((extension) => {
|
|
57
|
+
if (extension.name === "lockedBlock") return lockedView(options);
|
|
58
|
+
return extension.name === "rawInline" ? rawInlineView : extension;
|
|
59
|
+
}), richKeyboard(keyboard)];
|
|
42
60
|
}
|
|
43
61
|
function pastedHtml(html, linkOrigins) {
|
|
44
62
|
const parsed = new DOMParser().parseFromString(html, "text/html");
|
|
@@ -1,11 +1,159 @@
|
|
|
1
1
|
import { LockedBlock } from "./markdown-rich-locked.js";
|
|
2
|
+
import { rawInline } from "./markdown-rich-raw-inline.js";
|
|
2
3
|
import { Extension, getSchema } from "@tiptap/core";
|
|
3
4
|
import Image from "@tiptap/extension-image";
|
|
4
|
-
import { TaskItem, TaskList } from "@tiptap/extension-list";
|
|
5
|
+
import { BulletList, OrderedList, TaskItem, TaskList } from "@tiptap/extension-list";
|
|
5
6
|
import { TableKit } from "@tiptap/extension-table";
|
|
6
7
|
import { MarkdownManager } from "@tiptap/markdown";
|
|
7
8
|
import StarterKit from "@tiptap/starter-kit";
|
|
9
|
+
import { Marked } from "marked";
|
|
8
10
|
//#region src/markdown-rich-extensions.ts
|
|
11
|
+
const listAttributes = { loose: {
|
|
12
|
+
default: false,
|
|
13
|
+
parseHTML: (element) => element.getAttribute("data-loose") === "true",
|
|
14
|
+
renderHTML: (attrs) => attrs.loose ? { "data-loose": "true" } : {}
|
|
15
|
+
} };
|
|
16
|
+
const listSeparator = (node) => node.attrs?.loose ? "\n\n" : "\n";
|
|
17
|
+
const listItemPattern = /^(\s*)([-+*]|\d+[.)])(\s+)(?:\[[ xX]\]\s+)?/;
|
|
18
|
+
const indentWidth = (value) => value.replace(/\t/g, " ").length;
|
|
19
|
+
const lineIndent = (line) => indentWidth(/^\s*/.exec(line)?.[0] ?? "");
|
|
20
|
+
const itemContentIndent = (match) => indentWidth(match[1] ?? "") + (match[2]?.length ?? 0) + indentWidth(match[3] ?? "");
|
|
21
|
+
function updateListContext(match, indent, state) {
|
|
22
|
+
if (match && indent === state.baseIndent) {
|
|
23
|
+
state.contentIndent = itemContentIndent(match);
|
|
24
|
+
state.nestedIndent = void 0;
|
|
25
|
+
} else if (match && indent >= state.contentIndent && state.nestedIndent === void 0) state.nestedIndent = indent;
|
|
26
|
+
}
|
|
27
|
+
function consumeFence(line, indent, state) {
|
|
28
|
+
const match = /^(\s*)(`{3,}|~{3,})/.exec(line);
|
|
29
|
+
if (!match || indent < state.contentIndent || state.nestedIndent !== void 0) return false;
|
|
30
|
+
const marker = match[2]?.[0] ?? "";
|
|
31
|
+
const length = match[2]?.length ?? 0;
|
|
32
|
+
if (!state.fence) state.fence = {
|
|
33
|
+
marker,
|
|
34
|
+
length,
|
|
35
|
+
indent
|
|
36
|
+
};
|
|
37
|
+
else if (marker === state.fence.marker && length >= state.fence.length && indent <= state.fence.indent) state.fence = void 0;
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
function nextNonblank(lines, index) {
|
|
41
|
+
return lines.slice(index + 1).find((line) => !/^\s*$/.test(line));
|
|
42
|
+
}
|
|
43
|
+
function blankLineIsLoose(lines, index, state) {
|
|
44
|
+
const nextLine = nextNonblank(lines, index);
|
|
45
|
+
if (nextLine === void 0) return false;
|
|
46
|
+
const nextItem = listItemPattern.exec(nextLine);
|
|
47
|
+
const nextIndent = lineIndent(nextLine);
|
|
48
|
+
if (nextItem && nextIndent === state.baseIndent) return true;
|
|
49
|
+
if (state.nestedIndent !== void 0) {
|
|
50
|
+
if (nextIndent > state.nestedIndent || nextItem && nextIndent === state.nestedIndent) return false;
|
|
51
|
+
state.nestedIndent = void 0;
|
|
52
|
+
}
|
|
53
|
+
return nextIndent >= state.contentIndent;
|
|
54
|
+
}
|
|
55
|
+
function listLineIsLoose(lines, index, state) {
|
|
56
|
+
const line = lines[index] ?? "";
|
|
57
|
+
const match = listItemPattern.exec(line);
|
|
58
|
+
const indent = lineIndent(line);
|
|
59
|
+
updateListContext(match, indent, state);
|
|
60
|
+
if (consumeFence(line, indent, state)) return false;
|
|
61
|
+
return /^\s*$/.test(line) && state.fence === void 0 && blankLineIsLoose(lines, index, state);
|
|
62
|
+
}
|
|
63
|
+
function listIsLoose(raw) {
|
|
64
|
+
const lines = raw.split("\n");
|
|
65
|
+
const firstMatch = listItemPattern.exec(lines.find((line) => listItemPattern.test(line)) ?? "");
|
|
66
|
+
if (!firstMatch) return false;
|
|
67
|
+
const state = {
|
|
68
|
+
baseIndent: indentWidth(firstMatch[1] ?? ""),
|
|
69
|
+
contentIndent: itemContentIndent(firstMatch)
|
|
70
|
+
};
|
|
71
|
+
for (let index = 0; index < lines.length; index++) if (listLineIsLoose(lines, index, state)) return true;
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
const LooseBulletList = BulletList.extend({
|
|
75
|
+
addAttributes() {
|
|
76
|
+
return {
|
|
77
|
+
...this.parent?.(),
|
|
78
|
+
...listAttributes
|
|
79
|
+
};
|
|
80
|
+
},
|
|
81
|
+
parseMarkdown(token, helpers) {
|
|
82
|
+
if (token.type !== "list" || token.ordered) return [];
|
|
83
|
+
return {
|
|
84
|
+
type: "bulletList",
|
|
85
|
+
attrs: { loose: listIsLoose(token.raw ?? "") },
|
|
86
|
+
content: helpers.parseChildren(token.items ?? [])
|
|
87
|
+
};
|
|
88
|
+
},
|
|
89
|
+
renderMarkdown(node, helpers) {
|
|
90
|
+
return node.content ? helpers.renderChildren(node.content, listSeparator(node)) : "";
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
const LooseOrderedList = OrderedList.extend({
|
|
94
|
+
addAttributes() {
|
|
95
|
+
return {
|
|
96
|
+
...this.parent?.(),
|
|
97
|
+
...listAttributes
|
|
98
|
+
};
|
|
99
|
+
},
|
|
100
|
+
parseMarkdown(token, helpers) {
|
|
101
|
+
if (token.type !== "list" || !token.ordered) return [];
|
|
102
|
+
return {
|
|
103
|
+
type: "orderedList",
|
|
104
|
+
attrs: {
|
|
105
|
+
start: token.start || 1,
|
|
106
|
+
type: token.typeMarker ?? null,
|
|
107
|
+
loose: listIsLoose(token.raw ?? "")
|
|
108
|
+
},
|
|
109
|
+
content: helpers.parseChildren(token.items ?? [])
|
|
110
|
+
};
|
|
111
|
+
},
|
|
112
|
+
renderMarkdown(node, helpers) {
|
|
113
|
+
return node.content ? helpers.renderChildren(node.content, listSeparator(node)) : "";
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
const LooseTaskList = TaskList.extend({
|
|
117
|
+
addAttributes() {
|
|
118
|
+
return {
|
|
119
|
+
...this.parent?.(),
|
|
120
|
+
...listAttributes
|
|
121
|
+
};
|
|
122
|
+
},
|
|
123
|
+
parseMarkdown(token, helpers) {
|
|
124
|
+
return helpers.createNode("taskList", { loose: listIsLoose(token.raw ?? "") }, helpers.parseChildren(token.items ?? []));
|
|
125
|
+
},
|
|
126
|
+
renderMarkdown(node, helpers) {
|
|
127
|
+
return node.content ? helpers.renderChildren(node.content, listSeparator(node)) : "";
|
|
128
|
+
},
|
|
129
|
+
markdownTokenizer: {
|
|
130
|
+
name: "taskList",
|
|
131
|
+
level: "block",
|
|
132
|
+
start: (src) => src.search(/^\s*[-+*]\s+\[[ xX]\]\s+/m),
|
|
133
|
+
tokenize(src, _tokens, lexer) {
|
|
134
|
+
const token = new Marked().lexer(src)[0];
|
|
135
|
+
if (token?.type !== "list" || token.ordered || !token.items?.every((item) => item.task)) return void 0;
|
|
136
|
+
const items = token.items.map((item) => {
|
|
137
|
+
const lines = item.text.split("\n");
|
|
138
|
+
const mainContent = lines.shift() ?? "";
|
|
139
|
+
const nested = lines.join("\n").trim();
|
|
140
|
+
return {
|
|
141
|
+
type: "taskItem",
|
|
142
|
+
raw: item.raw,
|
|
143
|
+
checked: item.checked ?? false,
|
|
144
|
+
text: mainContent,
|
|
145
|
+
tokens: lexer.inlineTokens(mainContent),
|
|
146
|
+
nestedTokens: nested ? lexer.blockTokens(`${nested}\n`) : []
|
|
147
|
+
};
|
|
148
|
+
});
|
|
149
|
+
return {
|
|
150
|
+
type: "taskList",
|
|
151
|
+
raw: token.raw,
|
|
152
|
+
items
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
});
|
|
9
157
|
const directionalBlocks = Extension.create({
|
|
10
158
|
name: "directionalBlocks",
|
|
11
159
|
addGlobalAttributes() {
|
|
@@ -30,32 +178,46 @@ const directionalBlocks = Extension.create({
|
|
|
30
178
|
}];
|
|
31
179
|
}
|
|
32
180
|
});
|
|
33
|
-
function richExtensions() {
|
|
181
|
+
function richExtensions(definitionLabels = /* @__PURE__ */ new Set()) {
|
|
34
182
|
return [
|
|
35
183
|
StarterKit.configure({
|
|
36
184
|
trailingNode: false,
|
|
37
|
-
link: { openOnClick: false }
|
|
185
|
+
link: { openOnClick: false },
|
|
186
|
+
bulletList: false,
|
|
187
|
+
orderedList: false
|
|
38
188
|
}),
|
|
189
|
+
LooseBulletList,
|
|
190
|
+
LooseOrderedList,
|
|
39
191
|
TableKit,
|
|
40
|
-
|
|
192
|
+
LooseTaskList,
|
|
41
193
|
TaskItem.configure({ nested: true }),
|
|
42
194
|
Image.configure({ inline: true }),
|
|
43
195
|
directionalBlocks,
|
|
44
|
-
LockedBlock
|
|
196
|
+
LockedBlock,
|
|
197
|
+
rawInline(definitionLabels)
|
|
45
198
|
];
|
|
46
199
|
}
|
|
47
200
|
let manager;
|
|
201
|
+
const isolatedMarked = () => new Marked();
|
|
48
202
|
function richMarkdownManager() {
|
|
49
203
|
manager ??= new MarkdownManager({
|
|
204
|
+
marked: isolatedMarked(),
|
|
50
205
|
extensions: richExtensions(),
|
|
51
206
|
markedOptions: { gfm: true }
|
|
52
207
|
});
|
|
53
208
|
return manager;
|
|
54
209
|
}
|
|
210
|
+
function scopedRichMarkdownManager(definitionLabels) {
|
|
211
|
+
return new MarkdownManager({
|
|
212
|
+
marked: isolatedMarked(),
|
|
213
|
+
extensions: richExtensions(definitionLabels),
|
|
214
|
+
markedOptions: { gfm: true }
|
|
215
|
+
});
|
|
216
|
+
}
|
|
55
217
|
let schema;
|
|
56
218
|
function richSchema() {
|
|
57
219
|
schema ??= getSchema(richExtensions());
|
|
58
220
|
return schema;
|
|
59
221
|
}
|
|
60
222
|
//#endregion
|
|
61
|
-
export { richExtensions, richMarkdownManager, richSchema };
|
|
223
|
+
export { listIsLoose, richExtensions, richMarkdownManager, richSchema, scopedRichMarkdownManager };
|
|
@@ -2,6 +2,26 @@ import { isDiagramFence, isGithubAlertBlock } from "@segnavia/html";
|
|
|
2
2
|
import { Node } from "@tiptap/core";
|
|
3
3
|
//#region src/markdown-rich-locked.ts
|
|
4
4
|
const LOCKED_BLOCK_NAME = "lockedBlock";
|
|
5
|
+
const FOOTNOTE_OPENING = /^ {0,3}\[\^/m;
|
|
6
|
+
function matchFootnoteDefinition(src) {
|
|
7
|
+
const opening = FOOTNOTE_OPENING.exec(src);
|
|
8
|
+
if (opening?.index !== 0) return void 0;
|
|
9
|
+
const rest = /^[^\]\n]+\]:[^\n]*(?:\n|$)/.exec(src.slice(opening[0].length))?.[0];
|
|
10
|
+
const first = rest === void 0 ? void 0 : opening[0] + rest;
|
|
11
|
+
if (!first) return void 0;
|
|
12
|
+
let end = first.length;
|
|
13
|
+
while (end < src.length) {
|
|
14
|
+
const continuation = /^(?: {4,}|\t)[^\n]*(?:\n|$)/.exec(src.slice(end))?.[0];
|
|
15
|
+
if (continuation) {
|
|
16
|
+
end += continuation.length;
|
|
17
|
+
continue;
|
|
18
|
+
}
|
|
19
|
+
const blanks = /^(?:[ \t]*\n)+/.exec(src.slice(end))?.[0];
|
|
20
|
+
if (!blanks || !/^(?: {4,}|\t)\S/.test(src.slice(end + blanks.length))) break;
|
|
21
|
+
end += blanks.length;
|
|
22
|
+
}
|
|
23
|
+
return src.slice(0, end);
|
|
24
|
+
}
|
|
5
25
|
function lockedFenceLabel(lockedFences, language) {
|
|
6
26
|
return lockedFences[language?.toLowerCase() ?? ""];
|
|
7
27
|
}
|
|
@@ -27,7 +47,7 @@ function isHtmlComment(raw) {
|
|
|
27
47
|
return /^<!--[\s\S]*-->$/.test(raw.replace(/\n+$/, ""));
|
|
28
48
|
}
|
|
29
49
|
function isLockedToken(token, lockedFences = {}) {
|
|
30
|
-
if (token.type === "frontmatter" || token.type === "directive") return true;
|
|
50
|
+
if (token.type === "frontmatter" || token.type === "directive" || token.type === "footnoteDefinition") return true;
|
|
31
51
|
if (token.type === "code" && (isDiagramFence(token.lang) || Boolean(lockedFenceLabel(lockedFences, token.lang)))) return true;
|
|
32
52
|
if (token.type === "blockquote" && isGithubAlertBlock(token.raw)) return true;
|
|
33
53
|
return token.type === "html" && !isHtmlComment(token.raw);
|
|
@@ -75,7 +95,8 @@ const LockedBlock = Node.create({
|
|
|
75
95
|
name: "lockedFence",
|
|
76
96
|
level: "block",
|
|
77
97
|
start(src) {
|
|
78
|
-
|
|
98
|
+
if (src.startsWith("---") || src.startsWith(":::")) return 0;
|
|
99
|
+
return FOOTNOTE_OPENING.exec(src)?.index ?? -1;
|
|
79
100
|
},
|
|
80
101
|
tokenize(src, tokens) {
|
|
81
102
|
if (tokens.length === 0) {
|
|
@@ -85,6 +106,11 @@ const LockedBlock = Node.create({
|
|
|
85
106
|
raw: frontmatter
|
|
86
107
|
};
|
|
87
108
|
}
|
|
109
|
+
const footnote = matchFootnoteDefinition(src);
|
|
110
|
+
if (footnote) return {
|
|
111
|
+
type: "footnoteDefinition",
|
|
112
|
+
raw: footnote
|
|
113
|
+
};
|
|
88
114
|
const directive = matchDirectiveContainer(src);
|
|
89
115
|
return directive ? {
|
|
90
116
|
type: "directive",
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { Node } from "@tiptap/core";
|
|
2
|
+
//#region src/markdown-rich-raw-inline.ts
|
|
3
|
+
const RAW_INLINE_NAME = "rawInline";
|
|
4
|
+
const ASCII_PUNCTUATION = String.raw`[!-/:-@\[-` + "{-~]";
|
|
5
|
+
const escapePattern = new RegExp(`^\\\\${ASCII_PUNCTUATION}`);
|
|
6
|
+
const entityPattern = /^&(?:[A-Za-z][A-Za-z0-9]+|#[0-9]+|#[xX][0-9A-Fa-f]+);/;
|
|
7
|
+
const footnotePattern = /^\[\^[^\]\n]+\]/;
|
|
8
|
+
const inlineHtmlPattern = /^(?:<!--[\s\S]*?-->|<\/?[A-Za-z](?:[^<>"']|"[^"]*"|'[^']*')*?>)/;
|
|
9
|
+
const bracketContent = String.raw`(?:\\.|[^\[\]\\]|\[(?:\\.|[^\[\]\\])*\])*`;
|
|
10
|
+
const referencePattern = new RegExp(`^(!?)\\[(${bracketContent})\\](?:\\[(${bracketContent})\\])?`);
|
|
11
|
+
function normalizeReferenceLabel(label) {
|
|
12
|
+
return label.trim().replace(/\s+/g, " ").toLowerCase();
|
|
13
|
+
}
|
|
14
|
+
function referenceSource(src, labels) {
|
|
15
|
+
const match = referencePattern.exec(src);
|
|
16
|
+
if (!match) return void 0;
|
|
17
|
+
const raw = match[0];
|
|
18
|
+
const firstLabel = match[2] ?? "";
|
|
19
|
+
const secondLabel = match[3];
|
|
20
|
+
if (secondLabel === void 0 && src[raw.length] === "(") return void 0;
|
|
21
|
+
const label = secondLabel === void 0 || secondLabel === "" ? firstLabel : secondLabel;
|
|
22
|
+
return labels.has(normalizeReferenceLabel(label)) ? raw : void 0;
|
|
23
|
+
}
|
|
24
|
+
function rawSource(src, labels) {
|
|
25
|
+
const escaped = escapePattern.exec(src)?.[0];
|
|
26
|
+
if (escaped) return {
|
|
27
|
+
source: escaped,
|
|
28
|
+
display: escaped.slice(1)
|
|
29
|
+
};
|
|
30
|
+
const entity = entityPattern.exec(src)?.[0];
|
|
31
|
+
if (entity) return {
|
|
32
|
+
source: entity,
|
|
33
|
+
display: entity
|
|
34
|
+
};
|
|
35
|
+
const footnote = footnotePattern.exec(src)?.[0];
|
|
36
|
+
if (footnote) return {
|
|
37
|
+
source: footnote,
|
|
38
|
+
display: footnote
|
|
39
|
+
};
|
|
40
|
+
const reference = referenceSource(src, labels);
|
|
41
|
+
if (reference) return {
|
|
42
|
+
source: reference,
|
|
43
|
+
display: reference
|
|
44
|
+
};
|
|
45
|
+
const html = inlineHtmlPattern.exec(src)?.[0];
|
|
46
|
+
return html && (!html.startsWith("<!") ? !/\n[ \t]*\n/.test(html) : true) ? {
|
|
47
|
+
source: html,
|
|
48
|
+
display: html
|
|
49
|
+
} : void 0;
|
|
50
|
+
}
|
|
51
|
+
function rawInline(labels = /* @__PURE__ */ new Set()) {
|
|
52
|
+
return Node.create({
|
|
53
|
+
name: RAW_INLINE_NAME,
|
|
54
|
+
group: "inline",
|
|
55
|
+
inline: true,
|
|
56
|
+
atom: true,
|
|
57
|
+
selectable: true,
|
|
58
|
+
draggable: false,
|
|
59
|
+
addAttributes() {
|
|
60
|
+
return {
|
|
61
|
+
source: {
|
|
62
|
+
default: "",
|
|
63
|
+
parseHTML: (element) => element.getAttribute("data-raw-source") ?? "",
|
|
64
|
+
renderHTML: (attrs) => ({ "data-raw-source": attrs.source })
|
|
65
|
+
},
|
|
66
|
+
display: {
|
|
67
|
+
default: "",
|
|
68
|
+
parseHTML: (element) => element.getAttribute("data-raw-display") ?? "",
|
|
69
|
+
renderHTML: (attrs) => ({ "data-raw-display": attrs.display })
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
},
|
|
73
|
+
parseHTML() {
|
|
74
|
+
return [{ tag: "span[data-raw-inline]" }];
|
|
75
|
+
},
|
|
76
|
+
renderHTML({ HTMLAttributes }) {
|
|
77
|
+
return ["span", {
|
|
78
|
+
...HTMLAttributes,
|
|
79
|
+
"data-raw-inline": "",
|
|
80
|
+
dir: "auto"
|
|
81
|
+
}];
|
|
82
|
+
},
|
|
83
|
+
parseMarkdown(token) {
|
|
84
|
+
return {
|
|
85
|
+
type: RAW_INLINE_NAME,
|
|
86
|
+
attrs: {
|
|
87
|
+
source: token.source,
|
|
88
|
+
display: token.display
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
},
|
|
92
|
+
renderMarkdown(node) {
|
|
93
|
+
return typeof node.attrs?.source === "string" ? node.attrs.source : "";
|
|
94
|
+
},
|
|
95
|
+
markdownTokenizer: {
|
|
96
|
+
name: RAW_INLINE_NAME,
|
|
97
|
+
level: "inline",
|
|
98
|
+
start(src) {
|
|
99
|
+
for (let index = 0; index < src.length; index++) if (rawSource(src.slice(index), labels)) return index;
|
|
100
|
+
return -1;
|
|
101
|
+
},
|
|
102
|
+
tokenize(src) {
|
|
103
|
+
const raw = rawSource(src, labels);
|
|
104
|
+
return raw ? {
|
|
105
|
+
type: RAW_INLINE_NAME,
|
|
106
|
+
raw: raw.source,
|
|
107
|
+
source: raw.source,
|
|
108
|
+
display: raw.display
|
|
109
|
+
} : void 0;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
const RawInline = rawInline();
|
|
115
|
+
//#endregion
|
|
116
|
+
export { RAW_INLINE_NAME, RawInline, normalizeReferenceLabel, rawInline };
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { isLockedToken, isSilentToken, lockedFenceLabel, lockedNode } from "./markdown-rich-locked.js";
|
|
2
|
-
import {
|
|
2
|
+
import { normalizeReferenceLabel } from "./markdown-rich-raw-inline.js";
|
|
3
|
+
import { richMarkdownManager, richSchema, scopedRichMarkdownManager } from "./markdown-rich-extensions.js";
|
|
3
4
|
import { Node } from "@tiptap/pm/model";
|
|
4
5
|
//#region src/markdown-rich-save.ts
|
|
5
6
|
let cachedOriginal;
|
|
@@ -39,15 +40,22 @@ const dropTrailingEmptyParagraph = (nodes) => {
|
|
|
39
40
|
return last && isEmptyParagraph(last) ? nodes.slice(0, -1) : nodes;
|
|
40
41
|
};
|
|
41
42
|
const restoreAmpersands = (markdown) => markdown.replace(/&(?![a-zA-Z][a-zA-Z0-9]*;)/g, "&");
|
|
42
|
-
function parseBlock(token, raw, lockedFences) {
|
|
43
|
+
function parseBlock(manager, token, raw, lockedFences) {
|
|
43
44
|
if (isSilentToken(token)) return [];
|
|
44
45
|
if (isLockedToken(token, lockedFences)) return [lockedNode(raw, lockedFenceLabel(lockedFences, token.lang))];
|
|
45
|
-
const parsed =
|
|
46
|
+
const parsed = manager.parse(raw);
|
|
46
47
|
return Node.fromJSON(richSchema(), parsed).toJSON().content ?? [];
|
|
47
48
|
}
|
|
48
49
|
function blocks(markdown, lockedFences = {}) {
|
|
49
50
|
const out = [];
|
|
50
|
-
const
|
|
51
|
+
const initialTokens = richMarkdownManager().instance.lexer(markdown);
|
|
52
|
+
const labels = /* @__PURE__ */ new Set();
|
|
53
|
+
for (const token of initialTokens) {
|
|
54
|
+
const definition = token.type === "def" ? /^ {0,3}\[([^\]]+)\]:/.exec(token.raw)?.[1] : void 0;
|
|
55
|
+
if (definition !== void 0) labels.add(normalizeReferenceLabel(definition));
|
|
56
|
+
}
|
|
57
|
+
const manager = scopedRichMarkdownManager(labels);
|
|
58
|
+
const tokens = manager.instance.lexer(markdown);
|
|
51
59
|
let at = 0;
|
|
52
60
|
let prevEnd = 0;
|
|
53
61
|
for (const token of tokens) {
|
|
@@ -55,7 +63,7 @@ function blocks(markdown, lockedFences = {}) {
|
|
|
55
63
|
at += token.raw.length;
|
|
56
64
|
if (token.type === "space") continue;
|
|
57
65
|
const raw = token.raw.replace(/\n+$/, "");
|
|
58
|
-
const nodes = parseBlock(token, raw, lockedFences);
|
|
66
|
+
const nodes = parseBlock(manager, token, raw, lockedFences);
|
|
59
67
|
out.push({
|
|
60
68
|
raw,
|
|
61
69
|
sep: markdown.slice(prevEnd, start),
|
|
@@ -107,10 +115,22 @@ function firstAtOrAfter(indices, minimum) {
|
|
|
107
115
|
return indices[low] ?? -1;
|
|
108
116
|
}
|
|
109
117
|
function serializeNodes(nodes) {
|
|
118
|
+
const json = nodes.map(asJson);
|
|
119
|
+
let marker = "raw-inline-amp";
|
|
120
|
+
const source = JSON.stringify(json);
|
|
121
|
+
while (source.includes(marker)) marker += "-";
|
|
122
|
+
const protect = (node) => ({
|
|
123
|
+
...node,
|
|
124
|
+
attrs: node.type === "rawInline" && typeof node.attrs?.source === "string" ? {
|
|
125
|
+
...node.attrs,
|
|
126
|
+
source: node.attrs.source.replaceAll("&", marker)
|
|
127
|
+
} : node.attrs,
|
|
128
|
+
content: node.content?.map(protect)
|
|
129
|
+
});
|
|
110
130
|
return restoreAmpersands(richMarkdownManager().serialize({
|
|
111
131
|
type: "doc",
|
|
112
|
-
content:
|
|
113
|
-
}).trim());
|
|
132
|
+
content: json.map(protect)
|
|
133
|
+
}).trim()).replaceAll(marker, "&");
|
|
114
134
|
}
|
|
115
135
|
function matchingBlockIndex(entry, keys, keyIndex, blockIndex) {
|
|
116
136
|
const currentKey = keys[keyIndex];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@segnavia/markdown-editor",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.45.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -36,9 +36,9 @@
|
|
|
36
36
|
}
|
|
37
37
|
},
|
|
38
38
|
"dependencies": {
|
|
39
|
-
"@segnavia/format": "0.
|
|
40
|
-
"@segnavia/frame": "0.
|
|
41
|
-
"@segnavia/html": "0.
|
|
39
|
+
"@segnavia/format": "0.45.0",
|
|
40
|
+
"@segnavia/frame": "0.45.0",
|
|
41
|
+
"@segnavia/html": "0.45.0"
|
|
42
42
|
},
|
|
43
43
|
"peerDependencies": {
|
|
44
44
|
"@codemirror/commands": "^6.11.1",
|
|
@@ -56,7 +56,8 @@
|
|
|
56
56
|
"@tiptap/markdown": "^3.32.0",
|
|
57
57
|
"@tiptap/pm": "^3.32.0",
|
|
58
58
|
"@tiptap/starter-kit": "^3.32.0",
|
|
59
|
-
"codemirror-lang-mermaid": "^0.5.0"
|
|
59
|
+
"codemirror-lang-mermaid": "^0.5.0",
|
|
60
|
+
"marked": "^17.0.1"
|
|
60
61
|
},
|
|
61
62
|
"devDependencies": {
|
|
62
63
|
"@codemirror/commands": "6.11.1",
|
|
@@ -74,6 +75,7 @@
|
|
|
74
75
|
"@tiptap/markdown": "3.32.0",
|
|
75
76
|
"@tiptap/pm": "3.32.0",
|
|
76
77
|
"@tiptap/starter-kit": "3.32.0",
|
|
77
|
-
"codemirror-lang-mermaid": "0.5.0"
|
|
78
|
+
"codemirror-lang-mermaid": "0.5.0",
|
|
79
|
+
"marked": "17.0.6"
|
|
78
80
|
}
|
|
79
81
|
}
|