@segnavia/markdown-editor 0.45.0 → 0.47.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/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-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 +33 -18
- package/dist/markdown-rich-extensions.js +57 -16
- package/dist/markdown-rich-raw-inline.js +13 -6
- package/dist/markdown-rich-save.js +111 -13
- 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 +4 -4
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
export declare const MARKDOWN_EDITOR_TAG = "segnavia-markdown-editor";
|
|
3
3
|
export declare const MARKDOWN_EDITOR_PROPERTIES: {
|
|
4
4
|
readonly tag: "segnavia-markdown-editor";
|
|
5
|
-
readonly properties: readonly ["markdown", "mode", "title", "titleSource", "lockedFences", "css", "fonts", "fontReferences", "linkOrigins", "alertLabels"];
|
|
6
|
-
readonly events: readonly ["segnavia-markdown-change", "segnavia-title-change", "segnavia-mode-change", "segnavia-save", "segnavia-editor-state", "segnavia-link-request"];
|
|
5
|
+
readonly properties: readonly ["markdown", "mode", "title", "titleSource", "lockedFences", "css", "fonts", "fontReferences", "linkOrigins", "alertLabels", "labels"];
|
|
6
|
+
readonly events: readonly ["segnavia-markdown-change", "segnavia-title-change", "segnavia-mode-change", "segnavia-save", "segnavia-editor-state", "segnavia-link-request", "segnavia-raw-inline-indicate"];
|
|
7
7
|
};
|
|
8
8
|
//#endregion
|
|
@@ -12,7 +12,8 @@ const MARKDOWN_EDITOR_PROPERTIES = {
|
|
|
12
12
|
"fonts",
|
|
13
13
|
"fontReferences",
|
|
14
14
|
"linkOrigins",
|
|
15
|
-
"alertLabels"
|
|
15
|
+
"alertLabels",
|
|
16
|
+
"labels"
|
|
16
17
|
],
|
|
17
18
|
events: [
|
|
18
19
|
"segnavia-markdown-change",
|
|
@@ -20,7 +21,8 @@ const MARKDOWN_EDITOR_PROPERTIES = {
|
|
|
20
21
|
"segnavia-mode-change",
|
|
21
22
|
"segnavia-save",
|
|
22
23
|
"segnavia-editor-state",
|
|
23
|
-
"segnavia-link-request"
|
|
24
|
+
"segnavia-link-request",
|
|
25
|
+
"segnavia-raw-inline-indicate"
|
|
24
26
|
]
|
|
25
27
|
};
|
|
26
28
|
//#endregion
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
//#region src/markdown-editor-labels.ts
|
|
2
|
+
const ENGLISH_MARKDOWN_EDITOR_LABELS = { rawInline: (display, source) => `${display}, Markdown source: ${source}` };
|
|
3
|
+
function markdownEditorLabels(labels) {
|
|
4
|
+
return { rawInline: labels.rawInline ?? ENGLISH_MARKDOWN_EDITOR_LABELS.rawInline };
|
|
5
|
+
}
|
|
6
|
+
//#endregion
|
|
7
|
+
export { ENGLISH_MARKDOWN_EDITOR_LABELS, markdownEditorLabels };
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { richEditorState } from "./markdown-rich-commands.js";
|
|
2
|
+
import { redoDepth, undoDepth } from "@codemirror/commands";
|
|
3
|
+
//#region src/markdown-editor-state.ts
|
|
4
|
+
var MarkdownEditorStateEmitter = class {
|
|
5
|
+
dispatch;
|
|
6
|
+
key = "";
|
|
7
|
+
constructor(dispatch) {
|
|
8
|
+
this.dispatch = dispatch;
|
|
9
|
+
}
|
|
10
|
+
source(view, canUndo = view ? undoDepth(view.state) > 0 : false, canRedo = view ? redoDepth(view.state) > 0 : false) {
|
|
11
|
+
this.emit({
|
|
12
|
+
marks: [],
|
|
13
|
+
nodes: [],
|
|
14
|
+
canUndo,
|
|
15
|
+
canRedo
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
visible(mode, richView, sourceView) {
|
|
19
|
+
if (mode === "rich" && richView) this.emit(richEditorState(richView));
|
|
20
|
+
else this.source(sourceView);
|
|
21
|
+
}
|
|
22
|
+
emit(state) {
|
|
23
|
+
const key = JSON.stringify(state);
|
|
24
|
+
if (key === this.key) return;
|
|
25
|
+
this.key = key;
|
|
26
|
+
this.dispatch(state);
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
//#endregion
|
|
30
|
+
export { MarkdownEditorStateEmitter };
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { normalizeMarkdownTitle } from "./markdown-title.js";
|
|
2
|
+
import { setModelTitle } from "./markdown-editor-model.js";
|
|
3
|
+
import { markdownEditorKeyAction } from "./markdown-editor-policy.js";
|
|
4
|
+
//#region src/markdown-editor-title-input.ts
|
|
5
|
+
var MarkdownEditorTitleInput = class {
|
|
6
|
+
options;
|
|
7
|
+
constructor(options) {
|
|
8
|
+
this.options = options;
|
|
9
|
+
}
|
|
10
|
+
connect() {
|
|
11
|
+
const { field } = this.options;
|
|
12
|
+
field.addEventListener("input", this.input);
|
|
13
|
+
field.addEventListener("keydown", this.keyDown);
|
|
14
|
+
field.addEventListener("paste", this.paste);
|
|
15
|
+
}
|
|
16
|
+
disconnect() {
|
|
17
|
+
const { field } = this.options;
|
|
18
|
+
field.removeEventListener("input", this.input);
|
|
19
|
+
field.removeEventListener("keydown", this.keyDown);
|
|
20
|
+
field.removeEventListener("paste", this.paste);
|
|
21
|
+
}
|
|
22
|
+
input = () => {
|
|
23
|
+
const { field } = this.options;
|
|
24
|
+
const normalized = normalizeMarkdownTitle(field.value);
|
|
25
|
+
field.value = normalized;
|
|
26
|
+
const model = setModelTitle(this.options.model(), normalized);
|
|
27
|
+
if (model === this.options.model()) return;
|
|
28
|
+
this.options.update(model);
|
|
29
|
+
this.options.emit("segnavia-title-change", { title: normalized });
|
|
30
|
+
if (model.titleSource === "leading-heading") this.options.emit("segnavia-markdown-change", { markdown: model.markdown });
|
|
31
|
+
};
|
|
32
|
+
keyDown = (event) => {
|
|
33
|
+
if (event.key === "Enter") event.preventDefault();
|
|
34
|
+
const action = markdownEditorKeyAction(event);
|
|
35
|
+
if (!action) return;
|
|
36
|
+
event.preventDefault();
|
|
37
|
+
if (action === "save") this.options.emit("segnavia-save");
|
|
38
|
+
else this.options.toggleMode();
|
|
39
|
+
};
|
|
40
|
+
paste = (event) => {
|
|
41
|
+
const pasted = event.clipboardData?.getData("text");
|
|
42
|
+
if (pasted === void 0) return;
|
|
43
|
+
event.preventDefault();
|
|
44
|
+
const { field } = this.options;
|
|
45
|
+
field.setRangeText(normalizeMarkdownTitle(pasted), field.selectionStart ?? 0, field.selectionEnd ?? 0, "end");
|
|
46
|
+
this.input();
|
|
47
|
+
};
|
|
48
|
+
};
|
|
49
|
+
//#endregion
|
|
50
|
+
export { MarkdownEditorTitleInput };
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { MarkdownEditorLabels } from "./markdown-editor-labels.js";
|
|
1
2
|
import { MarkdownTitleSource } from "./markdown-editor-model.js";
|
|
2
3
|
import { MarkdownEditorCommand, MarkdownEditorCommandArgs, MarkdownEditorMode } from "./markdown-editor-policy.js";
|
|
3
4
|
import { AlertLabels } from "@segnavia/html";
|
|
@@ -22,6 +23,7 @@ export interface SegnaviaMarkdownEditor extends Omit<HTMLElement, 'title'> {
|
|
|
22
23
|
fontReferences: FontReference[];
|
|
23
24
|
linkOrigins: Iterable<string> | undefined;
|
|
24
25
|
alertLabels: Partial<AlertLabels> | undefined;
|
|
26
|
+
labels: Partial<MarkdownEditorLabels>;
|
|
25
27
|
unstyled: boolean;
|
|
26
28
|
run<C extends MarkdownEditorCommand>(command: C, args?: C extends keyof MarkdownEditorCommandArgs ? MarkdownEditorCommandArgs[C] : never): boolean;
|
|
27
29
|
}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
import { ENGLISH_MARKDOWN_EDITOR_LABELS, MarkdownEditorLabels } from "./markdown-editor-labels.js";
|
|
1
2
|
import { MarkdownTitleSource } from "./markdown-editor-model.js";
|
|
2
3
|
import { MARKDOWN_EDITOR_COMMANDS, MARKDOWN_EDITOR_MODES, MarkdownEditorCommand, MarkdownEditorMode } from "./markdown-editor-policy.js";
|
|
3
4
|
import { MarkdownEditorStateDetail, SegnaviaMarkdownEditor as SegnaviaMarkdownEditor$1 } from "./markdown-editor-types.js";
|
|
4
5
|
import { MARKDOWN_EDITOR_PROPERTIES, MARKDOWN_EDITOR_TAG } from "./element-properties.js";
|
|
6
|
+
import { RawInlineIndicationDetail } from "./markdown-raw-inline-indication.js";
|
|
5
7
|
import { joinLeadingHeading, normalizeMarkdownTitle, splitLeadingHeading } from "./markdown-title.js";
|
|
6
8
|
import { FontsOptions } from "@segnavia/frame/fonts";
|
|
7
9
|
//#region src/markdown-editor.d.ts
|
|
@@ -10,4 +12,4 @@ export declare const SegnaviaMarkdownEditor: {
|
|
|
10
12
|
new (): SegnaviaMarkdownEditor;
|
|
11
13
|
};
|
|
12
14
|
//#endregion
|
|
13
|
-
export { type FontsOptions, MARKDOWN_EDITOR_COMMANDS, MARKDOWN_EDITOR_MODES, MARKDOWN_EDITOR_PROPERTIES, MARKDOWN_EDITOR_TAG, type MarkdownEditorCommand, type MarkdownEditorMode, type MarkdownEditorStateDetail, type MarkdownTitleSource, joinLeadingHeading, normalizeMarkdownTitle, splitLeadingHeading };
|
|
15
|
+
export { ENGLISH_MARKDOWN_EDITOR_LABELS, type FontsOptions, MARKDOWN_EDITOR_COMMANDS, MARKDOWN_EDITOR_MODES, MARKDOWN_EDITOR_PROPERTIES, MARKDOWN_EDITOR_TAG, type MarkdownEditorCommand, type MarkdownEditorLabels, type MarkdownEditorMode, type MarkdownEditorStateDetail, type MarkdownTitleSource, type RawInlineIndicationDetail, joinLeadingHeading, normalizeMarkdownTitle, splitLeadingHeading };
|
package/dist/markdown-editor.js
CHANGED
|
@@ -1,17 +1,22 @@
|
|
|
1
1
|
import { ensureEditorStyle } from "./editor-style.js";
|
|
2
2
|
import { MARKDOWN_EDITOR_PROPERTIES, MARKDOWN_EDITOR_TAG } from "./element-properties.js";
|
|
3
3
|
import { MarkdownEditorFontRegistration } from "./markdown-editor-font-registration.js";
|
|
4
|
+
import { ENGLISH_MARKDOWN_EDITOR_LABELS, markdownEditorLabels } from "./markdown-editor-labels.js";
|
|
4
5
|
import { validateMarkdownLink } from "./markdown-editor-link.js";
|
|
5
6
|
import { joinLeadingHeading, normalizeMarkdownTitle, splitLeadingHeading } from "./markdown-title.js";
|
|
6
|
-
import { markdownEditorModel, setModelEditable, setModelMarkdown,
|
|
7
|
+
import { markdownEditorModel, setModelEditable, setModelMarkdown, setModelTitleSource } from "./markdown-editor-model.js";
|
|
7
8
|
import { equalAlertLabels, equalFontReferences, equalFonts, equalLockedFences, normalizeAlertLabels, normalizeFontReferences, normalizeLockedFences } from "./markdown-editor-options.js";
|
|
8
|
-
import { MARKDOWN_EDITOR_COMMANDS, MARKDOWN_EDITOR_MODES, availableMarkdownMode,
|
|
9
|
+
import { MARKDOWN_EDITOR_COMMANDS, MARKDOWN_EDITOR_MODES, availableMarkdownMode, richModeCommandRefusal } from "./markdown-editor-policy.js";
|
|
10
|
+
import { RICH_COMMANDS, lockedBlockSourceOffset, selectedLockedBlock } from "./markdown-rich-commands.js";
|
|
11
|
+
import { MarkdownEditorStateEmitter } from "./markdown-editor-state.js";
|
|
9
12
|
import { renderMarkdownEditorTitle, updateMarkdownEditorTitle } from "./markdown-editor-title.js";
|
|
10
|
-
import {
|
|
13
|
+
import { MarkdownEditorTitleInput } from "./markdown-editor-title-input.js";
|
|
14
|
+
import { RAW_INLINE_ATTRIBUTE } from "./markdown-rich-raw-inline.js";
|
|
15
|
+
import { RawInlineIndicator, labelRawInline } from "./markdown-raw-inline-indication.js";
|
|
11
16
|
import { mountRichEditor } from "./markdown-rich-editor.js";
|
|
12
17
|
import { parseRichDocument, saveRichDocument } from "./markdown-rich-save.js";
|
|
13
18
|
import { markdownSourceState } from "./markdown-source.js";
|
|
14
|
-
import { redo,
|
|
19
|
+
import { redo, undo } from "@codemirror/commands";
|
|
15
20
|
import { EditorView } from "@codemirror/view";
|
|
16
21
|
import { upgradeProperties } from "@segnavia/frame/element-properties";
|
|
17
22
|
import { sanitizeBookCss } from "@segnavia/html";
|
|
@@ -36,14 +41,23 @@ var MarkdownEditorElement = class extends HTMLElement {
|
|
|
36
41
|
modeValue = "rich";
|
|
37
42
|
reflecting = false;
|
|
38
43
|
richOriginal = "";
|
|
39
|
-
stateKey = "";
|
|
40
44
|
lockedFenceOptions = {};
|
|
41
45
|
cssValue = "";
|
|
42
46
|
fontOptions;
|
|
43
47
|
fontReferenceOptions = [];
|
|
44
48
|
linkOriginOptions;
|
|
45
49
|
alertLabelOptions;
|
|
50
|
+
words = ENGLISH_MARKDOWN_EDITOR_LABELS;
|
|
51
|
+
rawInlineIndicator = new RawInlineIndicator((detail) => this.emit("segnavia-raw-inline-indicate", detail));
|
|
46
52
|
fontRegistration = new MarkdownEditorFontRegistration(this);
|
|
53
|
+
state = new MarkdownEditorStateEmitter((detail) => this.emit("segnavia-editor-state", detail));
|
|
54
|
+
titleInput = new MarkdownEditorTitleInput({
|
|
55
|
+
field: this.titleField,
|
|
56
|
+
model: () => this.model,
|
|
57
|
+
update: (model) => this.model = model,
|
|
58
|
+
emit: (name, detail) => this.emit(name, detail),
|
|
59
|
+
toggleMode: () => this.run("toggle-mode")
|
|
60
|
+
});
|
|
47
61
|
constructor() {
|
|
48
62
|
super();
|
|
49
63
|
upgradeProperties(this);
|
|
@@ -59,16 +73,13 @@ var MarkdownEditorElement = class extends HTMLElement {
|
|
|
59
73
|
}
|
|
60
74
|
connectedCallback() {
|
|
61
75
|
ensureEditorStyle(this.container);
|
|
62
|
-
this.
|
|
63
|
-
this.titleField.addEventListener("keydown", this.onTitleKeyDown);
|
|
64
|
-
this.titleField.addEventListener("paste", this.onTitlePaste);
|
|
76
|
+
this.titleInput.connect();
|
|
65
77
|
this.mountMode();
|
|
66
78
|
renderMarkdownEditorTitle(this.titleField, this.model);
|
|
67
79
|
}
|
|
68
80
|
disconnectedCallback() {
|
|
69
|
-
this.
|
|
70
|
-
this.
|
|
71
|
-
this.titleField.removeEventListener("paste", this.onTitlePaste);
|
|
81
|
+
this.rawInlineIndicator.end();
|
|
82
|
+
this.titleInput.disconnect();
|
|
72
83
|
this.sourceView?.destroy();
|
|
73
84
|
this.sourceView = null;
|
|
74
85
|
this.richView?.destroy();
|
|
@@ -185,6 +196,13 @@ var MarkdownEditorElement = class extends HTMLElement {
|
|
|
185
196
|
this.alertLabelOptions = next;
|
|
186
197
|
if (this.modeValue === "rich") this.resetRich();
|
|
187
198
|
}
|
|
199
|
+
get labels() {
|
|
200
|
+
return this.words;
|
|
201
|
+
}
|
|
202
|
+
set labels(value) {
|
|
203
|
+
this.words = markdownEditorLabels(value ?? {});
|
|
204
|
+
for (const atom of this.richMount.querySelectorAll(`[${RAW_INLINE_ATTRIBUTE}]`)) labelRawInline(atom, this.words);
|
|
205
|
+
}
|
|
188
206
|
get unstyled() {
|
|
189
207
|
return this.hasAttribute("unstyled");
|
|
190
208
|
}
|
|
@@ -209,6 +227,7 @@ var MarkdownEditorElement = class extends HTMLElement {
|
|
|
209
227
|
setMode(mode, emit) {
|
|
210
228
|
if (mode === this.modeValue) return false;
|
|
211
229
|
this.modeValue = mode;
|
|
230
|
+
this.rawInlineIndicator.end();
|
|
212
231
|
this.reflect("mode", mode);
|
|
213
232
|
this.mountMode();
|
|
214
233
|
if (emit) this.emit("segnavia-mode-change", { mode });
|
|
@@ -243,10 +262,11 @@ var MarkdownEditorElement = class extends HTMLElement {
|
|
|
243
262
|
resetVisibleEditor() {
|
|
244
263
|
if (this.modeValue === "source" && this.sourceView) this.sourceView.setState(this.sourceState());
|
|
245
264
|
if (this.modeValue === "rich") this.resetRich();
|
|
246
|
-
this.
|
|
265
|
+
this.state.visible(this.modeValue, this.richView, this.sourceView);
|
|
247
266
|
}
|
|
248
267
|
resetRich() {
|
|
249
268
|
if (!this.isConnected) return;
|
|
269
|
+
this.rawInlineIndicator.end();
|
|
250
270
|
this.richView?.destroy();
|
|
251
271
|
this.richMount.replaceChildren();
|
|
252
272
|
this.richOriginal = this.model.editable;
|
|
@@ -258,8 +278,10 @@ var MarkdownEditorElement = class extends HTMLElement {
|
|
|
258
278
|
fontReferences: this.fontReferenceOptions,
|
|
259
279
|
alertLabels: this.alertLabelOptions,
|
|
260
280
|
linkOrigins: () => this.linkOriginOptions,
|
|
281
|
+
labels: () => this.words,
|
|
282
|
+
indicator: this.rawInlineIndicator,
|
|
261
283
|
change: (content) => this.onRichChange(content),
|
|
262
|
-
state: (state) => this.
|
|
284
|
+
state: (state) => this.state.emit(state),
|
|
263
285
|
linkRequest: (href) => this.emit("segnavia-link-request", { href }),
|
|
264
286
|
save: () => this.emit("segnavia-save"),
|
|
265
287
|
toggleMode: () => this.setMode("source", true),
|
|
@@ -292,47 +314,8 @@ var MarkdownEditorElement = class extends HTMLElement {
|
|
|
292
314
|
this.model = model;
|
|
293
315
|
this.emit("segnavia-markdown-change", { markdown: model.markdown });
|
|
294
316
|
}
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
this.titleField.value = normalized;
|
|
298
|
-
const model = setModelTitle(this.model, normalized);
|
|
299
|
-
if (model === this.model) return;
|
|
300
|
-
this.model = model;
|
|
301
|
-
this.emit("segnavia-title-change", { title: normalized });
|
|
302
|
-
if (model.titleSource === "leading-heading") this.emit("segnavia-markdown-change", { markdown: model.markdown });
|
|
303
|
-
};
|
|
304
|
-
onTitleKeyDown = (event) => {
|
|
305
|
-
if (event.key === "Enter") event.preventDefault();
|
|
306
|
-
const action = markdownEditorKeyAction(event);
|
|
307
|
-
if (!action) return;
|
|
308
|
-
event.preventDefault();
|
|
309
|
-
if (action === "save") this.emit("segnavia-save");
|
|
310
|
-
else this.run("toggle-mode");
|
|
311
|
-
};
|
|
312
|
-
onTitlePaste = (event) => {
|
|
313
|
-
const pasted = event.clipboardData?.getData("text");
|
|
314
|
-
if (pasted === void 0) return;
|
|
315
|
-
event.preventDefault();
|
|
316
|
-
this.titleField.setRangeText(normalizeMarkdownTitle(pasted), this.titleField.selectionStart ?? 0, this.titleField.selectionEnd ?? 0, "end");
|
|
317
|
-
this.onTitleInput();
|
|
318
|
-
};
|
|
319
|
-
emitSourceState(canUndo = this.sourceView ? undoDepth(this.sourceView.state) > 0 : false, canRedo = this.sourceView ? redoDepth(this.sourceView.state) > 0 : false) {
|
|
320
|
-
this.emitState({
|
|
321
|
-
marks: [],
|
|
322
|
-
nodes: [],
|
|
323
|
-
canUndo,
|
|
324
|
-
canRedo
|
|
325
|
-
});
|
|
326
|
-
}
|
|
327
|
-
emitVisibleState() {
|
|
328
|
-
if (this.modeValue === "rich" && this.richView) this.emitState(richEditorState(this.richView));
|
|
329
|
-
else this.emitSourceState();
|
|
330
|
-
}
|
|
331
|
-
emitState(state) {
|
|
332
|
-
const key = JSON.stringify(state);
|
|
333
|
-
if (key === this.stateKey) return;
|
|
334
|
-
this.stateKey = key;
|
|
335
|
-
this.emit("segnavia-editor-state", state);
|
|
317
|
+
emitSourceState(canUndo, canRedo) {
|
|
318
|
+
this.state.source(this.sourceView, canUndo, canRedo);
|
|
336
319
|
}
|
|
337
320
|
emit(name, detail) {
|
|
338
321
|
this.dispatchEvent(new CustomEvent(name, {
|
|
@@ -352,4 +335,4 @@ var MarkdownEditorElement = class extends HTMLElement {
|
|
|
352
335
|
const SegnaviaMarkdownEditor = MarkdownEditorElement;
|
|
353
336
|
if (!customElements.get("segnavia-markdown-editor")) customElements.define(MARKDOWN_EDITOR_TAG, SegnaviaMarkdownEditor);
|
|
354
337
|
//#endregion
|
|
355
|
-
export { MARKDOWN_EDITOR_COMMANDS, MARKDOWN_EDITOR_MODES, MARKDOWN_EDITOR_PROPERTIES, MARKDOWN_EDITOR_TAG, SegnaviaMarkdownEditor, joinLeadingHeading, normalizeMarkdownTitle, splitLeadingHeading };
|
|
338
|
+
export { ENGLISH_MARKDOWN_EDITOR_LABELS, MARKDOWN_EDITOR_COMMANDS, MARKDOWN_EDITOR_MODES, MARKDOWN_EDITOR_PROPERTIES, MARKDOWN_EDITOR_TAG, SegnaviaMarkdownEditor, joinLeadingHeading, normalizeMarkdownTitle, splitLeadingHeading };
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
//#region src/markdown-raw-inline-indication.d.ts
|
|
2
|
+
export type RawInlineIndicationDetail = Readonly<{
|
|
3
|
+
source: string;
|
|
4
|
+
display: string;
|
|
5
|
+
rect: Readonly<{
|
|
6
|
+
x: number;
|
|
7
|
+
y: number;
|
|
8
|
+
width: number;
|
|
9
|
+
height: number;
|
|
10
|
+
}>;
|
|
11
|
+
via: 'pointer' | 'selection';
|
|
12
|
+
}>;
|
|
13
|
+
//#endregion
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { RAW_INLINE_ATTRIBUTE } from "./markdown-rich-raw-inline.js";
|
|
2
|
+
import { NodeSelection } from "@tiptap/pm/state";
|
|
3
|
+
import { resolveDirection } from "@segnavia/format";
|
|
4
|
+
//#region src/markdown-raw-inline-indication.ts
|
|
5
|
+
const rawInlineSelector = `[${RAW_INLINE_ATTRIBUTE}]`;
|
|
6
|
+
function rawInlineElement(target) {
|
|
7
|
+
return target && "closest" in target ? target.closest(rawInlineSelector) : null;
|
|
8
|
+
}
|
|
9
|
+
function selectedRawInlineAtom(editor) {
|
|
10
|
+
const selection = editor.state.selection;
|
|
11
|
+
if (!(selection instanceof NodeSelection) || selection.node.type.name !== "rawInline") return null;
|
|
12
|
+
return rawInlineElement(editor.view.nodeDOM(selection.from));
|
|
13
|
+
}
|
|
14
|
+
function readRawInlineAtom(atom) {
|
|
15
|
+
return {
|
|
16
|
+
source: atom.getAttribute("data-raw-source") ?? "",
|
|
17
|
+
display: atom.getAttribute("data-raw-display") ?? ""
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
function directionContext(atom) {
|
|
21
|
+
let dir;
|
|
22
|
+
let lang;
|
|
23
|
+
for (let element = atom; element && (!dir || !lang);) {
|
|
24
|
+
const declaredDir = element.getAttribute("dir");
|
|
25
|
+
if (!dir && (declaredDir === "ltr" || declaredDir === "rtl")) dir = declaredDir;
|
|
26
|
+
if (!lang) lang = element.getAttribute("lang") || void 0;
|
|
27
|
+
const root = element.getRootNode();
|
|
28
|
+
element = element.parentElement ?? (root && "host" in root ? root.host : null);
|
|
29
|
+
}
|
|
30
|
+
return {
|
|
31
|
+
dir,
|
|
32
|
+
lang
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
function labelRawInline(atom, labels) {
|
|
36
|
+
const { source, display } = readRawInlineAtom(atom);
|
|
37
|
+
const { dir, lang } = directionContext(atom);
|
|
38
|
+
const isolate = resolveDirection(dir, lang, source) === "rtl" ? "" : "";
|
|
39
|
+
atom.setAttribute("role", "img");
|
|
40
|
+
atom.setAttribute("aria-label", labels.rawInline(display, `${isolate}${source}\u2069`));
|
|
41
|
+
}
|
|
42
|
+
function detail(atom, via) {
|
|
43
|
+
const { x, y, width, height } = atom.getBoundingClientRect();
|
|
44
|
+
const { source, display } = readRawInlineAtom(atom);
|
|
45
|
+
return {
|
|
46
|
+
source,
|
|
47
|
+
display,
|
|
48
|
+
rect: {
|
|
49
|
+
x,
|
|
50
|
+
y,
|
|
51
|
+
width,
|
|
52
|
+
height
|
|
53
|
+
},
|
|
54
|
+
via
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
var RawInlineIndicator = class {
|
|
58
|
+
dispatch;
|
|
59
|
+
pointer = null;
|
|
60
|
+
selection = null;
|
|
61
|
+
key = "null";
|
|
62
|
+
constructor(dispatch) {
|
|
63
|
+
this.dispatch = dispatch;
|
|
64
|
+
}
|
|
65
|
+
pointerEnter(atom) {
|
|
66
|
+
this.pointer = atom;
|
|
67
|
+
this.emit();
|
|
68
|
+
}
|
|
69
|
+
pointerLeave(atom) {
|
|
70
|
+
if (this.pointer === atom) this.pointer = null;
|
|
71
|
+
this.emit();
|
|
72
|
+
}
|
|
73
|
+
selectionSelect(atom) {
|
|
74
|
+
this.selection = atom;
|
|
75
|
+
this.emit();
|
|
76
|
+
}
|
|
77
|
+
end() {
|
|
78
|
+
this.pointer = null;
|
|
79
|
+
this.selection = null;
|
|
80
|
+
this.emit();
|
|
81
|
+
}
|
|
82
|
+
emit() {
|
|
83
|
+
const next = this.selection ? detail(this.selection, "selection") : this.pointer ? detail(this.pointer, "pointer") : null;
|
|
84
|
+
const key = JSON.stringify(next);
|
|
85
|
+
if (key === this.key) return;
|
|
86
|
+
this.key = key;
|
|
87
|
+
this.dispatch(next);
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
//#endregion
|
|
91
|
+
export { RawInlineIndicator, labelRawInline, rawInlineElement, readRawInlineAtom, selectedRawInlineAtom };
|
|
@@ -1,6 +1,7 @@
|
|
|
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
|
+
import { RAW_INLINE_ATTRIBUTE, RAW_INLINE_DISPLAY_ATTRIBUTE, RAW_INLINE_SOURCE_ATTRIBUTE, RawInline } from "./markdown-rich-raw-inline.js";
|
|
4
|
+
import { labelRawInline, selectedRawInlineAtom } from "./markdown-raw-inline-indication.js";
|
|
4
5
|
import { richExtensions } from "./markdown-rich-extensions.js";
|
|
5
6
|
import { safeMarkdownUrl } from "@segnavia/html";
|
|
6
7
|
import { HEADLESS_ELEMENTS, SegnaviaPage, createPageElement } from "@segnavia/frame";
|
|
@@ -38,24 +39,31 @@ function lockedView(options) {
|
|
|
38
39
|
};
|
|
39
40
|
} });
|
|
40
41
|
}
|
|
41
|
-
|
|
42
|
-
return ({
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
42
|
+
function rawInlineView(options) {
|
|
43
|
+
return RawInline.extend({ addNodeView() {
|
|
44
|
+
return ({ node }) => {
|
|
45
|
+
const dom = document.createElement("span");
|
|
46
|
+
dom.part.add("raw-inline");
|
|
47
|
+
dom.setAttribute(RAW_INLINE_ATTRIBUTE, "");
|
|
48
|
+
dom.setAttribute(RAW_INLINE_SOURCE_ATTRIBUTE, String(node.attrs.source ?? ""));
|
|
49
|
+
dom.setAttribute(RAW_INLINE_DISPLAY_ATTRIBUTE, String(node.attrs.display ?? ""));
|
|
50
|
+
dom.dir = "auto";
|
|
51
|
+
dom.contentEditable = "false";
|
|
52
|
+
dom.textContent = String(node.attrs.display ?? "");
|
|
53
|
+
labelRawInline(dom, options().labels());
|
|
54
|
+
dom.addEventListener("pointerenter", () => options().indicator.pointerEnter(dom));
|
|
55
|
+
dom.addEventListener("pointerleave", () => options().indicator.pointerLeave(dom));
|
|
56
|
+
return {
|
|
57
|
+
dom,
|
|
58
|
+
destroy: () => options().indicator.pointerLeave(dom)
|
|
59
|
+
};
|
|
60
|
+
};
|
|
61
|
+
} });
|
|
62
|
+
}
|
|
55
63
|
function extensions(options, keyboard) {
|
|
56
64
|
return [...richExtensions().map((extension) => {
|
|
57
65
|
if (extension.name === "lockedBlock") return lockedView(options);
|
|
58
|
-
return extension.name === "rawInline" ? rawInlineView : extension;
|
|
66
|
+
return extension.name === "rawInline" ? rawInlineView(options) : extension;
|
|
59
67
|
}), richKeyboard(keyboard)];
|
|
60
68
|
}
|
|
61
69
|
function pastedHtml(html, linkOrigins) {
|
|
@@ -87,8 +95,15 @@ function mountRichEditor(options) {
|
|
|
87
95
|
transformPastedHTML: (html) => pastedHtml(html, options.linkOrigins())
|
|
88
96
|
},
|
|
89
97
|
onUpdate: ({ editor: instance }) => options.change(Array.from(instance.state.doc.content.content)),
|
|
90
|
-
onTransaction: ({ editor: instance }) =>
|
|
91
|
-
|
|
98
|
+
onTransaction: ({ editor: instance }) => {
|
|
99
|
+
options.state(richEditorState(instance));
|
|
100
|
+
options.indicator.selectionSelect(selectedRawInlineAtom(instance));
|
|
101
|
+
},
|
|
102
|
+
onSelectionUpdate: ({ editor: instance }) => {
|
|
103
|
+
options.state(richEditorState(instance));
|
|
104
|
+
options.indicator.selectionSelect(selectedRawInlineAtom(instance));
|
|
105
|
+
},
|
|
106
|
+
onBlur: () => options.indicator.end()
|
|
92
107
|
});
|
|
93
108
|
options.state(richEditorState(editor));
|
|
94
109
|
return editor;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { LockedBlock } from "./markdown-rich-locked.js";
|
|
2
2
|
import { rawInline } from "./markdown-rich-raw-inline.js";
|
|
3
|
-
import {
|
|
3
|
+
import { listSourceAttributes, sourceFormStarterExtensions, sourceListAttrs, tableRowSourceAttributes, tableSourceExtension, taskItemSourceExtension } from "./markdown-rich-source-forms.js";
|
|
4
|
+
import { Extension, getExtensionField, getSchema } from "@tiptap/core";
|
|
4
5
|
import Image from "@tiptap/extension-image";
|
|
5
6
|
import { BulletList, OrderedList, TaskItem, TaskList } from "@tiptap/extension-list";
|
|
6
7
|
import { TableKit } from "@tiptap/extension-table";
|
|
@@ -75,14 +76,18 @@ const LooseBulletList = BulletList.extend({
|
|
|
75
76
|
addAttributes() {
|
|
76
77
|
return {
|
|
77
78
|
...this.parent?.(),
|
|
78
|
-
...listAttributes
|
|
79
|
+
...listAttributes,
|
|
80
|
+
...listSourceAttributes
|
|
79
81
|
};
|
|
80
82
|
},
|
|
81
83
|
parseMarkdown(token, helpers) {
|
|
82
84
|
if (token.type !== "list" || token.ordered) return [];
|
|
83
85
|
return {
|
|
84
86
|
type: "bulletList",
|
|
85
|
-
attrs: {
|
|
87
|
+
attrs: {
|
|
88
|
+
loose: listIsLoose(token.raw ?? ""),
|
|
89
|
+
...sourceListAttrs(token.raw ?? "")
|
|
90
|
+
},
|
|
86
91
|
content: helpers.parseChildren(token.items ?? [])
|
|
87
92
|
};
|
|
88
93
|
},
|
|
@@ -94,7 +99,8 @@ const LooseOrderedList = OrderedList.extend({
|
|
|
94
99
|
addAttributes() {
|
|
95
100
|
return {
|
|
96
101
|
...this.parent?.(),
|
|
97
|
-
...listAttributes
|
|
102
|
+
...listAttributes,
|
|
103
|
+
...listSourceAttributes
|
|
98
104
|
};
|
|
99
105
|
},
|
|
100
106
|
parseMarkdown(token, helpers) {
|
|
@@ -104,24 +110,39 @@ const LooseOrderedList = OrderedList.extend({
|
|
|
104
110
|
attrs: {
|
|
105
111
|
start: token.start || 1,
|
|
106
112
|
type: token.typeMarker ?? null,
|
|
107
|
-
loose: listIsLoose(token.raw ?? "")
|
|
113
|
+
loose: listIsLoose(token.raw ?? ""),
|
|
114
|
+
...sourceListAttrs(token.raw ?? "", true)
|
|
108
115
|
},
|
|
109
116
|
content: helpers.parseChildren(token.items ?? [])
|
|
110
117
|
};
|
|
111
118
|
},
|
|
112
119
|
renderMarkdown(node, helpers) {
|
|
113
|
-
|
|
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));
|
|
114
131
|
}
|
|
115
132
|
});
|
|
116
133
|
const LooseTaskList = TaskList.extend({
|
|
117
134
|
addAttributes() {
|
|
118
135
|
return {
|
|
119
136
|
...this.parent?.(),
|
|
120
|
-
...listAttributes
|
|
137
|
+
...listAttributes,
|
|
138
|
+
...listSourceAttributes
|
|
121
139
|
};
|
|
122
140
|
},
|
|
123
141
|
parseMarkdown(token, helpers) {
|
|
124
|
-
return helpers.createNode("taskList", {
|
|
142
|
+
return helpers.createNode("taskList", {
|
|
143
|
+
loose: listIsLoose(token.raw ?? ""),
|
|
144
|
+
...sourceListAttrs(token.raw ?? "")
|
|
145
|
+
}, helpers.parseChildren(token.items ?? []));
|
|
125
146
|
},
|
|
126
147
|
renderMarkdown(node, helpers) {
|
|
127
148
|
return node.content ? helpers.renderChildren(node.content, listSeparator(node)) : "";
|
|
@@ -179,18 +200,38 @@ const directionalBlocks = Extension.create({
|
|
|
179
200
|
}
|
|
180
201
|
});
|
|
181
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
|
+
})?.() ?? [];
|
|
182
220
|
return [
|
|
183
|
-
|
|
184
|
-
trailingNode: false,
|
|
185
|
-
link: { openOnClick: false },
|
|
186
|
-
bulletList: false,
|
|
187
|
-
orderedList: false
|
|
188
|
-
}),
|
|
221
|
+
...sourceFormStarterExtensions(starterChildren),
|
|
189
222
|
LooseBulletList,
|
|
190
223
|
LooseOrderedList,
|
|
191
|
-
|
|
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);
|
|
232
|
+
}),
|
|
192
233
|
LooseTaskList,
|
|
193
|
-
TaskItem.configure({ nested: true }),
|
|
234
|
+
taskItemSourceExtension(TaskItem.configure({ nested: true })),
|
|
194
235
|
Image.configure({ inline: true }),
|
|
195
236
|
directionalBlocks,
|
|
196
237
|
LockedBlock,
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { Node } from "@tiptap/core";
|
|
2
2
|
//#region src/markdown-rich-raw-inline.ts
|
|
3
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";
|
|
4
7
|
const ASCII_PUNCTUATION = String.raw`[!-/:-@\[-` + "{-~]";
|
|
5
8
|
const escapePattern = new RegExp(`^\\\\${ASCII_PUNCTUATION}`);
|
|
6
9
|
const entityPattern = /^&(?:[A-Za-z][A-Za-z0-9]+|#[0-9]+|#[xX][0-9A-Fa-f]+);/;
|
|
@@ -61,22 +64,22 @@ function rawInline(labels = /* @__PURE__ */ new Set()) {
|
|
|
61
64
|
source: {
|
|
62
65
|
default: "",
|
|
63
66
|
parseHTML: (element) => element.getAttribute("data-raw-source") ?? "",
|
|
64
|
-
renderHTML: (attrs) => ({
|
|
67
|
+
renderHTML: (attrs) => ({ [RAW_INLINE_SOURCE_ATTRIBUTE]: attrs.source })
|
|
65
68
|
},
|
|
66
69
|
display: {
|
|
67
70
|
default: "",
|
|
68
71
|
parseHTML: (element) => element.getAttribute("data-raw-display") ?? "",
|
|
69
|
-
renderHTML: (attrs) => ({
|
|
72
|
+
renderHTML: (attrs) => ({ [RAW_INLINE_DISPLAY_ATTRIBUTE]: attrs.display })
|
|
70
73
|
}
|
|
71
74
|
};
|
|
72
75
|
},
|
|
73
76
|
parseHTML() {
|
|
74
|
-
return [{ tag:
|
|
77
|
+
return [{ tag: `span[${RAW_INLINE_ATTRIBUTE}]` }];
|
|
75
78
|
},
|
|
76
79
|
renderHTML({ HTMLAttributes }) {
|
|
77
80
|
return ["span", {
|
|
78
81
|
...HTMLAttributes,
|
|
79
|
-
|
|
82
|
+
[RAW_INLINE_ATTRIBUTE]: "",
|
|
80
83
|
dir: "auto"
|
|
81
84
|
}];
|
|
82
85
|
},
|
|
@@ -96,7 +99,11 @@ function rawInline(labels = /* @__PURE__ */ new Set()) {
|
|
|
96
99
|
name: RAW_INLINE_NAME,
|
|
97
100
|
level: "inline",
|
|
98
101
|
start(src) {
|
|
99
|
-
|
|
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
|
+
}
|
|
100
107
|
return -1;
|
|
101
108
|
},
|
|
102
109
|
tokenize(src) {
|
|
@@ -113,4 +120,4 @@ function rawInline(labels = /* @__PURE__ */ new Set()) {
|
|
|
113
120
|
}
|
|
114
121
|
const RawInline = rawInline();
|
|
115
122
|
//#endregion
|
|
116
|
-
export { RAW_INLINE_NAME, RawInline, normalizeReferenceLabel, rawInline };
|
|
123
|
+
export { RAW_INLINE_ATTRIBUTE, RAW_INLINE_DISPLAY_ATTRIBUTE, RAW_INLINE_NAME, RAW_INLINE_SOURCE_ATTRIBUTE, RawInline, normalizeReferenceLabel, rawInline };
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { isLockedToken, isSilentToken, lockedFenceLabel, lockedNode } from "./markdown-rich-locked.js";
|
|
2
2
|
import { normalizeReferenceLabel } from "./markdown-rich-raw-inline.js";
|
|
3
|
+
import { meaningfulAttributes, withoutRecordedSourceAttrs } from "./markdown-rich-source-attrs.js";
|
|
3
4
|
import { richMarkdownManager, richSchema, scopedRichMarkdownManager } from "./markdown-rich-extensions.js";
|
|
4
5
|
import { Node } from "@tiptap/pm/model";
|
|
5
6
|
//#region src/markdown-rich-save.ts
|
|
@@ -46,14 +47,16 @@ function parseBlock(manager, token, raw, lockedFences) {
|
|
|
46
47
|
const parsed = manager.parse(raw);
|
|
47
48
|
return Node.fromJSON(richSchema(), parsed).toJSON().content ?? [];
|
|
48
49
|
}
|
|
49
|
-
function
|
|
50
|
-
const out = [];
|
|
51
|
-
const initialTokens = richMarkdownManager().instance.lexer(markdown);
|
|
50
|
+
function definitionLabels(tokens) {
|
|
52
51
|
const labels = /* @__PURE__ */ new Set();
|
|
53
|
-
for (const token of
|
|
52
|
+
for (const token of tokens) {
|
|
54
53
|
const definition = token.type === "def" ? /^ {0,3}\[([^\]]+)\]:/.exec(token.raw)?.[1] : void 0;
|
|
55
54
|
if (definition !== void 0) labels.add(normalizeReferenceLabel(definition));
|
|
56
55
|
}
|
|
56
|
+
return labels;
|
|
57
|
+
}
|
|
58
|
+
function blocks(markdown, labels, lockedFences = {}) {
|
|
59
|
+
const out = [];
|
|
57
60
|
const manager = scopedRichMarkdownManager(labels);
|
|
58
61
|
const tokens = manager.instance.lexer(markdown);
|
|
59
62
|
let at = 0;
|
|
@@ -81,7 +84,9 @@ function originalEntry(original, options) {
|
|
|
81
84
|
const nextFencesKey = fencesKey(options.lockedFences);
|
|
82
85
|
if (cachedOriginal?.original === original && cachedOriginal.fencesKey === nextFencesKey && cachedOriginal.bodyTransform === options.bodyTransform) return cachedOriginal;
|
|
83
86
|
const split = options.bodyTransform?.split(original);
|
|
84
|
-
const
|
|
87
|
+
const editable = split?.editable ?? original;
|
|
88
|
+
const labels = definitionLabels(richMarkdownManager().instance.lexer(editable));
|
|
89
|
+
const editableBlocks = blocks(editable, labels, options.lockedFences);
|
|
85
90
|
const nodes = editableBlocks.flatMap((block) => block.nodes);
|
|
86
91
|
const keys = editableBlocks.flatMap((block) => block.keys);
|
|
87
92
|
const indicesByFirstKey = /* @__PURE__ */ new Map();
|
|
@@ -100,7 +105,8 @@ function originalEntry(original, options) {
|
|
|
100
105
|
blocks: editableBlocks,
|
|
101
106
|
nodes,
|
|
102
107
|
keys,
|
|
103
|
-
indicesByFirstKey
|
|
108
|
+
indicesByFirstKey,
|
|
109
|
+
definitionLabels: labels
|
|
104
110
|
};
|
|
105
111
|
return cachedOriginal;
|
|
106
112
|
}
|
|
@@ -114,23 +120,115 @@ function firstAtOrAfter(indices, minimum) {
|
|
|
114
120
|
}
|
|
115
121
|
return indices[low] ?? -1;
|
|
116
122
|
}
|
|
117
|
-
function
|
|
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) {
|
|
118
201
|
const json = nodes.map(asJson);
|
|
119
202
|
let marker = "raw-inline-amp";
|
|
120
203
|
const source = JSON.stringify(json);
|
|
121
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, "&");
|
|
122
212
|
const protect = (node) => ({
|
|
123
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,
|
|
124
215
|
attrs: node.type === "rawInline" && typeof node.attrs?.source === "string" ? {
|
|
125
216
|
...node.attrs,
|
|
126
|
-
source: node.attrs.source.replaceAll("&", marker)
|
|
217
|
+
source: node.attrs.source.replaceAll("&", marker).replaceAll("\\[", openMarker).replaceAll("\\]", closeMarker)
|
|
127
218
|
} : node.attrs,
|
|
128
219
|
content: node.content?.map(protect)
|
|
129
220
|
});
|
|
130
|
-
|
|
221
|
+
const manager = scopedRichMarkdownManager(definitionLabels);
|
|
222
|
+
const render = (content) => restoreAmpersands(manager.serialize({
|
|
131
223
|
type: "doc",
|
|
132
|
-
content:
|
|
133
|
-
}).
|
|
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);
|
|
134
232
|
}
|
|
135
233
|
function matchingBlockIndex(entry, keys, keyIndex, blockIndex) {
|
|
136
234
|
const currentKey = keys[keyIndex];
|
|
@@ -153,7 +251,7 @@ function stitch(entry, edited, keys) {
|
|
|
153
251
|
const pending = [];
|
|
154
252
|
const emitNew = () => {
|
|
155
253
|
if (!pending.length) return;
|
|
156
|
-
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);
|
|
157
255
|
last = -1;
|
|
158
256
|
};
|
|
159
257
|
const emitOld = (hit) => {
|
|
@@ -199,4 +297,4 @@ function saveRichDocument(original, edited, options = {}) {
|
|
|
199
297
|
return entry.split && options.bodyTransform ? options.bodyTransform.join(saved, entry.split.retained) : saved;
|
|
200
298
|
}
|
|
201
299
|
//#endregion
|
|
202
|
-
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 };
|
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
import { meaningfulAttributes } from "./markdown-rich-source-attrs.js";
|
|
2
|
+
import { renderNestedMarkdownContent } from "@tiptap/core";
|
|
3
|
+
//#region src/markdown-rich-source-forms.ts
|
|
4
|
+
const hidden = (defaultValue = null) => ({
|
|
5
|
+
default: defaultValue,
|
|
6
|
+
renderHTML: () => ({})
|
|
7
|
+
});
|
|
8
|
+
function withAttrs(value, attrs) {
|
|
9
|
+
if (!value || Array.isArray(value) || typeof value !== "object") return value;
|
|
10
|
+
const node = value;
|
|
11
|
+
return {
|
|
12
|
+
...node,
|
|
13
|
+
attrs: {
|
|
14
|
+
...node.attrs,
|
|
15
|
+
...attrs
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
function sourceFormAttrs(names) {
|
|
20
|
+
return Object.fromEntries(names.map((name) => [name, hidden()]));
|
|
21
|
+
}
|
|
22
|
+
function emphasisMarker(token) {
|
|
23
|
+
const first = token.raw?.[0];
|
|
24
|
+
return first === "_" || first === "*" ? first : null;
|
|
25
|
+
}
|
|
26
|
+
function titleQuote(raw) {
|
|
27
|
+
if (!raw) return null;
|
|
28
|
+
const match = /\s(["'(])(?=[\s\S]*(?:"|'|\))\)$)/g;
|
|
29
|
+
let quote = null;
|
|
30
|
+
for (const hit of raw.matchAll(match)) quote = hit[1] ?? null;
|
|
31
|
+
return quote;
|
|
32
|
+
}
|
|
33
|
+
function safeTitleQuote(value, quote) {
|
|
34
|
+
if (typeof value !== "string" || !value) return "";
|
|
35
|
+
if (quote === "'" && !value.includes("'")) return "'";
|
|
36
|
+
if (quote === "(" && !value.includes(")")) return "(";
|
|
37
|
+
return "\"";
|
|
38
|
+
}
|
|
39
|
+
function fenceCloseRegex(fence, flags = "") {
|
|
40
|
+
const character = fence[0] === "~" ? "~" : "`";
|
|
41
|
+
return new RegExp(`^ {0,3}${character}{${fence.length},}[ \\t]*$`, flags);
|
|
42
|
+
}
|
|
43
|
+
function fenceAttrs(token) {
|
|
44
|
+
const lines = (token.raw ?? "").replace(/\n$/, "").split("\n");
|
|
45
|
+
const opening = /^ {0,3}(`{3,}|~{3,})([^\n]*)/.exec(lines[0] ?? "");
|
|
46
|
+
if (!opening) return {};
|
|
47
|
+
const fence = opening[1] ?? "";
|
|
48
|
+
const suffix = opening[2] ?? "";
|
|
49
|
+
const language = token.lang ?? "";
|
|
50
|
+
const at = language ? suffix.indexOf(language) : -1;
|
|
51
|
+
const infoBefore = at >= 0 ? suffix.slice(0, at) : suffix;
|
|
52
|
+
const infoAfter = at >= 0 ? suffix.slice(at + language.length) : "";
|
|
53
|
+
return {
|
|
54
|
+
sourceFence: fence,
|
|
55
|
+
sourceClosingFence: fenceCloseRegex(fence).test(lines.at(-1) ?? "") ? lines.at(-1)?.trim() ?? fence : fence,
|
|
56
|
+
sourceInfoBefore: infoBefore,
|
|
57
|
+
sourceInfoAfter: infoAfter
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
function fenceIsSafe(content, fence) {
|
|
61
|
+
const character = fence[0];
|
|
62
|
+
if (character !== "`" && character !== "~" || fence.length < 3) return false;
|
|
63
|
+
return !fenceCloseRegex(fence, "m").test(content);
|
|
64
|
+
}
|
|
65
|
+
function longestFenceRun(content, character) {
|
|
66
|
+
let longest = 0;
|
|
67
|
+
const pattern = new RegExp(`^ {0,3}(${character === "~" ? "~" : "`"}+)`, "gm");
|
|
68
|
+
for (const match of content.matchAll(pattern)) longest = Math.max(longest, match[1]?.length ?? 0);
|
|
69
|
+
return longest;
|
|
70
|
+
}
|
|
71
|
+
function safeFallbackFence(content, recorded) {
|
|
72
|
+
const firstCharacter = recorded[0] === "~" ? "~" : "`";
|
|
73
|
+
for (const character of [firstCharacter, firstCharacter === "~" ? "`" : "~"]) {
|
|
74
|
+
const fence = character.repeat(Math.max(3, longestFenceRun(content, character) + 1));
|
|
75
|
+
if (fenceIsSafe(content, fence)) return fence;
|
|
76
|
+
}
|
|
77
|
+
throw new Error("Unable to choose a safe Markdown code fence.");
|
|
78
|
+
}
|
|
79
|
+
const stable = (value) => meaningfulAttributes(value, {
|
|
80
|
+
keepEmptyArrays: true,
|
|
81
|
+
omitDefaultTableSpans: true
|
|
82
|
+
});
|
|
83
|
+
const rowSignature = (row) => JSON.stringify(stable(row));
|
|
84
|
+
function rowCells(line) {
|
|
85
|
+
const pipes = [];
|
|
86
|
+
for (let index = 0; index < line.length; index++) {
|
|
87
|
+
if (line[index] !== "|") continue;
|
|
88
|
+
let escapes = 0;
|
|
89
|
+
for (let before = index - 1; before >= 0 && line[before] === "\\"; before--) escapes++;
|
|
90
|
+
if (escapes % 2 === 0) pipes.push(index);
|
|
91
|
+
}
|
|
92
|
+
const bounds = [
|
|
93
|
+
-1,
|
|
94
|
+
...pipes,
|
|
95
|
+
line.length
|
|
96
|
+
];
|
|
97
|
+
const cells = bounds.slice(0, -1).map((left, index) => ({
|
|
98
|
+
start: left + 1,
|
|
99
|
+
end: bounds[index + 1] ?? line.length
|
|
100
|
+
}));
|
|
101
|
+
if (cells[0] && !line.slice(cells[0].start, cells[0].end).trim()) cells.shift();
|
|
102
|
+
if (cells.at(-1) && !line.slice(cells.at(-1)?.start, cells.at(-1)?.end).trim()) cells.pop();
|
|
103
|
+
return cells;
|
|
104
|
+
}
|
|
105
|
+
function changedRow(source, normalized, signature, row) {
|
|
106
|
+
let old;
|
|
107
|
+
try {
|
|
108
|
+
old = JSON.parse(signature);
|
|
109
|
+
} catch {
|
|
110
|
+
return normalized;
|
|
111
|
+
}
|
|
112
|
+
const sourceCells = rowCells(source);
|
|
113
|
+
const normalizedCells = rowCells(normalized);
|
|
114
|
+
if (sourceCells.length !== row.content?.length || normalizedCells.length !== row.content.length) return normalized;
|
|
115
|
+
let output = source;
|
|
116
|
+
for (let index = sourceCells.length - 1; index >= 0; index--) {
|
|
117
|
+
if (JSON.stringify(stable(old.content?.[index])) === JSON.stringify(stable(row.content?.[index]))) continue;
|
|
118
|
+
const target = sourceCells[index];
|
|
119
|
+
const replacement = normalizedCells[index];
|
|
120
|
+
if (!target || !replacement) return normalized;
|
|
121
|
+
const original = source.slice(target.start, target.end);
|
|
122
|
+
const leading = /^\s*/.exec(original)?.[0] ?? "";
|
|
123
|
+
const trailing = /\s*$/.exec(original)?.[0] ?? "";
|
|
124
|
+
const content = normalized.slice(replacement.start, replacement.end).trim();
|
|
125
|
+
output = `${output.slice(0, target.start)}${leading}${content}${trailing}${output.slice(target.end)}`;
|
|
126
|
+
}
|
|
127
|
+
return output;
|
|
128
|
+
}
|
|
129
|
+
function tableLines(raw) {
|
|
130
|
+
const lines = raw.replace(/\n$/, "").split("\n");
|
|
131
|
+
const delimiter = lines[1] ?? "";
|
|
132
|
+
const columns = delimiter.replace(/^\s*\|/, "").replace(/\|\s*$/, "").split("|").length;
|
|
133
|
+
return {
|
|
134
|
+
rows: [lines[0] ?? "", ...lines.slice(2)],
|
|
135
|
+
delimiter,
|
|
136
|
+
columns
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
function extendInlineForm(extension) {
|
|
140
|
+
if (extension.name !== "italic" && extension.name !== "bold") return extension;
|
|
141
|
+
const attrName = extension.name === "italic" ? "sourceEmphasisDelimiter" : "sourceStrongDelimiter";
|
|
142
|
+
return extension.extend({
|
|
143
|
+
addAttributes() {
|
|
144
|
+
return {
|
|
145
|
+
...this.parent?.(),
|
|
146
|
+
...sourceFormAttrs([attrName])
|
|
147
|
+
};
|
|
148
|
+
},
|
|
149
|
+
parseMarkdown(token, helpers) {
|
|
150
|
+
const parsed = this.parent?.(token, helpers);
|
|
151
|
+
return withAttrs(parsed, { [attrName]: emphasisMarker(token) });
|
|
152
|
+
},
|
|
153
|
+
renderMarkdown(node, helpers) {
|
|
154
|
+
const count = extension.name === "bold" ? 2 : 1;
|
|
155
|
+
const recorded = node.attrs?.[attrName];
|
|
156
|
+
const marker = recorded === "_" || recorded === "*" ? recorded.repeat(count) : "*".repeat(count);
|
|
157
|
+
return `${marker}${helpers.renderChildren(node)}${marker}`;
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
function extendHeading(extension) {
|
|
162
|
+
if (extension.name !== "heading") return extension;
|
|
163
|
+
return extension.extend({
|
|
164
|
+
addAttributes() {
|
|
165
|
+
return {
|
|
166
|
+
...this.parent?.(),
|
|
167
|
+
...sourceFormAttrs(["sourceHeadingStyle", "sourceSetextUnderline"])
|
|
168
|
+
};
|
|
169
|
+
},
|
|
170
|
+
parseMarkdown(token, helpers) {
|
|
171
|
+
const parsed = this.parent?.(token, helpers);
|
|
172
|
+
const lines = token.raw?.replace(/\n$/, "").split("\n") ?? [];
|
|
173
|
+
const underline = lines.length > 1 && /^ {0,3}(?:=+|-+)[ \t]*$/.test(lines.at(-1) ?? "") ? lines.at(-1) : null;
|
|
174
|
+
return withAttrs(parsed, {
|
|
175
|
+
sourceHeadingStyle: underline ? "setext" : "atx",
|
|
176
|
+
sourceSetextUnderline: underline
|
|
177
|
+
});
|
|
178
|
+
},
|
|
179
|
+
renderMarkdown(node, helpers) {
|
|
180
|
+
const content = helpers.renderChildren(node.content ?? []);
|
|
181
|
+
if (node.attrs?.sourceHeadingStyle === "setext" && typeof node.attrs.sourceSetextUnderline === "string") return `${content}\n${node.attrs.sourceSetextUnderline}`;
|
|
182
|
+
const level = typeof node.attrs?.level === "number" ? node.attrs.level : 1;
|
|
183
|
+
return `${"#".repeat(level)} ${content}`;
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
function extendHardBreak(extension) {
|
|
188
|
+
if (extension.name !== "hardBreak") return extension;
|
|
189
|
+
return extension.extend({
|
|
190
|
+
addAttributes() {
|
|
191
|
+
return {
|
|
192
|
+
...this.parent?.(),
|
|
193
|
+
...sourceFormAttrs(["sourceHardBreak"])
|
|
194
|
+
};
|
|
195
|
+
},
|
|
196
|
+
parseMarkdown(token) {
|
|
197
|
+
return {
|
|
198
|
+
type: "hardBreak",
|
|
199
|
+
attrs: { sourceHardBreak: token.raw?.slice(0, -1) ?? null }
|
|
200
|
+
};
|
|
201
|
+
},
|
|
202
|
+
renderMarkdown(node) {
|
|
203
|
+
const form = node.attrs?.sourceHardBreak;
|
|
204
|
+
return `${form === "\\" || typeof form === "string" && /^ {2,}$/.test(form) ? form : " "}\n`;
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
function extendCodeBlock(extension) {
|
|
209
|
+
if (extension.name !== "codeBlock") return extension;
|
|
210
|
+
return extension.extend({
|
|
211
|
+
addAttributes() {
|
|
212
|
+
return {
|
|
213
|
+
...this.parent?.(),
|
|
214
|
+
...sourceFormAttrs([
|
|
215
|
+
"sourceFence",
|
|
216
|
+
"sourceClosingFence",
|
|
217
|
+
"sourceInfoBefore",
|
|
218
|
+
"sourceInfoAfter"
|
|
219
|
+
])
|
|
220
|
+
};
|
|
221
|
+
},
|
|
222
|
+
parseMarkdown(token, helpers) {
|
|
223
|
+
return withAttrs(this.parent?.(token, helpers), fenceAttrs(token));
|
|
224
|
+
},
|
|
225
|
+
renderMarkdown(node, helpers) {
|
|
226
|
+
const content = helpers.renderChildren(node.content ?? []);
|
|
227
|
+
const recorded = typeof node.attrs?.sourceFence === "string" ? node.attrs.sourceFence : "";
|
|
228
|
+
const fence = fenceIsSafe(content, recorded) ? recorded : safeFallbackFence(content, recorded);
|
|
229
|
+
const closing = fence === recorded && typeof node.attrs?.sourceClosingFence === "string" ? node.attrs.sourceClosingFence : fence;
|
|
230
|
+
const language = typeof node.attrs?.language === "string" ? node.attrs.language : "";
|
|
231
|
+
const before = fence === recorded && typeof node.attrs?.sourceInfoBefore === "string" ? node.attrs.sourceInfoBefore : "";
|
|
232
|
+
const after = fence === recorded && typeof node.attrs?.sourceInfoAfter === "string" ? node.attrs.sourceInfoAfter : "";
|
|
233
|
+
return `${fence}${language ? `${before}${language}${after}` : ""}\n${content}\n${closing}`;
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
function extendLink(extension) {
|
|
238
|
+
if (extension.name !== "link") return extension;
|
|
239
|
+
return extension.extend({
|
|
240
|
+
addAttributes() {
|
|
241
|
+
return {
|
|
242
|
+
...this.parent?.(),
|
|
243
|
+
...sourceFormAttrs(["sourceTitleQuote"])
|
|
244
|
+
};
|
|
245
|
+
},
|
|
246
|
+
parseMarkdown(token, helpers) {
|
|
247
|
+
return withAttrs(this.parent?.(token, helpers), { sourceTitleQuote: titleQuote(token.raw) });
|
|
248
|
+
},
|
|
249
|
+
renderMarkdown(node, helpers) {
|
|
250
|
+
const href = node.attrs?.href ?? "";
|
|
251
|
+
const title = node.attrs?.title;
|
|
252
|
+
const content = helpers.renderChildren(node);
|
|
253
|
+
if (typeof title !== "string" || !title) return `[${content}](${href})`;
|
|
254
|
+
const quote = safeTitleQuote(title, node.attrs?.sourceTitleQuote);
|
|
255
|
+
return `[${content}](${href} ${quote}${title}${quote === "(" ? ")" : quote})`;
|
|
256
|
+
}
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
function orderedMarker(attrs, index) {
|
|
260
|
+
const orderedDelimiter = attrs?.sourceOrderedDelimiter === ")" ? ")" : ".";
|
|
261
|
+
const written = attrs?.sourceNumber;
|
|
262
|
+
const start = typeof attrs?.start === "number" ? attrs.start : 1;
|
|
263
|
+
return `${attrs?.sourceKeepWrittenNumbers && typeof written === "string" && /^\d{1,9}$/.test(written) ? written : String(start + index)}${orderedDelimiter} `;
|
|
264
|
+
}
|
|
265
|
+
function extendListItem(extension) {
|
|
266
|
+
if (extension.name !== "listItem") return extension;
|
|
267
|
+
return extension.extend({
|
|
268
|
+
addAttributes() {
|
|
269
|
+
return {
|
|
270
|
+
...this.parent?.(),
|
|
271
|
+
...sourceFormAttrs(["sourceNumber"])
|
|
272
|
+
};
|
|
273
|
+
},
|
|
274
|
+
parseMarkdown(token, helpers) {
|
|
275
|
+
const parsed = this.parent?.(token, helpers);
|
|
276
|
+
return withAttrs(parsed, { sourceNumber: /^\s*(\d+)[.)]/.exec(token.raw ?? "")?.[1] ?? null });
|
|
277
|
+
},
|
|
278
|
+
renderMarkdown(node, helpers, context) {
|
|
279
|
+
return renderNestedMarkdownContent(node, helpers, () => {
|
|
280
|
+
if (context.parentType === "bulletList") return `${context.meta?.parentAttrs?.sourceBulletMarker ?? "-"} `;
|
|
281
|
+
if (context.parentType === "orderedList") return orderedMarker({
|
|
282
|
+
...context.meta?.parentAttrs,
|
|
283
|
+
...node.attrs
|
|
284
|
+
}, context.index ?? 0);
|
|
285
|
+
return "- ";
|
|
286
|
+
}, context, { alignNestedToPrefix: context.parentType === "orderedList" });
|
|
287
|
+
}
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
function sourceFormStarterExtensions(extensions) {
|
|
291
|
+
return extensions.map((extension) => {
|
|
292
|
+
return extendListItem(extendLink(extendCodeBlock(extendHardBreak(extendHeading(extendInlineForm(extension))))));
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
function sourceListAttrs(raw, ordered = false) {
|
|
296
|
+
const marker = /^\s*([-+*]|\d+([.)]))/.exec(raw);
|
|
297
|
+
if (!ordered) return { sourceBulletMarker: marker?.[1] && /^[-+*]$/.test(marker[1]) ? marker[1] : null };
|
|
298
|
+
const first = /^(\s*)(\d+)[.)](?:\s|$)/.exec(raw);
|
|
299
|
+
const sourceOrderedNumbers = raw.split("\n").map((line) => /^(\s*)(\d+)[.)](?:\s|$)/.exec(line)).filter((match) => match?.[1] === first?.[1]).map((match) => match?.[2]);
|
|
300
|
+
return {
|
|
301
|
+
sourceOrderedDelimiter: marker?.[2] ?? null,
|
|
302
|
+
sourceOrderedNumbers
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
const listSourceAttributes = {
|
|
306
|
+
sourceBulletMarker: hidden(),
|
|
307
|
+
sourceOrderedDelimiter: hidden(),
|
|
308
|
+
sourceOrderedNumbers: hidden()
|
|
309
|
+
};
|
|
310
|
+
function taskItemSourceExtension(extension) {
|
|
311
|
+
return extension.extend({ renderMarkdown(node, helpers, context) {
|
|
312
|
+
const checked = node.attrs?.checked ? "x" : " ";
|
|
313
|
+
const marker = context.meta?.parentAttrs?.sourceBulletMarker ?? "-";
|
|
314
|
+
return renderNestedMarkdownContent(node, helpers, `${marker} [${checked}] `);
|
|
315
|
+
} });
|
|
316
|
+
}
|
|
317
|
+
function tableSourceExtension(extension) {
|
|
318
|
+
if (extension.name !== "table") return extension;
|
|
319
|
+
return extension.extend({
|
|
320
|
+
addAttributes() {
|
|
321
|
+
return {
|
|
322
|
+
...this.parent?.(),
|
|
323
|
+
...sourceFormAttrs(["sourceDelimiter", "sourceColumns"])
|
|
324
|
+
};
|
|
325
|
+
},
|
|
326
|
+
parseMarkdown(token, helpers) {
|
|
327
|
+
const parsed = this.parent?.(token, helpers);
|
|
328
|
+
const source = tableLines(token.raw ?? "");
|
|
329
|
+
const content = parsed.content?.map((row, index) => ({
|
|
330
|
+
...row,
|
|
331
|
+
attrs: {
|
|
332
|
+
...row.attrs,
|
|
333
|
+
sourceRow: source.rows[index] ?? null,
|
|
334
|
+
sourceSignature: rowSignature(row)
|
|
335
|
+
}
|
|
336
|
+
}));
|
|
337
|
+
return {
|
|
338
|
+
...parsed,
|
|
339
|
+
attrs: {
|
|
340
|
+
...parsed.attrs,
|
|
341
|
+
sourceDelimiter: source.delimiter,
|
|
342
|
+
sourceColumns: source.columns
|
|
343
|
+
},
|
|
344
|
+
content
|
|
345
|
+
};
|
|
346
|
+
},
|
|
347
|
+
renderMarkdown(node, helpers, context) {
|
|
348
|
+
const lines = (this.parent?.(node, helpers, context) ?? "").trim().split("\n");
|
|
349
|
+
const rows = node.content ?? [];
|
|
350
|
+
const bodyLines = [lines[0] ?? "", ...lines.slice(2)];
|
|
351
|
+
for (const [index, row] of rows.entries()) if (typeof row.attrs?.sourceRow === "string" && row.attrs.sourceSignature === rowSignature(row)) bodyLines[index] = row.attrs.sourceRow;
|
|
352
|
+
else if (typeof row.attrs?.sourceRow === "string" && typeof row.attrs.sourceSignature === "string") bodyLines[index] = changedRow(row.attrs.sourceRow, bodyLines[index] ?? "", row.attrs.sourceSignature, row);
|
|
353
|
+
const columns = rows[0]?.content?.length ?? 0;
|
|
354
|
+
const delimiter = node.attrs?.sourceColumns === columns && typeof node.attrs.sourceDelimiter === "string" ? node.attrs.sourceDelimiter : lines[1] ?? "";
|
|
355
|
+
return [
|
|
356
|
+
bodyLines[0] ?? "",
|
|
357
|
+
delimiter,
|
|
358
|
+
...bodyLines.slice(1)
|
|
359
|
+
].join("\n");
|
|
360
|
+
}
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
const tableRowSourceAttributes = {
|
|
364
|
+
sourceRow: hidden(),
|
|
365
|
+
sourceSignature: hidden()
|
|
366
|
+
};
|
|
367
|
+
//#endregion
|
|
368
|
+
export { listSourceAttributes, sourceFormStarterExtensions, sourceListAttrs, tableRowSourceAttributes, tableSourceExtension, taskItemSourceExtension };
|
package/dist/vue.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { MarkdownEditorMode } from "./markdown-editor-policy.js";
|
|
2
2
|
import { MarkdownEditorStateDetail } from "./markdown-editor-types.js";
|
|
3
3
|
import { MARKDOWN_EDITOR_PROPERTIES } from "./element-properties.js";
|
|
4
|
+
import { RawInlineIndicationDetail } from "./markdown-raw-inline-indication.js";
|
|
4
5
|
import { SegnaviaMarkdownEditor } from "./markdown-editor.js";
|
|
5
6
|
import { EventProps } from "@segnavia/frame/vue-types";
|
|
6
7
|
import { DefineComponent } from "vue";
|
|
@@ -20,6 +21,7 @@ export interface MarkdownEditorVueEventDetails {
|
|
|
20
21
|
'segnavia-link-request': {
|
|
21
22
|
href: string | null;
|
|
22
23
|
};
|
|
24
|
+
'segnavia-raw-inline-indicate': RawInlineIndicationDetail | null;
|
|
23
25
|
}
|
|
24
26
|
type MarkdownEditorComponent = Partial<Pick<SegnaviaMarkdownEditor, (typeof MARKDOWN_EDITOR_PROPERTIES.properties)[number]>> & EventProps<typeof MARKDOWN_EDITOR_PROPERTIES.events, MarkdownEditorVueEventDetails>;
|
|
25
27
|
type MarkdownEditorGlobalComponents = { [Tag in typeof MARKDOWN_EDITOR_PROPERTIES.tag]: DefineComponent<MarkdownEditorComponent>; };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@segnavia/markdown-editor",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.47.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.47.0",
|
|
40
|
+
"@segnavia/frame": "0.47.0",
|
|
41
|
+
"@segnavia/html": "0.47.0"
|
|
42
42
|
},
|
|
43
43
|
"peerDependencies": {
|
|
44
44
|
"@codemirror/commands": "^6.11.1",
|