@segnavia/markdown-editor 0.44.0 → 0.46.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/element-properties.d.ts +2 -2
- package/dist/element-properties.js +4 -2
- package/dist/markdown-editor-labels.d.ts +6 -0
- package/dist/markdown-editor-labels.js +7 -0
- package/dist/markdown-editor-model.js +12 -7
- package/dist/markdown-editor-state.js +30 -0
- package/dist/markdown-editor-title-input.js +50 -0
- package/dist/markdown-editor-types.d.ts +2 -0
- package/dist/markdown-editor.d.ts +3 -1
- package/dist/markdown-editor.js +38 -55
- package/dist/markdown-raw-inline-indication.d.ts +13 -0
- package/dist/markdown-raw-inline-indication.js +91 -0
- package/dist/markdown-rich-editor.js +36 -3
- package/dist/markdown-rich-extensions.js +214 -11
- package/dist/markdown-rich-locked.js +28 -2
- package/dist/markdown-rich-raw-inline.js +123 -0
- package/dist/markdown-rich-save.js +132 -14
- package/dist/markdown-rich-source-attrs.js +32 -0
- package/dist/markdown-rich-source-forms.js +368 -0
- package/dist/vue.d.ts +2 -0
- package/package.json +8 -6
|
@@ -1,11 +1,180 @@
|
|
|
1
1
|
import { LockedBlock } from "./markdown-rich-locked.js";
|
|
2
|
-
import {
|
|
2
|
+
import { rawInline } from "./markdown-rich-raw-inline.js";
|
|
3
|
+
import { listSourceAttributes, sourceFormStarterExtensions, sourceListAttrs, tableRowSourceAttributes, tableSourceExtension, taskItemSourceExtension } from "./markdown-rich-source-forms.js";
|
|
4
|
+
import { Extension, getExtensionField, getSchema } from "@tiptap/core";
|
|
3
5
|
import Image from "@tiptap/extension-image";
|
|
4
|
-
import { TaskItem, TaskList } from "@tiptap/extension-list";
|
|
6
|
+
import { BulletList, OrderedList, TaskItem, TaskList } from "@tiptap/extension-list";
|
|
5
7
|
import { TableKit } from "@tiptap/extension-table";
|
|
6
8
|
import { MarkdownManager } from "@tiptap/markdown";
|
|
7
9
|
import StarterKit from "@tiptap/starter-kit";
|
|
10
|
+
import { Marked } from "marked";
|
|
8
11
|
//#region src/markdown-rich-extensions.ts
|
|
12
|
+
const listAttributes = { loose: {
|
|
13
|
+
default: false,
|
|
14
|
+
parseHTML: (element) => element.getAttribute("data-loose") === "true",
|
|
15
|
+
renderHTML: (attrs) => attrs.loose ? { "data-loose": "true" } : {}
|
|
16
|
+
} };
|
|
17
|
+
const listSeparator = (node) => node.attrs?.loose ? "\n\n" : "\n";
|
|
18
|
+
const listItemPattern = /^(\s*)([-+*]|\d+[.)])(\s+)(?:\[[ xX]\]\s+)?/;
|
|
19
|
+
const indentWidth = (value) => value.replace(/\t/g, " ").length;
|
|
20
|
+
const lineIndent = (line) => indentWidth(/^\s*/.exec(line)?.[0] ?? "");
|
|
21
|
+
const itemContentIndent = (match) => indentWidth(match[1] ?? "") + (match[2]?.length ?? 0) + indentWidth(match[3] ?? "");
|
|
22
|
+
function updateListContext(match, indent, state) {
|
|
23
|
+
if (match && indent === state.baseIndent) {
|
|
24
|
+
state.contentIndent = itemContentIndent(match);
|
|
25
|
+
state.nestedIndent = void 0;
|
|
26
|
+
} else if (match && indent >= state.contentIndent && state.nestedIndent === void 0) state.nestedIndent = indent;
|
|
27
|
+
}
|
|
28
|
+
function consumeFence(line, indent, state) {
|
|
29
|
+
const match = /^(\s*)(`{3,}|~{3,})/.exec(line);
|
|
30
|
+
if (!match || indent < state.contentIndent || state.nestedIndent !== void 0) return false;
|
|
31
|
+
const marker = match[2]?.[0] ?? "";
|
|
32
|
+
const length = match[2]?.length ?? 0;
|
|
33
|
+
if (!state.fence) state.fence = {
|
|
34
|
+
marker,
|
|
35
|
+
length,
|
|
36
|
+
indent
|
|
37
|
+
};
|
|
38
|
+
else if (marker === state.fence.marker && length >= state.fence.length && indent <= state.fence.indent) state.fence = void 0;
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
function nextNonblank(lines, index) {
|
|
42
|
+
return lines.slice(index + 1).find((line) => !/^\s*$/.test(line));
|
|
43
|
+
}
|
|
44
|
+
function blankLineIsLoose(lines, index, state) {
|
|
45
|
+
const nextLine = nextNonblank(lines, index);
|
|
46
|
+
if (nextLine === void 0) return false;
|
|
47
|
+
const nextItem = listItemPattern.exec(nextLine);
|
|
48
|
+
const nextIndent = lineIndent(nextLine);
|
|
49
|
+
if (nextItem && nextIndent === state.baseIndent) return true;
|
|
50
|
+
if (state.nestedIndent !== void 0) {
|
|
51
|
+
if (nextIndent > state.nestedIndent || nextItem && nextIndent === state.nestedIndent) return false;
|
|
52
|
+
state.nestedIndent = void 0;
|
|
53
|
+
}
|
|
54
|
+
return nextIndent >= state.contentIndent;
|
|
55
|
+
}
|
|
56
|
+
function listLineIsLoose(lines, index, state) {
|
|
57
|
+
const line = lines[index] ?? "";
|
|
58
|
+
const match = listItemPattern.exec(line);
|
|
59
|
+
const indent = lineIndent(line);
|
|
60
|
+
updateListContext(match, indent, state);
|
|
61
|
+
if (consumeFence(line, indent, state)) return false;
|
|
62
|
+
return /^\s*$/.test(line) && state.fence === void 0 && blankLineIsLoose(lines, index, state);
|
|
63
|
+
}
|
|
64
|
+
function listIsLoose(raw) {
|
|
65
|
+
const lines = raw.split("\n");
|
|
66
|
+
const firstMatch = listItemPattern.exec(lines.find((line) => listItemPattern.test(line)) ?? "");
|
|
67
|
+
if (!firstMatch) return false;
|
|
68
|
+
const state = {
|
|
69
|
+
baseIndent: indentWidth(firstMatch[1] ?? ""),
|
|
70
|
+
contentIndent: itemContentIndent(firstMatch)
|
|
71
|
+
};
|
|
72
|
+
for (let index = 0; index < lines.length; index++) if (listLineIsLoose(lines, index, state)) return true;
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
const LooseBulletList = BulletList.extend({
|
|
76
|
+
addAttributes() {
|
|
77
|
+
return {
|
|
78
|
+
...this.parent?.(),
|
|
79
|
+
...listAttributes,
|
|
80
|
+
...listSourceAttributes
|
|
81
|
+
};
|
|
82
|
+
},
|
|
83
|
+
parseMarkdown(token, helpers) {
|
|
84
|
+
if (token.type !== "list" || token.ordered) return [];
|
|
85
|
+
return {
|
|
86
|
+
type: "bulletList",
|
|
87
|
+
attrs: {
|
|
88
|
+
loose: listIsLoose(token.raw ?? ""),
|
|
89
|
+
...sourceListAttrs(token.raw ?? "")
|
|
90
|
+
},
|
|
91
|
+
content: helpers.parseChildren(token.items ?? [])
|
|
92
|
+
};
|
|
93
|
+
},
|
|
94
|
+
renderMarkdown(node, helpers) {
|
|
95
|
+
return node.content ? helpers.renderChildren(node.content, listSeparator(node)) : "";
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
const LooseOrderedList = OrderedList.extend({
|
|
99
|
+
addAttributes() {
|
|
100
|
+
return {
|
|
101
|
+
...this.parent?.(),
|
|
102
|
+
...listAttributes,
|
|
103
|
+
...listSourceAttributes
|
|
104
|
+
};
|
|
105
|
+
},
|
|
106
|
+
parseMarkdown(token, helpers) {
|
|
107
|
+
if (token.type !== "list" || !token.ordered) return [];
|
|
108
|
+
return {
|
|
109
|
+
type: "orderedList",
|
|
110
|
+
attrs: {
|
|
111
|
+
start: token.start || 1,
|
|
112
|
+
type: token.typeMarker ?? null,
|
|
113
|
+
loose: listIsLoose(token.raw ?? ""),
|
|
114
|
+
...sourceListAttrs(token.raw ?? "", true)
|
|
115
|
+
},
|
|
116
|
+
content: helpers.parseChildren(token.items ?? [])
|
|
117
|
+
};
|
|
118
|
+
},
|
|
119
|
+
renderMarkdown(node, helpers) {
|
|
120
|
+
if (!node.content) return "";
|
|
121
|
+
const recorded = node.attrs?.sourceOrderedNumbers;
|
|
122
|
+
const keepsWrittenNumbers = Array.isArray(recorded) && recorded.length === node.content.length && node.content.every((item, index) => item.attrs?.sourceNumber === recorded[index]);
|
|
123
|
+
const content = node.content.map((item) => ({
|
|
124
|
+
...item,
|
|
125
|
+
attrs: {
|
|
126
|
+
...item.attrs,
|
|
127
|
+
sourceKeepWrittenNumbers: keepsWrittenNumbers
|
|
128
|
+
}
|
|
129
|
+
}));
|
|
130
|
+
return helpers.renderChildren(content, listSeparator(node));
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
const LooseTaskList = TaskList.extend({
|
|
134
|
+
addAttributes() {
|
|
135
|
+
return {
|
|
136
|
+
...this.parent?.(),
|
|
137
|
+
...listAttributes,
|
|
138
|
+
...listSourceAttributes
|
|
139
|
+
};
|
|
140
|
+
},
|
|
141
|
+
parseMarkdown(token, helpers) {
|
|
142
|
+
return helpers.createNode("taskList", {
|
|
143
|
+
loose: listIsLoose(token.raw ?? ""),
|
|
144
|
+
...sourceListAttrs(token.raw ?? "")
|
|
145
|
+
}, helpers.parseChildren(token.items ?? []));
|
|
146
|
+
},
|
|
147
|
+
renderMarkdown(node, helpers) {
|
|
148
|
+
return node.content ? helpers.renderChildren(node.content, listSeparator(node)) : "";
|
|
149
|
+
},
|
|
150
|
+
markdownTokenizer: {
|
|
151
|
+
name: "taskList",
|
|
152
|
+
level: "block",
|
|
153
|
+
start: (src) => src.search(/^\s*[-+*]\s+\[[ xX]\]\s+/m),
|
|
154
|
+
tokenize(src, _tokens, lexer) {
|
|
155
|
+
const token = new Marked().lexer(src)[0];
|
|
156
|
+
if (token?.type !== "list" || token.ordered || !token.items?.every((item) => item.task)) return void 0;
|
|
157
|
+
const items = token.items.map((item) => {
|
|
158
|
+
const lines = item.text.split("\n");
|
|
159
|
+
const mainContent = lines.shift() ?? "";
|
|
160
|
+
const nested = lines.join("\n").trim();
|
|
161
|
+
return {
|
|
162
|
+
type: "taskItem",
|
|
163
|
+
raw: item.raw,
|
|
164
|
+
checked: item.checked ?? false,
|
|
165
|
+
text: mainContent,
|
|
166
|
+
tokens: lexer.inlineTokens(mainContent),
|
|
167
|
+
nestedTokens: nested ? lexer.blockTokens(`${nested}\n`) : []
|
|
168
|
+
};
|
|
169
|
+
});
|
|
170
|
+
return {
|
|
171
|
+
type: "taskList",
|
|
172
|
+
raw: token.raw,
|
|
173
|
+
items
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
});
|
|
9
178
|
const directionalBlocks = Extension.create({
|
|
10
179
|
name: "directionalBlocks",
|
|
11
180
|
addGlobalAttributes() {
|
|
@@ -30,32 +199,66 @@ const directionalBlocks = Extension.create({
|
|
|
30
199
|
}];
|
|
31
200
|
}
|
|
32
201
|
});
|
|
33
|
-
function richExtensions() {
|
|
202
|
+
function richExtensions(definitionLabels = /* @__PURE__ */ new Set()) {
|
|
203
|
+
const starter = StarterKit.configure({
|
|
204
|
+
trailingNode: false,
|
|
205
|
+
link: { openOnClick: false },
|
|
206
|
+
bulletList: false,
|
|
207
|
+
orderedList: false
|
|
208
|
+
});
|
|
209
|
+
const starterChildren = getExtensionField(starter, "addExtensions", {
|
|
210
|
+
name: starter.name,
|
|
211
|
+
options: starter.options,
|
|
212
|
+
storage: starter.storage
|
|
213
|
+
})?.() ?? [];
|
|
214
|
+
const tables = TableKit.configure();
|
|
215
|
+
const tableChildren = getExtensionField(tables, "addExtensions", {
|
|
216
|
+
name: tables.name,
|
|
217
|
+
options: tables.options,
|
|
218
|
+
storage: tables.storage
|
|
219
|
+
})?.() ?? [];
|
|
34
220
|
return [
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
221
|
+
...sourceFormStarterExtensions(starterChildren),
|
|
222
|
+
LooseBulletList,
|
|
223
|
+
LooseOrderedList,
|
|
224
|
+
...tableChildren.map((extension) => {
|
|
225
|
+
if (extension.name === "tableRow") return extension.extend({ addAttributes() {
|
|
226
|
+
return {
|
|
227
|
+
...this.parent?.(),
|
|
228
|
+
...tableRowSourceAttributes
|
|
229
|
+
};
|
|
230
|
+
} });
|
|
231
|
+
return tableSourceExtension(extension);
|
|
38
232
|
}),
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
TaskItem.configure({ nested: true }),
|
|
233
|
+
LooseTaskList,
|
|
234
|
+
taskItemSourceExtension(TaskItem.configure({ nested: true })),
|
|
42
235
|
Image.configure({ inline: true }),
|
|
43
236
|
directionalBlocks,
|
|
44
|
-
LockedBlock
|
|
237
|
+
LockedBlock,
|
|
238
|
+
rawInline(definitionLabels)
|
|
45
239
|
];
|
|
46
240
|
}
|
|
47
241
|
let manager;
|
|
242
|
+
const isolatedMarked = () => new Marked();
|
|
48
243
|
function richMarkdownManager() {
|
|
49
244
|
manager ??= new MarkdownManager({
|
|
245
|
+
marked: isolatedMarked(),
|
|
50
246
|
extensions: richExtensions(),
|
|
51
247
|
markedOptions: { gfm: true }
|
|
52
248
|
});
|
|
53
249
|
return manager;
|
|
54
250
|
}
|
|
251
|
+
function scopedRichMarkdownManager(definitionLabels) {
|
|
252
|
+
return new MarkdownManager({
|
|
253
|
+
marked: isolatedMarked(),
|
|
254
|
+
extensions: richExtensions(definitionLabels),
|
|
255
|
+
markedOptions: { gfm: true }
|
|
256
|
+
});
|
|
257
|
+
}
|
|
55
258
|
let schema;
|
|
56
259
|
function richSchema() {
|
|
57
260
|
schema ??= getSchema(richExtensions());
|
|
58
261
|
return schema;
|
|
59
262
|
}
|
|
60
263
|
//#endregion
|
|
61
|
-
export { richExtensions, richMarkdownManager, richSchema };
|
|
264
|
+
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,123 @@
|
|
|
1
|
+
import { Node } from "@tiptap/core";
|
|
2
|
+
//#region src/markdown-rich-raw-inline.ts
|
|
3
|
+
const RAW_INLINE_NAME = "rawInline";
|
|
4
|
+
const RAW_INLINE_ATTRIBUTE = "data-raw-inline";
|
|
5
|
+
const RAW_INLINE_SOURCE_ATTRIBUTE = "data-raw-source";
|
|
6
|
+
const RAW_INLINE_DISPLAY_ATTRIBUTE = "data-raw-display";
|
|
7
|
+
const ASCII_PUNCTUATION = String.raw`[!-/:-@\[-` + "{-~]";
|
|
8
|
+
const escapePattern = new RegExp(`^\\\\${ASCII_PUNCTUATION}`);
|
|
9
|
+
const entityPattern = /^&(?:[A-Za-z][A-Za-z0-9]+|#[0-9]+|#[xX][0-9A-Fa-f]+);/;
|
|
10
|
+
const footnotePattern = /^\[\^[^\]\n]+\]/;
|
|
11
|
+
const inlineHtmlPattern = /^(?:<!--[\s\S]*?-->|<\/?[A-Za-z](?:[^<>"']|"[^"]*"|'[^']*')*?>)/;
|
|
12
|
+
const bracketContent = String.raw`(?:\\.|[^\[\]\\]|\[(?:\\.|[^\[\]\\])*\])*`;
|
|
13
|
+
const referencePattern = new RegExp(`^(!?)\\[(${bracketContent})\\](?:\\[(${bracketContent})\\])?`);
|
|
14
|
+
function normalizeReferenceLabel(label) {
|
|
15
|
+
return label.trim().replace(/\s+/g, " ").toLowerCase();
|
|
16
|
+
}
|
|
17
|
+
function referenceSource(src, labels) {
|
|
18
|
+
const match = referencePattern.exec(src);
|
|
19
|
+
if (!match) return void 0;
|
|
20
|
+
const raw = match[0];
|
|
21
|
+
const firstLabel = match[2] ?? "";
|
|
22
|
+
const secondLabel = match[3];
|
|
23
|
+
if (secondLabel === void 0 && src[raw.length] === "(") return void 0;
|
|
24
|
+
const label = secondLabel === void 0 || secondLabel === "" ? firstLabel : secondLabel;
|
|
25
|
+
return labels.has(normalizeReferenceLabel(label)) ? raw : void 0;
|
|
26
|
+
}
|
|
27
|
+
function rawSource(src, labels) {
|
|
28
|
+
const escaped = escapePattern.exec(src)?.[0];
|
|
29
|
+
if (escaped) return {
|
|
30
|
+
source: escaped,
|
|
31
|
+
display: escaped.slice(1)
|
|
32
|
+
};
|
|
33
|
+
const entity = entityPattern.exec(src)?.[0];
|
|
34
|
+
if (entity) return {
|
|
35
|
+
source: entity,
|
|
36
|
+
display: entity
|
|
37
|
+
};
|
|
38
|
+
const footnote = footnotePattern.exec(src)?.[0];
|
|
39
|
+
if (footnote) return {
|
|
40
|
+
source: footnote,
|
|
41
|
+
display: footnote
|
|
42
|
+
};
|
|
43
|
+
const reference = referenceSource(src, labels);
|
|
44
|
+
if (reference) return {
|
|
45
|
+
source: reference,
|
|
46
|
+
display: reference
|
|
47
|
+
};
|
|
48
|
+
const html = inlineHtmlPattern.exec(src)?.[0];
|
|
49
|
+
return html && (!html.startsWith("<!") ? !/\n[ \t]*\n/.test(html) : true) ? {
|
|
50
|
+
source: html,
|
|
51
|
+
display: html
|
|
52
|
+
} : void 0;
|
|
53
|
+
}
|
|
54
|
+
function rawInline(labels = /* @__PURE__ */ new Set()) {
|
|
55
|
+
return Node.create({
|
|
56
|
+
name: RAW_INLINE_NAME,
|
|
57
|
+
group: "inline",
|
|
58
|
+
inline: true,
|
|
59
|
+
atom: true,
|
|
60
|
+
selectable: true,
|
|
61
|
+
draggable: false,
|
|
62
|
+
addAttributes() {
|
|
63
|
+
return {
|
|
64
|
+
source: {
|
|
65
|
+
default: "",
|
|
66
|
+
parseHTML: (element) => element.getAttribute("data-raw-source") ?? "",
|
|
67
|
+
renderHTML: (attrs) => ({ [RAW_INLINE_SOURCE_ATTRIBUTE]: attrs.source })
|
|
68
|
+
},
|
|
69
|
+
display: {
|
|
70
|
+
default: "",
|
|
71
|
+
parseHTML: (element) => element.getAttribute("data-raw-display") ?? "",
|
|
72
|
+
renderHTML: (attrs) => ({ [RAW_INLINE_DISPLAY_ATTRIBUTE]: attrs.display })
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
},
|
|
76
|
+
parseHTML() {
|
|
77
|
+
return [{ tag: `span[${RAW_INLINE_ATTRIBUTE}]` }];
|
|
78
|
+
},
|
|
79
|
+
renderHTML({ HTMLAttributes }) {
|
|
80
|
+
return ["span", {
|
|
81
|
+
...HTMLAttributes,
|
|
82
|
+
[RAW_INLINE_ATTRIBUTE]: "",
|
|
83
|
+
dir: "auto"
|
|
84
|
+
}];
|
|
85
|
+
},
|
|
86
|
+
parseMarkdown(token) {
|
|
87
|
+
return {
|
|
88
|
+
type: RAW_INLINE_NAME,
|
|
89
|
+
attrs: {
|
|
90
|
+
source: token.source,
|
|
91
|
+
display: token.display
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
},
|
|
95
|
+
renderMarkdown(node) {
|
|
96
|
+
return typeof node.attrs?.source === "string" ? node.attrs.source : "";
|
|
97
|
+
},
|
|
98
|
+
markdownTokenizer: {
|
|
99
|
+
name: RAW_INLINE_NAME,
|
|
100
|
+
level: "inline",
|
|
101
|
+
start(src) {
|
|
102
|
+
const candidates = labels.size ? /\\|&|!?\[|</g : /\\|&|\[\^|</g;
|
|
103
|
+
for (const match of src.matchAll(candidates)) {
|
|
104
|
+
if (match.index === void 0) continue;
|
|
105
|
+
if (rawSource(src.slice(match.index), labels)) return match.index;
|
|
106
|
+
}
|
|
107
|
+
return -1;
|
|
108
|
+
},
|
|
109
|
+
tokenize(src) {
|
|
110
|
+
const raw = rawSource(src, labels);
|
|
111
|
+
return raw ? {
|
|
112
|
+
type: RAW_INLINE_NAME,
|
|
113
|
+
raw: raw.source,
|
|
114
|
+
source: raw.source,
|
|
115
|
+
display: raw.display
|
|
116
|
+
} : void 0;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
const RawInline = rawInline();
|
|
122
|
+
//#endregion
|
|
123
|
+
export { RAW_INLINE_ATTRIBUTE, RAW_INLINE_DISPLAY_ATTRIBUTE, RAW_INLINE_NAME, RAW_INLINE_SOURCE_ATTRIBUTE, RawInline, normalizeReferenceLabel, rawInline };
|
|
@@ -1,5 +1,7 @@
|
|
|
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 { meaningfulAttributes, withoutRecordedSourceAttrs } from "./markdown-rich-source-attrs.js";
|
|
4
|
+
import { richMarkdownManager, richSchema, scopedRichMarkdownManager } from "./markdown-rich-extensions.js";
|
|
3
5
|
import { Node } from "@tiptap/pm/model";
|
|
4
6
|
//#region src/markdown-rich-save.ts
|
|
5
7
|
let cachedOriginal;
|
|
@@ -39,15 +41,24 @@ const dropTrailingEmptyParagraph = (nodes) => {
|
|
|
39
41
|
return last && isEmptyParagraph(last) ? nodes.slice(0, -1) : nodes;
|
|
40
42
|
};
|
|
41
43
|
const restoreAmpersands = (markdown) => markdown.replace(/&(?![a-zA-Z][a-zA-Z0-9]*;)/g, "&");
|
|
42
|
-
function parseBlock(token, raw, lockedFences) {
|
|
44
|
+
function parseBlock(manager, token, raw, lockedFences) {
|
|
43
45
|
if (isSilentToken(token)) return [];
|
|
44
46
|
if (isLockedToken(token, lockedFences)) return [lockedNode(raw, lockedFenceLabel(lockedFences, token.lang))];
|
|
45
|
-
const parsed =
|
|
47
|
+
const parsed = manager.parse(raw);
|
|
46
48
|
return Node.fromJSON(richSchema(), parsed).toJSON().content ?? [];
|
|
47
49
|
}
|
|
48
|
-
function
|
|
50
|
+
function definitionLabels(tokens) {
|
|
51
|
+
const labels = /* @__PURE__ */ new Set();
|
|
52
|
+
for (const token of tokens) {
|
|
53
|
+
const definition = token.type === "def" ? /^ {0,3}\[([^\]]+)\]:/.exec(token.raw)?.[1] : void 0;
|
|
54
|
+
if (definition !== void 0) labels.add(normalizeReferenceLabel(definition));
|
|
55
|
+
}
|
|
56
|
+
return labels;
|
|
57
|
+
}
|
|
58
|
+
function blocks(markdown, labels, lockedFences = {}) {
|
|
49
59
|
const out = [];
|
|
50
|
-
const
|
|
60
|
+
const manager = scopedRichMarkdownManager(labels);
|
|
61
|
+
const tokens = manager.instance.lexer(markdown);
|
|
51
62
|
let at = 0;
|
|
52
63
|
let prevEnd = 0;
|
|
53
64
|
for (const token of tokens) {
|
|
@@ -55,7 +66,7 @@ function blocks(markdown, lockedFences = {}) {
|
|
|
55
66
|
at += token.raw.length;
|
|
56
67
|
if (token.type === "space") continue;
|
|
57
68
|
const raw = token.raw.replace(/\n+$/, "");
|
|
58
|
-
const nodes = parseBlock(token, raw, lockedFences);
|
|
69
|
+
const nodes = parseBlock(manager, token, raw, lockedFences);
|
|
59
70
|
out.push({
|
|
60
71
|
raw,
|
|
61
72
|
sep: markdown.slice(prevEnd, start),
|
|
@@ -73,7 +84,9 @@ function originalEntry(original, options) {
|
|
|
73
84
|
const nextFencesKey = fencesKey(options.lockedFences);
|
|
74
85
|
if (cachedOriginal?.original === original && cachedOriginal.fencesKey === nextFencesKey && cachedOriginal.bodyTransform === options.bodyTransform) return cachedOriginal;
|
|
75
86
|
const split = options.bodyTransform?.split(original);
|
|
76
|
-
const
|
|
87
|
+
const editable = split?.editable ?? original;
|
|
88
|
+
const labels = definitionLabels(richMarkdownManager().instance.lexer(editable));
|
|
89
|
+
const editableBlocks = blocks(editable, labels, options.lockedFences);
|
|
77
90
|
const nodes = editableBlocks.flatMap((block) => block.nodes);
|
|
78
91
|
const keys = editableBlocks.flatMap((block) => block.keys);
|
|
79
92
|
const indicesByFirstKey = /* @__PURE__ */ new Map();
|
|
@@ -92,7 +105,8 @@ function originalEntry(original, options) {
|
|
|
92
105
|
blocks: editableBlocks,
|
|
93
106
|
nodes,
|
|
94
107
|
keys,
|
|
95
|
-
indicesByFirstKey
|
|
108
|
+
indicesByFirstKey,
|
|
109
|
+
definitionLabels: labels
|
|
96
110
|
};
|
|
97
111
|
return cachedOriginal;
|
|
98
112
|
}
|
|
@@ -106,11 +120,115 @@ function firstAtOrAfter(indices, minimum) {
|
|
|
106
120
|
}
|
|
107
121
|
return indices[low] ?? -1;
|
|
108
122
|
}
|
|
109
|
-
function
|
|
110
|
-
|
|
123
|
+
function semantic(value, attrNodeType) {
|
|
124
|
+
if (Array.isArray(value)) {
|
|
125
|
+
const values = value.map((item) => semantic(item));
|
|
126
|
+
const merged = [];
|
|
127
|
+
for (const item of values) {
|
|
128
|
+
const previous = merged.at(-1);
|
|
129
|
+
if (isRecord(item) && item.type === "text" && typeof item.text === "string" && isRecord(previous) && previous.type === "text" && typeof previous.text === "string" && JSON.stringify(previous.marks) === JSON.stringify(item.marks)) previous.text += item.text;
|
|
130
|
+
else merged.push(item);
|
|
131
|
+
}
|
|
132
|
+
return merged;
|
|
133
|
+
}
|
|
134
|
+
if (!isRecord(value)) return value;
|
|
135
|
+
if (value.type === "rawInline" && isRecord(value.attrs) && /^\\(?:\[|\])$/.test(String(value.attrs.source))) return {
|
|
136
|
+
type: "text",
|
|
137
|
+
text: value.attrs.display
|
|
138
|
+
};
|
|
139
|
+
return meaningfulAttributes(Object.fromEntries(Object.entries(value).map(([key, item]) => [key, semantic(item, key === "attrs" ? value.type : void 0)])), {}, attrNodeType);
|
|
140
|
+
}
|
|
141
|
+
function sameDocument(markdown, nodes, manager) {
|
|
142
|
+
const parsed = Node.fromJSON(richSchema(), manager.parse(markdown)).toJSON().content ?? [];
|
|
143
|
+
return JSON.stringify(semantic(parsed)) === JSON.stringify(semantic(nodes));
|
|
144
|
+
}
|
|
145
|
+
function chooseBracketEscapes(markdown, readsSame, dangerousOffsets) {
|
|
146
|
+
if (!/\\(?:\[|\])/.test(markdown)) return {
|
|
147
|
+
markdown,
|
|
148
|
+
same: readsSame(markdown)
|
|
149
|
+
};
|
|
150
|
+
const all = markdown.replace(/\\(\[|\])/g, "$1");
|
|
151
|
+
if (readsSame(all)) return {
|
|
152
|
+
markdown: all,
|
|
153
|
+
same: true
|
|
154
|
+
};
|
|
155
|
+
let selective = all;
|
|
156
|
+
for (const offset of dangerousOffsets(all).sort((left, right) => right - left)) selective = `${selective.slice(0, offset)}\\${selective.slice(offset)}`;
|
|
157
|
+
if (selective !== all && readsSame(selective)) return {
|
|
158
|
+
markdown: selective,
|
|
159
|
+
same: true
|
|
160
|
+
};
|
|
161
|
+
return {
|
|
162
|
+
markdown,
|
|
163
|
+
same: readsSame(markdown)
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
function dangerousBracketOffsets(markdown, manager) {
|
|
167
|
+
const sources = [];
|
|
168
|
+
const seen = /* @__PURE__ */ new Set();
|
|
169
|
+
const pending = [manager.instance.lexer(markdown)];
|
|
170
|
+
while (pending.length) {
|
|
171
|
+
const value = pending.pop();
|
|
172
|
+
if (!value || typeof value !== "object" || seen.has(value)) continue;
|
|
173
|
+
seen.add(value);
|
|
174
|
+
if (Array.isArray(value)) {
|
|
175
|
+
pending.push(...[...value].reverse());
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
const token = value;
|
|
179
|
+
const raw = typeof token.raw === "string" ? token.raw : "";
|
|
180
|
+
const type = token.type;
|
|
181
|
+
const source = typeof token.source === "string" ? token.source : raw;
|
|
182
|
+
if ((type === "link" || type === "image" || type === "rawInline") && source.includes("[")) {
|
|
183
|
+
sources.push(raw || source);
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
const children = Object.entries(token).filter(([key]) => key !== "raw" && key !== "text" && key !== "source").map(([, child]) => child).reverse();
|
|
187
|
+
pending.push(...children);
|
|
188
|
+
}
|
|
189
|
+
const offsets = [];
|
|
190
|
+
let cursor = 0;
|
|
191
|
+
for (const source of sources) {
|
|
192
|
+
const at = markdown.indexOf(source, cursor);
|
|
193
|
+
const bracket = source.indexOf("[");
|
|
194
|
+
if (at < 0 || bracket < 0) continue;
|
|
195
|
+
offsets.push(at + bracket);
|
|
196
|
+
cursor = at + source.length;
|
|
197
|
+
}
|
|
198
|
+
return offsets;
|
|
199
|
+
}
|
|
200
|
+
function serializeNodes(nodes, definitionLabels) {
|
|
201
|
+
const json = nodes.map(asJson);
|
|
202
|
+
let marker = "raw-inline-amp";
|
|
203
|
+
const source = JSON.stringify(json);
|
|
204
|
+
while (source.includes(marker)) marker += "-";
|
|
205
|
+
const openMarker = `${marker}open`;
|
|
206
|
+
const closeMarker = `${marker}close`;
|
|
207
|
+
let textMarkerCode = 57600;
|
|
208
|
+
while (source.includes(String.fromCodePoint(textMarkerCode)) || source.includes(String.fromCodePoint(textMarkerCode + 1))) textMarkerCode += 2;
|
|
209
|
+
const textOpenMarker = String.fromCodePoint(textMarkerCode);
|
|
210
|
+
const textCloseMarker = String.fromCodePoint(textMarkerCode + 1);
|
|
211
|
+
const restoreProtected = (value) => value.replaceAll(openMarker, "\\[").replaceAll(closeMarker, "\\]").replaceAll(marker, "&");
|
|
212
|
+
const protect = (node) => ({
|
|
213
|
+
...node,
|
|
214
|
+
text: node.type === "text" && typeof node.text === "string" && !node.marks?.some((mark) => mark.type === "link") ? node.text.replaceAll("[", textOpenMarker).replaceAll("]", textCloseMarker) : node.text,
|
|
215
|
+
attrs: node.type === "rawInline" && typeof node.attrs?.source === "string" ? {
|
|
216
|
+
...node.attrs,
|
|
217
|
+
source: node.attrs.source.replaceAll("&", marker).replaceAll("\\[", openMarker).replaceAll("\\]", closeMarker)
|
|
218
|
+
} : node.attrs,
|
|
219
|
+
content: node.content?.map(protect)
|
|
220
|
+
});
|
|
221
|
+
const manager = scopedRichMarkdownManager(definitionLabels);
|
|
222
|
+
const render = (content) => restoreAmpersands(manager.serialize({
|
|
111
223
|
type: "doc",
|
|
112
|
-
content:
|
|
113
|
-
}).
|
|
224
|
+
content: content.map(protect)
|
|
225
|
+
}).replace(/^\n+|\n+$/g, "")).replaceAll(textOpenMarker, "\\[").replaceAll(textCloseMarker, "\\]");
|
|
226
|
+
const meaningful = (candidate) => {
|
|
227
|
+
return chooseBracketEscapes(candidate, (value) => sameDocument(restoreProtected(value), json, manager), (value) => dangerousBracketOffsets(value, manager));
|
|
228
|
+
};
|
|
229
|
+
let result = meaningful(render(json));
|
|
230
|
+
if (!result.same) result = meaningful(render(json.map(withoutRecordedSourceAttrs)));
|
|
231
|
+
return restoreProtected(result.markdown);
|
|
114
232
|
}
|
|
115
233
|
function matchingBlockIndex(entry, keys, keyIndex, blockIndex) {
|
|
116
234
|
const currentKey = keys[keyIndex];
|
|
@@ -133,7 +251,7 @@ function stitch(entry, edited, keys) {
|
|
|
133
251
|
const pending = [];
|
|
134
252
|
const emitNew = () => {
|
|
135
253
|
if (!pending.length) return;
|
|
136
|
-
out += (out ? "\n\n" : bs[blockIndex]?.sep ?? "") + serializeNodes(pending.splice(0));
|
|
254
|
+
out += (out ? "\n\n" : bs[blockIndex]?.sep ?? "") + serializeNodes(pending.splice(0), entry.definitionLabels);
|
|
137
255
|
last = -1;
|
|
138
256
|
};
|
|
139
257
|
const emitOld = (hit) => {
|
|
@@ -179,4 +297,4 @@ function saveRichDocument(original, edited, options = {}) {
|
|
|
179
297
|
return entry.split && options.bodyTransform ? options.bodyTransform.join(saved, entry.split.retained) : saved;
|
|
180
298
|
}
|
|
181
299
|
//#endregion
|
|
182
|
-
export { parseRichDocument, saveRichDocument };
|
|
300
|
+
export { chooseBracketEscapes, parseRichDocument, saveRichDocument };
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import "./markdown-rich-raw-inline.js";
|
|
2
|
+
//#region src/markdown-rich-source-attrs.ts
|
|
3
|
+
function isRecordedSourceAttr(key, nodeType) {
|
|
4
|
+
return key.startsWith("source") && !(nodeType === "rawInline" && key === "source");
|
|
5
|
+
}
|
|
6
|
+
function meaningfulAttributes(value, options = {}, attrNodeType) {
|
|
7
|
+
if (Array.isArray(value)) return value.map((item) => meaningfulAttributes(item, options));
|
|
8
|
+
if (!value || typeof value !== "object") return value;
|
|
9
|
+
const record = value;
|
|
10
|
+
const nodeType = record.type ?? attrNodeType;
|
|
11
|
+
const out = {};
|
|
12
|
+
for (const [key, item] of Object.entries(record)) {
|
|
13
|
+
if (key === "dir" || isRecordedSourceAttr(key, nodeType) || item === null || item === void 0) continue;
|
|
14
|
+
if (!options.keepEmptyArrays && Array.isArray(item) && item.length === 0) continue;
|
|
15
|
+
if (options.omitDefaultTableSpans && (key === "colspan" || key === "rowspan") && item === 1) continue;
|
|
16
|
+
const next = meaningfulAttributes(item, options, key === "attrs" ? nodeType : void 0);
|
|
17
|
+
if (next && typeof next === "object" && !Array.isArray(next) && Object.keys(next).length === 0) continue;
|
|
18
|
+
out[key] = next;
|
|
19
|
+
}
|
|
20
|
+
return out;
|
|
21
|
+
}
|
|
22
|
+
function withoutRecordedSourceAttrs(value) {
|
|
23
|
+
const attrs = value.attrs ? Object.fromEntries(Object.entries(value.attrs).filter(([key]) => !isRecordedSourceAttr(key, value.type))) : value.attrs;
|
|
24
|
+
return {
|
|
25
|
+
...value,
|
|
26
|
+
attrs,
|
|
27
|
+
marks: value.marks?.map((mark) => withoutRecordedSourceAttrs(mark)),
|
|
28
|
+
content: value.content?.map(withoutRecordedSourceAttrs)
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
//#endregion
|
|
32
|
+
export { isRecordedSourceAttr, meaningfulAttributes, withoutRecordedSourceAttrs };
|