@segnavia/markdown-editor 0.45.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.
@@ -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,6 @@
1
+ //#region src/markdown-editor-labels.d.ts
2
+ export interface MarkdownEditorLabels {
3
+ rawInline: (display: string, source: string) => string;
4
+ }
5
+ export declare const ENGLISH_MARKDOWN_EDITOR_LABELS: MarkdownEditorLabels;
6
+ //#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 };
@@ -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, setModelTitle, setModelTitleSource } from "./markdown-editor-model.js";
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, markdownEditorKeyAction, richModeCommandRefusal } from "./markdown-editor-policy.js";
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 { RICH_COMMANDS, lockedBlockSourceOffset, richEditorState, selectedLockedBlock } from "./markdown-rich-commands.js";
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, redoDepth, undo, undoDepth } from "@codemirror/commands";
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.titleField.addEventListener("input", this.onTitleInput);
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.titleField.removeEventListener("input", this.onTitleInput);
70
- this.titleField.removeEventListener("keydown", this.onTitleKeyDown);
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.emitVisibleState();
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.emitState(state),
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
- onTitleInput = () => {
296
- const normalized = normalizeMarkdownTitle(this.titleField.value);
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
- const rawInlineView = RawInline.extend({ addNodeView() {
42
- return ({ node }) => {
43
- const dom = document.createElement("span");
44
- dom.part.add("raw-inline");
45
- dom.dataset.rawInline = "";
46
- dom.dataset.rawSource = String(node.attrs.source ?? "");
47
- dom.dataset.rawDisplay = String(node.attrs.display ?? "");
48
- dom.dir = "auto";
49
- dom.contentEditable = "false";
50
- dom.title = String(node.attrs.source ?? "");
51
- dom.textContent = String(node.attrs.display ?? "");
52
- return { dom };
53
- };
54
- } });
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 }) => options.state(richEditorState(instance)),
91
- onSelectionUpdate: ({ editor: instance }) => options.state(richEditorState(instance))
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 { Extension, getSchema } from "@tiptap/core";
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: { loose: listIsLoose(token.raw ?? "") },
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
- return node.content ? helpers.renderChildren(node.content, listSeparator(node)) : "";
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", { loose: listIsLoose(token.raw ?? "") }, helpers.parseChildren(token.items ?? []));
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
- StarterKit.configure({
184
- trailingNode: false,
185
- link: { openOnClick: false },
186
- bulletList: false,
187
- orderedList: false
188
- }),
221
+ ...sourceFormStarterExtensions(starterChildren),
189
222
  LooseBulletList,
190
223
  LooseOrderedList,
191
- TableKit,
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) => ({ "data-raw-source": attrs.source })
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) => ({ "data-raw-display": attrs.display })
72
+ renderHTML: (attrs) => ({ [RAW_INLINE_DISPLAY_ATTRIBUTE]: attrs.display })
70
73
  }
71
74
  };
72
75
  },
73
76
  parseHTML() {
74
- return [{ tag: "span[data-raw-inline]" }];
77
+ return [{ tag: `span[${RAW_INLINE_ATTRIBUTE}]` }];
75
78
  },
76
79
  renderHTML({ HTMLAttributes }) {
77
80
  return ["span", {
78
81
  ...HTMLAttributes,
79
- "data-raw-inline": "",
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
- for (let index = 0; index < src.length; index++) if (rawSource(src.slice(index), labels)) return index;
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 blocks(markdown, lockedFences = {}) {
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 initialTokens) {
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 editableBlocks = blocks(split?.editable ?? original, options.lockedFences);
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 serializeNodes(nodes) {
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
- return restoreAmpersands(richMarkdownManager().serialize({
221
+ const manager = scopedRichMarkdownManager(definitionLabels);
222
+ const render = (content) => restoreAmpersands(manager.serialize({
131
223
  type: "doc",
132
- content: json.map(protect)
133
- }).trim()).replaceAll(marker, "&");
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.45.0",
3
+ "version": "0.46.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.45.0",
40
- "@segnavia/frame": "0.45.0",
41
- "@segnavia/html": "0.45.0"
39
+ "@segnavia/format": "0.46.0",
40
+ "@segnavia/frame": "0.46.0",
41
+ "@segnavia/html": "0.46.0"
42
42
  },
43
43
  "peerDependencies": {
44
44
  "@codemirror/commands": "^6.11.1",