@segnavia/markdown-editor 0.43.0 → 0.45.0

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