@segnavia/markdown-editor 0.44.0 → 0.46.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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.44.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.44.0",
40
- "@segnavia/frame": "0.44.0",
41
- "@segnavia/html": "0.44.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",
@@ -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
  }