@threadlabs/looma 0.28.15 → 0.28.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/editor/chunks/extensions.js +897 -112
- package/editor/extensions/callout.d.ts +2 -9
- package/editor/extensions/chip.d.ts +3 -2
- package/editor/extensions/figure.d.ts +12 -0
- package/editor/extensions/index.d.ts +2 -0
- package/editor/extensions/index.js +2 -2
- package/editor/extensions/markdown.d.ts +8 -10
- package/editor/extensions/table-of-contents.d.ts +1 -1
- package/editor/extensions/tabs.d.ts +13 -0
- package/editor/index.js +2 -2
- package/markdown/fixtures.d.ts +27 -0
- package/markdown/fixtures.js +224 -0
- package/markdown/index.d.ts +109 -0
- package/markdown/index.js +232 -0
- package/markdown/markdown-it.d.ts +13 -0
- package/markdown/markdown-it.js +397 -0
- package/package.json +17 -1
- package/vue/editor/index.js +61 -19
|
@@ -8,8 +8,8 @@ import { dropCursor } from "@tiptap/pm/dropcursor";
|
|
|
8
8
|
import { gapCursor } from "@tiptap/pm/gapcursor";
|
|
9
9
|
import { createLowlight } from "lowlight";
|
|
10
10
|
import { Mapping } from "@tiptap/pm/transform";
|
|
11
|
-
import MarkdownIt from "markdown-it";
|
|
12
11
|
import { MarkdownSerializer, defaultMarkdownSerializer } from "@tiptap/pm/markdown";
|
|
12
|
+
import MarkdownIt from "markdown-it";
|
|
13
13
|
import { CellSelection, TableMap, addColumn, addColumnAfter, addColumnBefore, addRow, addRowAfter, addRowBefore, cellAround, columnResizing, deleteCellSelection, deleteColumn, deleteRow, deleteTable, fixTables, goToNextCell, mergeCells, moveTableColumn, moveTableRow, setCellAttr, splitCell, tableEditing, toggleHeader, toggleHeaderCell } from "@tiptap/pm/tables";
|
|
14
14
|
//#region src/editor/extensions/active-block.ts
|
|
15
15
|
/**
|
|
@@ -127,10 +127,10 @@ var LoomaActiveBlock = Extension.create({
|
|
|
127
127
|
}
|
|
128
128
|
});
|
|
129
129
|
//#endregion
|
|
130
|
-
//#region src/
|
|
130
|
+
//#region src/markdown/index.ts
|
|
131
131
|
/**
|
|
132
|
-
*
|
|
133
|
-
*
|
|
132
|
+
* Callout tones a document stores, in the order the editor offers them. The Callout component also
|
|
133
|
+
* takes `neutral` and `success`, which Markdown and the editor store as `note`.
|
|
134
134
|
*/
|
|
135
135
|
var LOOMA_CALLOUT_TONES = [
|
|
136
136
|
"note",
|
|
@@ -140,17 +140,202 @@ var LOOMA_CALLOUT_TONES = [
|
|
|
140
140
|
"warning",
|
|
141
141
|
"danger"
|
|
142
142
|
];
|
|
143
|
-
|
|
143
|
+
/** Names that read as a stored tone: `caution` is `danger`, and `neutral` is the same muted aside as `note`. */
|
|
144
|
+
var LOOMA_CALLOUT_TONE_ALIASES = {
|
|
144
145
|
caution: "danger",
|
|
145
146
|
neutral: "note"
|
|
146
147
|
};
|
|
147
|
-
/**
|
|
148
|
+
/**
|
|
149
|
+
* Maps a tone name or alias, in any case, onto a stored tone.
|
|
150
|
+
* @failure Unknown and non-string values become `note`.
|
|
151
|
+
*/
|
|
148
152
|
function normalizeLoomaCalloutTone(value) {
|
|
149
153
|
if (typeof value !== "string") return "note";
|
|
150
154
|
const tone = value.toLowerCase();
|
|
151
|
-
return LOOMA_CALLOUT_TONES.includes(tone) ? tone :
|
|
155
|
+
return LOOMA_CALLOUT_TONES.includes(tone) ? tone : LOOMA_CALLOUT_TONE_ALIASES[tone] ?? "note";
|
|
156
|
+
}
|
|
157
|
+
/** The hues chips and swatches take as their tone, written `:chip[Label]{tone=green}`. */
|
|
158
|
+
var LOOMA_CHIP_TONES = [
|
|
159
|
+
"neutral",
|
|
160
|
+
"blue",
|
|
161
|
+
"green",
|
|
162
|
+
"yellow",
|
|
163
|
+
"red",
|
|
164
|
+
"purple"
|
|
165
|
+
];
|
|
166
|
+
/**
|
|
167
|
+
* Keeps a chip tone within the palette.
|
|
168
|
+
* @failure Unknown values become `neutral`.
|
|
169
|
+
*/
|
|
170
|
+
function normalizeLoomaChipTone(value) {
|
|
171
|
+
return LOOMA_CHIP_TONES.includes(value) ? value : "neutral";
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Names that open a `:::` container, matched without regard to case. A container with any other
|
|
175
|
+
* name is ordinary text.
|
|
176
|
+
*/
|
|
177
|
+
var LOOMA_CONTAINER_NAMES = /* @__PURE__ */ new Set([
|
|
178
|
+
...LOOMA_CALLOUT_TONES,
|
|
179
|
+
"caution",
|
|
180
|
+
"details",
|
|
181
|
+
"figure",
|
|
182
|
+
"tabs",
|
|
183
|
+
"code-group"
|
|
184
|
+
]);
|
|
185
|
+
var ATTRIBUTE_KEY = /[A-Za-z][\w-]*/y;
|
|
186
|
+
var ATTRIBUTE_ID = /#([\w-]+)/y;
|
|
187
|
+
var ATTRIBUTE_VALUE = /[^\s"'=<>`{}]+/y;
|
|
188
|
+
function sticky(pattern, source, index) {
|
|
189
|
+
pattern.lastIndex = index;
|
|
190
|
+
return pattern.exec(source);
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Reads a brace attribute list starting at `source[start]`, which must be `{`.
|
|
194
|
+
* @contract Returns the attributes and the index just past the closing brace.
|
|
195
|
+
* @failure Returns null for anything that is not a complete, well-formed list, so the text stays literal.
|
|
196
|
+
*/
|
|
197
|
+
function readLoomaAttributes(source, start = 0) {
|
|
198
|
+
if (source[start] !== "{") return null;
|
|
199
|
+
const attributes = {};
|
|
200
|
+
let index = start + 1;
|
|
201
|
+
for (;;) {
|
|
202
|
+
while (source[index] === " " || source[index] === " ") index++;
|
|
203
|
+
if (source[index] === "}") return {
|
|
204
|
+
attributes,
|
|
205
|
+
end: index + 1
|
|
206
|
+
};
|
|
207
|
+
if (index > start + 1 && !/\s/.test(source[index - 1])) return null;
|
|
208
|
+
const id = sticky(ATTRIBUTE_ID, source, index);
|
|
209
|
+
if (id) {
|
|
210
|
+
attributes.id = id[1];
|
|
211
|
+
index += id[0].length;
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
const key = sticky(ATTRIBUTE_KEY, source, index);
|
|
215
|
+
if (!key) return null;
|
|
216
|
+
index += key[0].length;
|
|
217
|
+
if (source[index] !== "=") {
|
|
218
|
+
attributes[key[0]] = true;
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
index++;
|
|
222
|
+
const quote = source[index];
|
|
223
|
+
if (quote === "\"" || quote === "'") {
|
|
224
|
+
const close = source.indexOf(quote, index + 1);
|
|
225
|
+
if (close < 0) return null;
|
|
226
|
+
attributes[key[0]] = source.slice(index + 1, close);
|
|
227
|
+
index = close + 1;
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
const value = sticky(ATTRIBUTE_VALUE, source, index);
|
|
231
|
+
if (!value) return null;
|
|
232
|
+
attributes[key[0]] = value[0];
|
|
233
|
+
index += value[0].length;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* Reads a whole string as one brace attribute list.
|
|
238
|
+
* @failure Returns null unless the string is exactly one well-formed list.
|
|
239
|
+
*/
|
|
240
|
+
function parseLoomaAttributes(source) {
|
|
241
|
+
const read = readLoomaAttributes(source);
|
|
242
|
+
return read && read.end === source.length ? read.attributes : null;
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* Writes attributes as a brace list, `id` first as `#id`, then the rest in order.
|
|
246
|
+
* @contract `true` writes a bare key; values are quoted only when they need it. `false`, null and
|
|
247
|
+
* undefined values are left out, and an empty set writes an empty string. An ID that `#` cannot
|
|
248
|
+
* carry, or any ID when `hashId` is false, is written `id=…`.
|
|
249
|
+
*/
|
|
250
|
+
function formatLoomaAttributes(attributes, { hashId = true } = {}) {
|
|
251
|
+
const items = [];
|
|
252
|
+
const { id, ...rest } = attributes;
|
|
253
|
+
const hashed = hashId && typeof id === "string" && /^[\w-]+$/.test(id);
|
|
254
|
+
if (hashed) items.push(`#${id}`);
|
|
255
|
+
for (const [key, value] of Object.entries(hashed ? rest : attributes)) if (value === true) items.push(key);
|
|
256
|
+
else if (value === false || value === null || value === void 0) continue;
|
|
257
|
+
else {
|
|
258
|
+
const text = String(value);
|
|
259
|
+
items.push(/^[^\s"'=<>`{}]+$/.test(text) ? `${key}=${text}` : text.includes("\"") ? `${key}='${text}'` : `${key}="${text}"`);
|
|
260
|
+
}
|
|
261
|
+
return items.length ? `{${items.join(" ")}}` : "";
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* The anchor a heading's text derives: NFC, lowercase, keeping letters, numbers and combining
|
|
265
|
+
* marks, with every other run of characters one hyphen.
|
|
266
|
+
* @failure Text with no letters or numbers, such as only emoji, derives `heading`.
|
|
267
|
+
*/
|
|
268
|
+
function loomaHeadingSlug(text) {
|
|
269
|
+
return text.normalize("NFC").toLowerCase().replace(/[^\p{L}\p{N}\p{M}]+/gu, "-").replace(/^-+|-+$/g, "") || "heading";
|
|
152
270
|
}
|
|
153
271
|
/**
|
|
272
|
+
* Gives every heading of a document its anchor, in document order.
|
|
273
|
+
* @contract A custom ID is kept unless an earlier heading already has it. Any other heading takes
|
|
274
|
+
* its slug, or the slug's first free suffix (`-1`, `-2`…), never one a later custom ID reserves.
|
|
275
|
+
*/
|
|
276
|
+
function assignLoomaHeadingIds(headings) {
|
|
277
|
+
const reserved = new Set(headings.map(({ id }) => id).filter((id) => Boolean(id)));
|
|
278
|
+
const used = /* @__PURE__ */ new Set();
|
|
279
|
+
return headings.map(({ text, id: custom }) => {
|
|
280
|
+
let id = custom || loomaHeadingSlug(text);
|
|
281
|
+
if (custom ? used.has(id) : used.has(id) || reserved.has(id)) {
|
|
282
|
+
const stem = id;
|
|
283
|
+
let suffix = 1;
|
|
284
|
+
while (used.has(`${stem}-${suffix}`) || reserved.has(`${stem}-${suffix}`)) suffix++;
|
|
285
|
+
id = `${stem}-${suffix}`;
|
|
286
|
+
}
|
|
287
|
+
used.add(id);
|
|
288
|
+
return id;
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
/** Reads a fence's info string: ```` ```ts title="a.ts" {1,3-5} :line-numbers ````. */
|
|
292
|
+
function parseLoomaFenceInfo(info) {
|
|
293
|
+
const source = info.trim();
|
|
294
|
+
const language = /^[^\s={:]+(?=[\s{:]|$)/.exec(source)?.[0];
|
|
295
|
+
const meta = (language ? source.slice(language.length) : source).trim();
|
|
296
|
+
const title = /(?:^|\s)title="([^"]*)"/.exec(meta)?.[1];
|
|
297
|
+
const rest = meta.replace(/(?:^|\s)title="[^"]*"/, "");
|
|
298
|
+
const lines = /* @__PURE__ */ new Set();
|
|
299
|
+
for (const range of /\{([\d\s,-]+)\}/.exec(rest)?.[1].split(",") ?? []) {
|
|
300
|
+
const [from = NaN, to = from] = range.split("-").map((part) => Number.parseInt(part, 10));
|
|
301
|
+
for (let line = from; line <= to; line++) lines.add(line);
|
|
302
|
+
}
|
|
303
|
+
const numbers = /:(no-)?line-numbers(?![\w-])/.exec(rest);
|
|
304
|
+
return {
|
|
305
|
+
...language ? { language } : {},
|
|
306
|
+
meta,
|
|
307
|
+
...title !== void 0 ? { title } : {},
|
|
308
|
+
lines: [...lines].sort((a, b) => a - b),
|
|
309
|
+
...numbers ? { lineNumbers: numbers[1] === void 0 } : {}
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
/**
|
|
313
|
+
* Splits YAML front matter from the start of a document.
|
|
314
|
+
* @contract Front matter opens with a `---` line at the very start, holds at least one `key: value`
|
|
315
|
+
* line, and closes at a `---` or `...` line. `source` is the block as written, delimiters included.
|
|
316
|
+
* @failure Returns null when the document has none.
|
|
317
|
+
*/
|
|
318
|
+
function splitLoomaFrontmatter(document) {
|
|
319
|
+
const match = /^---[ \t]*\r?\n([\s\S]*?)\r?\n(?:---|\.\.\.)[ \t]*(?:\r?\n|$)/.exec(document.slice(0, 8192));
|
|
320
|
+
if (!match || !/^[A-Za-z][\w-]*:[ \t]*\S/m.test(match[1])) return null;
|
|
321
|
+
return {
|
|
322
|
+
source: match[0].trimEnd(),
|
|
323
|
+
yaml: match[1],
|
|
324
|
+
body: document.slice(match[0].length)
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
/** The colons a container opens and closes with, given how deeply containers nest inside it. */
|
|
328
|
+
function loomaContainerMarker(nestedDepth) {
|
|
329
|
+
return ":".repeat(3 + Math.max(0, nestedDepth));
|
|
330
|
+
}
|
|
331
|
+
/** A backtick fence longer than any run of backticks in the code, and at least three long. */
|
|
332
|
+
function loomaCodeFence(code) {
|
|
333
|
+
const longest = Math.max(0, ...(code.match(/`+/g) ?? []).map((run) => run.length));
|
|
334
|
+
return "`".repeat(Math.max(3, longest + 1));
|
|
335
|
+
}
|
|
336
|
+
//#endregion
|
|
337
|
+
//#region src/editor/extensions/callout.ts
|
|
338
|
+
/**
|
|
154
339
|
* A durable block container for informational editor content, saved as Looma's Callout.
|
|
155
340
|
*
|
|
156
341
|
* @invariant Rendered `tone` values always belong to `LOOMA_CALLOUT_TONES`; missing or
|
|
@@ -196,19 +381,12 @@ var LoomaCallout = Node$1.create({
|
|
|
196
381
|
});
|
|
197
382
|
//#endregion
|
|
198
383
|
//#region src/editor/extensions/chip.ts
|
|
199
|
-
/** The six palette values a chip may persist; other values fall back to neutral. */
|
|
200
|
-
var LOOMA_CHIP_COLORS =
|
|
201
|
-
"neutral",
|
|
202
|
-
"blue",
|
|
203
|
-
"green",
|
|
204
|
-
"yellow",
|
|
205
|
-
"red",
|
|
206
|
-
"purple"
|
|
207
|
-
];
|
|
384
|
+
/** The six palette values a chip may persist, Looma Markdown's chip tones; other values fall back to neutral. */
|
|
385
|
+
var LOOMA_CHIP_COLORS = LOOMA_CHIP_TONES;
|
|
208
386
|
var CHIP_PLACEHOLDER = "Set a label";
|
|
209
387
|
/** Keep imported or programmatic colors within the supported palette. */
|
|
210
388
|
function normalizeLoomaChipColor(value) {
|
|
211
|
-
return
|
|
389
|
+
return normalizeLoomaChipTone(value);
|
|
212
390
|
}
|
|
213
391
|
/**
|
|
214
392
|
* An inline, atomic label. The editor's popover edits its attributes; its text
|
|
@@ -466,6 +644,144 @@ var LoomaExpand = Node$1.create({
|
|
|
466
644
|
}
|
|
467
645
|
});
|
|
468
646
|
//#endregion
|
|
647
|
+
//#region src/editor/extensions/figure.ts
|
|
648
|
+
/**
|
|
649
|
+
* An image with a caption, saved as `<figure><img><figcaption>` and written in Looma Markdown as
|
|
650
|
+
* `::: figure Caption`. The caption is inline content the author edits in place.
|
|
651
|
+
* @invariant A figure holds exactly one block image and one caption; an empty caption stays empty.
|
|
652
|
+
*/
|
|
653
|
+
var LoomaFigure = Node$1.create({
|
|
654
|
+
name: "loomaFigure",
|
|
655
|
+
group: "block",
|
|
656
|
+
content: "image loomaFigcaption",
|
|
657
|
+
defining: true,
|
|
658
|
+
parseHTML() {
|
|
659
|
+
return [{
|
|
660
|
+
tag: "figure",
|
|
661
|
+
getAttrs: (element) => element.querySelector(":scope > img") ? null : false
|
|
662
|
+
}];
|
|
663
|
+
},
|
|
664
|
+
renderHTML() {
|
|
665
|
+
return ["figure", 0];
|
|
666
|
+
}
|
|
667
|
+
});
|
|
668
|
+
/**
|
|
669
|
+
* A figure's caption: inline content under its image.
|
|
670
|
+
* @invariant It exists only as the second child of `LoomaFigure`.
|
|
671
|
+
*/
|
|
672
|
+
var LoomaFigcaption = Node$1.create({
|
|
673
|
+
name: "loomaFigcaption",
|
|
674
|
+
content: "inline*",
|
|
675
|
+
defining: true,
|
|
676
|
+
parseHTML() {
|
|
677
|
+
return [{ tag: "figcaption" }];
|
|
678
|
+
},
|
|
679
|
+
renderHTML() {
|
|
680
|
+
return ["figcaption", 0];
|
|
681
|
+
}
|
|
682
|
+
});
|
|
683
|
+
//#endregion
|
|
684
|
+
//#region src/editor/extensions/tabs.ts
|
|
685
|
+
/**
|
|
686
|
+
* Tabs or a code group, kept whole so Looma Markdown's `::: tabs` and `::: code-group` survive
|
|
687
|
+
* editing. Saved as `<ui-tabs group>` with a `<section aria-label>` per panel.
|
|
688
|
+
* @contract While editing, panels show one after another under their labels; the Tabs component
|
|
689
|
+
* draws them only where the saved HTML renders.
|
|
690
|
+
*/
|
|
691
|
+
var LoomaTabs = Node$1.create({
|
|
692
|
+
name: "loomaTabs",
|
|
693
|
+
group: "block",
|
|
694
|
+
content: "loomaTab+",
|
|
695
|
+
defining: true,
|
|
696
|
+
isolating: true,
|
|
697
|
+
addAttributes() {
|
|
698
|
+
return {
|
|
699
|
+
group: {
|
|
700
|
+
default: "",
|
|
701
|
+
parseHTML: (element) => element.getAttribute("group") ?? "",
|
|
702
|
+
renderHTML: (attributes) => attributes.group ? { group: attributes.group } : {}
|
|
703
|
+
},
|
|
704
|
+
codeGroup: {
|
|
705
|
+
default: false,
|
|
706
|
+
parseHTML: (element) => element.classList.contains("code-group"),
|
|
707
|
+
renderHTML: (attributes) => attributes.codeGroup ? { class: "code-group" } : {}
|
|
708
|
+
}
|
|
709
|
+
};
|
|
710
|
+
},
|
|
711
|
+
parseHTML() {
|
|
712
|
+
return [{ tag: "ui-tabs" }];
|
|
713
|
+
},
|
|
714
|
+
renderHTML({ HTMLAttributes }) {
|
|
715
|
+
return [
|
|
716
|
+
"ui-tabs",
|
|
717
|
+
HTMLAttributes,
|
|
718
|
+
0
|
|
719
|
+
];
|
|
720
|
+
},
|
|
721
|
+
addNodeView() {
|
|
722
|
+
return () => {
|
|
723
|
+
const dom = document.createElement("div");
|
|
724
|
+
dom.dataset.loomaTabs = "";
|
|
725
|
+
return {
|
|
726
|
+
dom,
|
|
727
|
+
contentDOM: dom
|
|
728
|
+
};
|
|
729
|
+
};
|
|
730
|
+
}
|
|
731
|
+
});
|
|
732
|
+
/**
|
|
733
|
+
* One panel of `LoomaTabs`, labelled by `label`.
|
|
734
|
+
* @invariant It exists only inside `LoomaTabs`; its label is the tab Tabs shows and the `== Label` line Markdown writes.
|
|
735
|
+
*/
|
|
736
|
+
var LoomaTab = Node$1.create({
|
|
737
|
+
name: "loomaTab",
|
|
738
|
+
content: "block+",
|
|
739
|
+
defining: true,
|
|
740
|
+
isolating: true,
|
|
741
|
+
addAttributes() {
|
|
742
|
+
return { label: {
|
|
743
|
+
default: "",
|
|
744
|
+
parseHTML: (element) => element.getAttribute("aria-label") ?? "",
|
|
745
|
+
renderHTML: (attributes) => ({ "aria-label": attributes.label })
|
|
746
|
+
} };
|
|
747
|
+
},
|
|
748
|
+
parseHTML() {
|
|
749
|
+
return [{ tag: "ui-tabs > section" }];
|
|
750
|
+
},
|
|
751
|
+
renderHTML({ HTMLAttributes }) {
|
|
752
|
+
return [
|
|
753
|
+
"section",
|
|
754
|
+
HTMLAttributes,
|
|
755
|
+
0
|
|
756
|
+
];
|
|
757
|
+
},
|
|
758
|
+
addNodeView() {
|
|
759
|
+
return ({ node }) => {
|
|
760
|
+
let current = node;
|
|
761
|
+
const dom = document.createElement("section");
|
|
762
|
+
const label = document.createElement("div");
|
|
763
|
+
label.contentEditable = "false";
|
|
764
|
+
label.dataset.loomaTabLabel = "";
|
|
765
|
+
label.textContent = node.attrs.label;
|
|
766
|
+
const contentDOM = document.createElement("div");
|
|
767
|
+
dom.setAttribute("aria-label", node.attrs.label);
|
|
768
|
+
dom.append(label, contentDOM);
|
|
769
|
+
return {
|
|
770
|
+
dom,
|
|
771
|
+
contentDOM,
|
|
772
|
+
update(next) {
|
|
773
|
+
if (next.type !== current.type) return false;
|
|
774
|
+
current = next;
|
|
775
|
+
label.textContent = next.attrs.label;
|
|
776
|
+
dom.setAttribute("aria-label", next.attrs.label);
|
|
777
|
+
return true;
|
|
778
|
+
},
|
|
779
|
+
ignoreMutation: (mutation) => mutation.type !== "selection" && !contentDOM.contains(mutation.target)
|
|
780
|
+
};
|
|
781
|
+
};
|
|
782
|
+
}
|
|
783
|
+
});
|
|
784
|
+
//#endregion
|
|
469
785
|
//#region src/editor/extensions/table-of-contents.ts
|
|
470
786
|
/** Normalize loaded configuration without persisting unsupported presentation values. */
|
|
471
787
|
function normalizeTableOfContentsConfig(attributes) {
|
|
@@ -512,7 +828,17 @@ function tableOfContentsTree(entries) {
|
|
|
512
828
|
}
|
|
513
829
|
return roots;
|
|
514
830
|
}
|
|
515
|
-
|
|
831
|
+
var derivedFrom = (id, text) => {
|
|
832
|
+
const slug = loomaHeadingSlug(text);
|
|
833
|
+
return id === slug || id.startsWith(`${slug}-`) && /^\d+$/.test(id.slice(slug.length + 1));
|
|
834
|
+
};
|
|
835
|
+
/**
|
|
836
|
+
* Keeps heading anchors on Looma Markdown's rule. An anchor derived from its heading's text
|
|
837
|
+
* (as it read before this change) follows the text; any other, including `looma-heading-N` anchors
|
|
838
|
+
* saved before this rule, is a custom anchor and stays, so links to it keep working. With
|
|
839
|
+
* a table of contents every heading gets an anchor; without one, only headings that have one.
|
|
840
|
+
*/
|
|
841
|
+
function anchorTransaction(state, previous) {
|
|
516
842
|
let hasToc = false;
|
|
517
843
|
const headings = [];
|
|
518
844
|
state.doc.descendants((node, position) => {
|
|
@@ -522,24 +848,28 @@ function anchorTransaction(state) {
|
|
|
522
848
|
position
|
|
523
849
|
});
|
|
524
850
|
});
|
|
525
|
-
if (!
|
|
526
|
-
const
|
|
527
|
-
|
|
851
|
+
if (!headings.length) return null;
|
|
852
|
+
const before = /* @__PURE__ */ new Map();
|
|
853
|
+
previous?.descendants((node) => {
|
|
854
|
+
if (node.type.name === "heading" && typeof node.attrs.id === "string") before.set(node.attrs.id, node.textContent.trim());
|
|
855
|
+
});
|
|
856
|
+
const custom = (node) => {
|
|
857
|
+
const id = node.attrs.id;
|
|
858
|
+
if (typeof id !== "string" || !id || /\s/.test(id)) return void 0;
|
|
859
|
+
return derivedFrom(id, before.get(id) ?? node.textContent.trim()) ? void 0 : id;
|
|
860
|
+
};
|
|
861
|
+
const ids = assignLoomaHeadingIds(headings.map(({ node }) => ({
|
|
862
|
+
text: node.textContent.trim(),
|
|
863
|
+
id: custom(node)
|
|
864
|
+
})));
|
|
528
865
|
const transaction = state.tr;
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
transaction.setNodeMarkup(position, void 0, {
|
|
537
|
-
...node.attrs,
|
|
538
|
-
id
|
|
539
|
-
});
|
|
540
|
-
}
|
|
541
|
-
used.add(id);
|
|
542
|
-
}
|
|
866
|
+
headings.forEach(({ node, position }, index) => {
|
|
867
|
+
if (node.attrs.id === ids[index] || !hasToc && !node.attrs.id) return;
|
|
868
|
+
transaction.setNodeMarkup(position, void 0, {
|
|
869
|
+
...node.attrs,
|
|
870
|
+
id: ids[index]
|
|
871
|
+
});
|
|
872
|
+
});
|
|
543
873
|
return transaction.docChanged ? transaction : null;
|
|
544
874
|
}
|
|
545
875
|
var LoomaHeadingAnchors = Extension.create({
|
|
@@ -555,7 +885,7 @@ var LoomaHeadingAnchors = Extension.create({
|
|
|
555
885
|
if (transaction) this.editor.view.dispatch(transaction.setMeta("addToHistory", false));
|
|
556
886
|
},
|
|
557
887
|
addProseMirrorPlugins() {
|
|
558
|
-
return [new Plugin({ appendTransaction: (transactions,
|
|
888
|
+
return [new Plugin({ appendTransaction: (transactions, oldState, state) => transactions.some((transaction) => transaction.docChanged) ? anchorTransaction(state, oldState.doc) : null })];
|
|
559
889
|
}
|
|
560
890
|
});
|
|
561
891
|
function contentsHtml(doc, config) {
|
|
@@ -9165,47 +9495,96 @@ function createDOMPurify() {
|
|
|
9165
9495
|
}
|
|
9166
9496
|
var purify = createDOMPurify();
|
|
9167
9497
|
//#endregion
|
|
9168
|
-
//#region src/
|
|
9169
|
-
var
|
|
9170
|
-
"note",
|
|
9171
|
-
"tip",
|
|
9172
|
-
"info",
|
|
9173
|
-
"important",
|
|
9174
|
-
"warning",
|
|
9175
|
-
"danger",
|
|
9176
|
-
"caution",
|
|
9177
|
-
"details"
|
|
9178
|
-
]);
|
|
9179
|
-
var CONTAINER_OPEN = /^(:{3,})[ \t]*([A-Za-z]+)(?:[ \t]+(.*?))?[ \t]*$/;
|
|
9498
|
+
//#region src/markdown/markdown-it.ts
|
|
9499
|
+
var CONTAINER_OPEN = /^(:{3,})[ \t]*([A-Za-z][\w-]*)(?:[ \t]+(.*?))?[ \t]*$/;
|
|
9180
9500
|
var CONTAINER_CLOSE = /^:{3,}[ \t]*$/;
|
|
9501
|
+
var FENCE_OPEN = /^(`{3,}|~{3,})/;
|
|
9502
|
+
var TAB_LABEL = /^==[ \t]+(\S.*?)[ \t]*$/;
|
|
9181
9503
|
var OPEN_FLAG = /[ \t]*\{open\}$/;
|
|
9182
9504
|
var ALERT = /^\[!(NOTE|TIP|IMPORTANT|WARNING|CAUTION)\][ \t]*(?:\n|$)/i;
|
|
9505
|
+
var LEAF = /^::([A-Za-z][\w-]*)(\{.*\})?[ \t]*$/;
|
|
9506
|
+
var TASK = /^\[([ xX])\](?:[ \t]+|$)/;
|
|
9507
|
+
var TOC_FORMATS = /* @__PURE__ */ new Set([
|
|
9508
|
+
"plain",
|
|
9509
|
+
"bulleted",
|
|
9510
|
+
"numbered"
|
|
9511
|
+
]);
|
|
9512
|
+
var PLACEMENTS = /* @__PURE__ */ new Set([
|
|
9513
|
+
"block",
|
|
9514
|
+
"center",
|
|
9515
|
+
"wrap-left",
|
|
9516
|
+
"wrap-right"
|
|
9517
|
+
]);
|
|
9518
|
+
var opensContainer = (text) => {
|
|
9519
|
+
const match = CONTAINER_OPEN.exec(text);
|
|
9520
|
+
return match && LOOMA_CONTAINER_NAMES.has(match[2].toLowerCase()) ? match : null;
|
|
9521
|
+
};
|
|
9183
9522
|
function lineText(state, line) {
|
|
9184
9523
|
return state.src.slice(state.bMarks[line] + state.tShift[line], state.eMarks[line]);
|
|
9185
9524
|
}
|
|
9525
|
+
function frontmatter(state, startLine, _endLine, silent) {
|
|
9526
|
+
if (startLine !== 0 || state.blkIndent !== 0) return false;
|
|
9527
|
+
const split = splitLoomaFrontmatter(state.src);
|
|
9528
|
+
if (!split) return false;
|
|
9529
|
+
if (silent) return true;
|
|
9530
|
+
const lines = split.source.split("\n").length;
|
|
9531
|
+
const token = state.push("looma_frontmatter", "", 0);
|
|
9532
|
+
Object.assign(token, {
|
|
9533
|
+
content: split.yaml,
|
|
9534
|
+
block: true,
|
|
9535
|
+
hidden: true,
|
|
9536
|
+
map: [0, lines]
|
|
9537
|
+
});
|
|
9538
|
+
state.env.frontmatter = split.yaml;
|
|
9539
|
+
state.line = lines;
|
|
9540
|
+
return true;
|
|
9541
|
+
}
|
|
9186
9542
|
function container(state, startLine, endLine, silent) {
|
|
9187
9543
|
if (state.sCount[startLine] - state.blkIndent >= 4) return false;
|
|
9188
|
-
const match =
|
|
9189
|
-
if (!match
|
|
9544
|
+
const match = opensContainer(lineText(state, startLine));
|
|
9545
|
+
if (!match) return false;
|
|
9190
9546
|
if (silent) return true;
|
|
9547
|
+
const name = match[2].toLowerCase();
|
|
9548
|
+
const grouped = name === "tabs" || name === "code-group";
|
|
9549
|
+
const labels = [];
|
|
9191
9550
|
let depth = 1;
|
|
9551
|
+
let fence = null;
|
|
9192
9552
|
let next = startLine + 1;
|
|
9193
9553
|
for (; next < endLine; next++) {
|
|
9194
|
-
if (state.sCount[next] < state.blkIndent) break;
|
|
9195
9554
|
const text = lineText(state, next);
|
|
9555
|
+
if (text && state.sCount[next] < state.blkIndent) break;
|
|
9556
|
+
if (fence) {
|
|
9557
|
+
const close = FENCE_OPEN.exec(text);
|
|
9558
|
+
if (close && close[1][0] === fence.char && close[1].length >= fence.length && !text.slice(close[1].length).trim()) fence = null;
|
|
9559
|
+
continue;
|
|
9560
|
+
}
|
|
9561
|
+
const opener = FENCE_OPEN.exec(text);
|
|
9562
|
+
if (opener) {
|
|
9563
|
+
fence = {
|
|
9564
|
+
char: opener[1][0],
|
|
9565
|
+
length: opener[1].length
|
|
9566
|
+
};
|
|
9567
|
+
continue;
|
|
9568
|
+
}
|
|
9196
9569
|
if (CONTAINER_CLOSE.test(text)) {
|
|
9197
9570
|
if (--depth === 0) break;
|
|
9198
|
-
} else if (
|
|
9571
|
+
} else if (opensContainer(text)) depth++;
|
|
9572
|
+
else if (grouped && depth === 1) {
|
|
9573
|
+
const label = TAB_LABEL.exec(text);
|
|
9574
|
+
if (label) labels.push({
|
|
9575
|
+
line: next,
|
|
9576
|
+
label: label[1]
|
|
9577
|
+
});
|
|
9578
|
+
}
|
|
9199
9579
|
}
|
|
9200
|
-
const name = match[2].toLowerCase();
|
|
9201
9580
|
let title = (match[3] ?? "").trim();
|
|
9202
|
-
const open = OPEN_FLAG.test(title);
|
|
9203
|
-
title = title.replace(OPEN_FLAG, "");
|
|
9581
|
+
const open = name === "details" && OPEN_FLAG.test(title);
|
|
9582
|
+
if (name === "details") title = title.replace(OPEN_FLAG, "");
|
|
9204
9583
|
const parentType = state.parentType;
|
|
9205
9584
|
const lineMax = state.lineMax;
|
|
9206
9585
|
state.parentType = "container";
|
|
9207
9586
|
state.lineMax = next;
|
|
9208
|
-
const opening = state.push("looma_container_open", "", 1);
|
|
9587
|
+
const opening = state.push("looma_container_open", "div", 1);
|
|
9209
9588
|
Object.assign(opening, {
|
|
9210
9589
|
info: name,
|
|
9211
9590
|
meta: {
|
|
@@ -9216,10 +9595,30 @@ function container(state, startLine, endLine, silent) {
|
|
|
9216
9595
|
map: [startLine, next],
|
|
9217
9596
|
markup: match[1]
|
|
9218
9597
|
});
|
|
9219
|
-
|
|
9220
|
-
|
|
9598
|
+
const panel = (label, from, to) => {
|
|
9599
|
+
state.push("looma_tab_open", "section", 1).meta = { label };
|
|
9600
|
+
state.md.block.tokenize(state, from, to);
|
|
9601
|
+
state.push("looma_tab_close", "section", -1);
|
|
9602
|
+
};
|
|
9603
|
+
if (labels.length) {
|
|
9604
|
+
state.md.block.tokenize(state, startLine + 1, labels[0].line);
|
|
9605
|
+
labels.forEach(({ line, label }, index) => panel(label, line + 1, labels[index + 1]?.line ?? next));
|
|
9606
|
+
} else {
|
|
9607
|
+
const first = state.tokens.length;
|
|
9608
|
+
state.md.block.tokenize(state, startLine + 1, next);
|
|
9609
|
+
if (name === "code-group") for (let index = state.tokens.length - 1; index >= first; index--) {
|
|
9610
|
+
const token = state.tokens[index];
|
|
9611
|
+
if (token.type !== "fence" || token.level !== opening.level + 1) continue;
|
|
9612
|
+
const { title: fenceTitle, language } = parseLoomaFenceInfo(token.info);
|
|
9613
|
+
const tabOpen = new state.Token("looma_tab_open", "section", 1);
|
|
9614
|
+
tabOpen.meta = { label: fenceTitle ?? language ?? "Code" };
|
|
9615
|
+
state.tokens.splice(index, 1, tabOpen, token, new state.Token("looma_tab_close", "section", -1));
|
|
9616
|
+
}
|
|
9617
|
+
}
|
|
9618
|
+
const closing = state.push("looma_container_close", "div", -1);
|
|
9221
9619
|
Object.assign(closing, {
|
|
9222
9620
|
info: name,
|
|
9621
|
+
meta: { title },
|
|
9223
9622
|
block: true,
|
|
9224
9623
|
markup: match[1]
|
|
9225
9624
|
});
|
|
@@ -9228,6 +9627,27 @@ function container(state, startLine, endLine, silent) {
|
|
|
9228
9627
|
state.line = Math.min(next + 1, endLine);
|
|
9229
9628
|
return true;
|
|
9230
9629
|
}
|
|
9630
|
+
function leaf(state, startLine, _endLine, silent) {
|
|
9631
|
+
if (state.sCount[startLine] - state.blkIndent >= 4) return false;
|
|
9632
|
+
const match = LEAF.exec(lineText(state, startLine));
|
|
9633
|
+
if (!match || match[1].toLowerCase() !== "toc") return false;
|
|
9634
|
+
const attributes = match[2] ? parseLoomaAttributes(match[2]) : {};
|
|
9635
|
+
if (!attributes) return false;
|
|
9636
|
+
if (silent) return true;
|
|
9637
|
+
const format = typeof attributes.format === "string" && TOC_FORMATS.has(attributes.format) ? attributes.format : "plain";
|
|
9638
|
+
const depth = attributes.depth === "1" || attributes.depth === "2" ? Number(attributes.depth) : 3;
|
|
9639
|
+
const token = state.push("looma_toc", "nav", 0);
|
|
9640
|
+
Object.assign(token, {
|
|
9641
|
+
block: true,
|
|
9642
|
+
map: [startLine, startLine + 1],
|
|
9643
|
+
meta: {
|
|
9644
|
+
format,
|
|
9645
|
+
depth
|
|
9646
|
+
}
|
|
9647
|
+
});
|
|
9648
|
+
state.line = startLine + 1;
|
|
9649
|
+
return true;
|
|
9650
|
+
}
|
|
9231
9651
|
function alerts(state) {
|
|
9232
9652
|
const tokens = state.tokens;
|
|
9233
9653
|
for (let index = 0; index < tokens.length; index++) {
|
|
@@ -9246,78 +9666,363 @@ function alerts(state) {
|
|
|
9246
9666
|
}
|
|
9247
9667
|
}
|
|
9248
9668
|
}
|
|
9249
|
-
function
|
|
9669
|
+
function taskLists(state) {
|
|
9670
|
+
const tokens = state.tokens;
|
|
9671
|
+
for (let index = 0; index < tokens.length; index++) {
|
|
9672
|
+
if (tokens[index].type !== "bullet_list_open") continue;
|
|
9673
|
+
const level = tokens[index].level;
|
|
9674
|
+
const items = [];
|
|
9675
|
+
let isTaskList = true;
|
|
9676
|
+
for (let cursor = index + 1; cursor < tokens.length; cursor++) {
|
|
9677
|
+
const token = tokens[cursor];
|
|
9678
|
+
if (token.type === "bullet_list_close" && token.level === level) break;
|
|
9679
|
+
if (token.type !== "list_item_open" || token.level !== level + 1) continue;
|
|
9680
|
+
const inline = tokens[cursor + 2];
|
|
9681
|
+
const match = tokens[cursor + 1]?.type === "paragraph_open" && inline?.type === "inline" ? TASK.exec(inline.content) : null;
|
|
9682
|
+
if (!match) {
|
|
9683
|
+
isTaskList = false;
|
|
9684
|
+
break;
|
|
9685
|
+
}
|
|
9686
|
+
items.push({
|
|
9687
|
+
item: token,
|
|
9688
|
+
inline,
|
|
9689
|
+
checked: match[1] !== " "
|
|
9690
|
+
});
|
|
9691
|
+
}
|
|
9692
|
+
if (!isTaskList || !items.length) continue;
|
|
9693
|
+
tokens[index].attrSet("data-type", "taskList");
|
|
9694
|
+
for (const { item, inline, checked } of items) {
|
|
9695
|
+
inline.content = inline.content.replace(TASK, "");
|
|
9696
|
+
item.attrSet("data-type", "taskItem");
|
|
9697
|
+
item.attrSet("data-checked", String(checked));
|
|
9698
|
+
item.meta = { checked };
|
|
9699
|
+
}
|
|
9700
|
+
}
|
|
9701
|
+
}
|
|
9702
|
+
function directive(state, silent) {
|
|
9703
|
+
const { src, pos } = state;
|
|
9704
|
+
if (src.charCodeAt(pos) !== 58) return false;
|
|
9705
|
+
const name = /^:(chip|mention)\[/.exec(src.slice(pos, pos + 10))?.[1];
|
|
9706
|
+
if (!name) return false;
|
|
9707
|
+
let label = "";
|
|
9708
|
+
let index = pos + name.length + 2;
|
|
9709
|
+
for (; index < state.posMax && src[index] !== "]"; index++) {
|
|
9710
|
+
if (src[index] === "\\" && index + 1 < state.posMax) index++;
|
|
9711
|
+
label += src[index];
|
|
9712
|
+
}
|
|
9713
|
+
if (src[index] !== "]") return false;
|
|
9714
|
+
const read = src[index + 1] === "{" ? readLoomaAttributes(src, index + 1) : {
|
|
9715
|
+
attributes: {},
|
|
9716
|
+
end: index + 1
|
|
9717
|
+
};
|
|
9718
|
+
if (!read || read.end > state.posMax) return false;
|
|
9719
|
+
if (name === "mention" && typeof read.attributes.id !== "string") return false;
|
|
9720
|
+
if (!silent) {
|
|
9721
|
+
const token = state.push(`looma_${name}`, "span", 0);
|
|
9722
|
+
token.meta = {
|
|
9723
|
+
label,
|
|
9724
|
+
attributes: read.attributes
|
|
9725
|
+
};
|
|
9726
|
+
}
|
|
9727
|
+
state.pos = read.end;
|
|
9728
|
+
return true;
|
|
9729
|
+
}
|
|
9730
|
+
function inlineText(token) {
|
|
9731
|
+
return (token?.children ?? []).map((child) => child.type === "image" ? inlineText(child) : child.content).join("");
|
|
9732
|
+
}
|
|
9733
|
+
function attributes(state) {
|
|
9734
|
+
const headings = [];
|
|
9735
|
+
const tokens = state.tokens;
|
|
9736
|
+
for (let index = 0; index < tokens.length; index++) {
|
|
9737
|
+
const token = tokens[index];
|
|
9738
|
+
if (token.type !== "inline" || !token.children) continue;
|
|
9739
|
+
const children = token.children;
|
|
9740
|
+
for (let child = children.length - 3; child >= 0; child--) {
|
|
9741
|
+
const text = children[child + 1];
|
|
9742
|
+
if (children[child].type !== "link_open" || children[child].markup !== "linkify" || /^(?:[a-z][\w+.-]*:|www\.)/i.test(text.content)) continue;
|
|
9743
|
+
children.splice(child, 3, Object.assign(new state.Token("text", "", 0), { content: text.content }));
|
|
9744
|
+
}
|
|
9745
|
+
if (tokens[index - 1]?.type === "heading_open") {
|
|
9746
|
+
const last = children.at(-1);
|
|
9747
|
+
const marker = last?.type === "text" ? /[ \t]+(\{[^{}]*\})$/.exec(last.content) : null;
|
|
9748
|
+
const parsed = marker && parseLoomaAttributes(marker[1]);
|
|
9749
|
+
let id;
|
|
9750
|
+
if (last && marker && parsed && typeof parsed.id === "string") {
|
|
9751
|
+
id = parsed.id;
|
|
9752
|
+
last.content = last.content.slice(0, marker.index);
|
|
9753
|
+
}
|
|
9754
|
+
headings.push({
|
|
9755
|
+
open: tokens[index - 1],
|
|
9756
|
+
text: inlineText(token).trim(),
|
|
9757
|
+
id
|
|
9758
|
+
});
|
|
9759
|
+
}
|
|
9760
|
+
for (let child = 0; child < children.length; child++) {
|
|
9761
|
+
const next = children[child + 1];
|
|
9762
|
+
if (children[child].type !== "image" || next?.type !== "text" || !next.content.startsWith("{")) continue;
|
|
9763
|
+
const read = readLoomaAttributes(next.content);
|
|
9764
|
+
if (!read) continue;
|
|
9765
|
+
applyImageAttributes(children[child], read.attributes);
|
|
9766
|
+
next.content = next.content.slice(read.end);
|
|
9767
|
+
if (!next.content) children.splice(child + 1, 1);
|
|
9768
|
+
}
|
|
9769
|
+
if (tokens[index - 1]?.type === "paragraph_open" && children.length === 1 && children[0].type === "image") {
|
|
9770
|
+
tokens[index - 1].hidden = true;
|
|
9771
|
+
tokens[index + 1].hidden = true;
|
|
9772
|
+
}
|
|
9773
|
+
}
|
|
9774
|
+
const ids = assignLoomaHeadingIds(headings);
|
|
9775
|
+
headings.forEach(({ open }, index) => open.attrSet("id", ids[index]));
|
|
9776
|
+
}
|
|
9777
|
+
function applyImageAttributes(image, attributes) {
|
|
9778
|
+
for (const key of ["width", "height"]) {
|
|
9779
|
+
const value = Number(attributes[key]);
|
|
9780
|
+
if (Number.isSafeInteger(value) && value > 0) image.attrSet(key, String(value));
|
|
9781
|
+
}
|
|
9782
|
+
const placement = attributes.placement;
|
|
9783
|
+
if (typeof placement === "string" && PLACEMENTS.has(placement) && placement !== "block") image.attrSet("data-placement", placement);
|
|
9784
|
+
if (attributes.responsive === true) image.attrSet("data-looma-responsive", "");
|
|
9785
|
+
}
|
|
9786
|
+
function tableOfContents(tokens, format, depth, escape) {
|
|
9787
|
+
const roots = [];
|
|
9788
|
+
const ancestors = [];
|
|
9789
|
+
tokens.forEach((token, index) => {
|
|
9790
|
+
if (token.type !== "heading_open") return;
|
|
9791
|
+
const level = Number(token.tag.slice(1));
|
|
9792
|
+
const text = inlineText(tokens[index + 1]).trim();
|
|
9793
|
+
if (level > depth || !text) return;
|
|
9794
|
+
while (ancestors.length && ancestors[ancestors.length - 1].level >= level) ancestors.pop();
|
|
9795
|
+
const entry = {
|
|
9796
|
+
id: token.attrGet("id") ?? "",
|
|
9797
|
+
text,
|
|
9798
|
+
children: []
|
|
9799
|
+
};
|
|
9800
|
+
(ancestors.at(-1)?.entry.children ?? roots).push(entry);
|
|
9801
|
+
ancestors.push({
|
|
9802
|
+
level,
|
|
9803
|
+
entry
|
|
9804
|
+
});
|
|
9805
|
+
});
|
|
9806
|
+
const [list, item] = format === "plain" ? ["div", "div"] : [format === "numbered" ? "ol" : "ul", "li"];
|
|
9807
|
+
const render = (entries) => `<${list} role="list">${entries.map(({ id, text, children }) => `<${item} role="listitem"><a href="#${escape(id)}">${escape(text)}</a>${children.length ? render(children) : ""}</${item}>`).join("")}</${list}>`;
|
|
9808
|
+
return `<nav data-looma-toc="" data-format="${format}" data-depth="${depth}" aria-label="Table of contents"><strong>Table of contents</strong>${roots.length ? render(roots) : ""}</nav>\n`;
|
|
9809
|
+
}
|
|
9810
|
+
/**
|
|
9811
|
+
* Adds Looma Markdown to a markdown-it instance: containers, alerts, task lists, the table of
|
|
9812
|
+
* contents, chips, mentions, brace attributes, heading anchors, code meta and front matter.
|
|
9813
|
+
* @contract Renders the canonical HTML, which is the HTML LoomaEditor saves. It is not sanitized.
|
|
9814
|
+
* Linkify and raw HTML are instance options; `createLoomaMarkdownIt` turns both on.
|
|
9815
|
+
*/
|
|
9816
|
+
function loomaMarkdown$1(md) {
|
|
9250
9817
|
const escape = md.utils.escapeHtml;
|
|
9818
|
+
md.block.ruler.before("table", "looma_frontmatter", frontmatter);
|
|
9251
9819
|
md.block.ruler.before("fence", "looma_container", container, { alt: [
|
|
9252
9820
|
"paragraph",
|
|
9253
9821
|
"reference",
|
|
9254
9822
|
"blockquote",
|
|
9255
9823
|
"list"
|
|
9256
9824
|
] });
|
|
9825
|
+
md.block.ruler.before("paragraph", "looma_leaf", leaf, { alt: [
|
|
9826
|
+
"paragraph",
|
|
9827
|
+
"reference",
|
|
9828
|
+
"blockquote",
|
|
9829
|
+
"list"
|
|
9830
|
+
] });
|
|
9831
|
+
md.inline.ruler.before("link", "looma_directive", directive);
|
|
9832
|
+
md.core.ruler.after("block", "looma_task_lists", taskLists);
|
|
9257
9833
|
md.core.ruler.before("inline", "looma_alerts", alerts);
|
|
9258
|
-
md.
|
|
9834
|
+
md.core.ruler.before("text_join", "looma_attributes", attributes);
|
|
9835
|
+
const rules = md.renderer.rules;
|
|
9836
|
+
rules.looma_frontmatter = () => "";
|
|
9837
|
+
rules.looma_container_open = (tokens, index) => {
|
|
9259
9838
|
const { info, meta } = tokens[index];
|
|
9260
9839
|
if (info === "details") return `<details${meta.open ? " open" : ""}><summary>${escape(meta.title || "Details")}</summary>\n`;
|
|
9840
|
+
if (info === "figure") return "<figure>\n";
|
|
9841
|
+
if (info === "tabs" || info === "code-group") return `<ui-tabs${info === "code-group" ? " class=\"code-group\"" : ""} group="${escape(meta.title || info)}">\n`;
|
|
9261
9842
|
return `<ui-callout tone="${normalizeLoomaCalloutTone(info)}"${meta.title ? ` heading="${escape(meta.title)}"` : ""}>\n`;
|
|
9262
9843
|
};
|
|
9263
|
-
|
|
9264
|
-
|
|
9844
|
+
rules.looma_container_close = (tokens, index, _options, env) => {
|
|
9845
|
+
const { info } = tokens[index];
|
|
9846
|
+
if (info === "details") return "</details>\n";
|
|
9847
|
+
if (info === "figure") {
|
|
9848
|
+
const title = tokens[index].meta.title;
|
|
9849
|
+
return `${title ? `<figcaption>${md.renderInline(title, env)}</figcaption>\n` : ""}</figure>\n`;
|
|
9850
|
+
}
|
|
9851
|
+
if (info === "tabs" || info === "code-group") return "</ui-tabs>\n";
|
|
9852
|
+
return "</ui-callout>\n";
|
|
9853
|
+
};
|
|
9854
|
+
rules.looma_tab_open = (tokens, index) => `<section aria-label="${escape(tokens[index].meta.label)}">\n`;
|
|
9855
|
+
rules.looma_tab_close = () => "</section>\n";
|
|
9856
|
+
rules.looma_toc = (tokens, index) => tableOfContents(tokens, tokens[index].meta.format, tokens[index].meta.depth, escape);
|
|
9857
|
+
rules.looma_chip = (tokens, index) => {
|
|
9858
|
+
const { label, attributes } = tokens[index].meta;
|
|
9859
|
+
const tone = normalizeLoomaChipTone(attributes.tone);
|
|
9860
|
+
return `<span data-looma-chip="" data-label="${escape(label)}" data-color="${tone}"${label ? "" : " data-placeholder=\"\""}>${escape(label)}</span>`;
|
|
9861
|
+
};
|
|
9862
|
+
rules.looma_mention = (tokens, index) => {
|
|
9863
|
+
const { label, attributes } = tokens[index].meta;
|
|
9864
|
+
const id = String(attributes.id);
|
|
9865
|
+
return `<span data-type="mention" class="looma-mention" data-id="${escape(id)}" data-label="${escape(label || id)}">@${escape(label || id)}</span>`;
|
|
9866
|
+
};
|
|
9867
|
+
const listItem = rules.list_item_open ?? ((tokens, index, options, _env, self) => self.renderToken(tokens, index, options));
|
|
9868
|
+
rules.list_item_open = (tokens, index, options, env, self) => {
|
|
9869
|
+
const html = listItem(tokens, index, options, env, self);
|
|
9870
|
+
const task = tokens[index].meta;
|
|
9871
|
+
return task && typeof task.checked === "boolean" ? `${html}<input type="checkbox" disabled=""${task.checked ? " checked=\"\"" : ""}>` : html;
|
|
9872
|
+
};
|
|
9873
|
+
const code = (content, info) => {
|
|
9874
|
+
const { language, meta } = parseLoomaFenceInfo(info);
|
|
9875
|
+
return `<pre><code${language ? ` class="language-${escape(language)}"` : ""}${meta ? ` data-meta="${escape(meta)}"` : ""}>${escape(content.replace(/\n$/, ""))}</code></pre>\n`;
|
|
9876
|
+
};
|
|
9877
|
+
rules.fence = (tokens, index) => code(tokens[index].content, tokens[index].info);
|
|
9878
|
+
rules.code_block = (tokens, index) => code(tokens[index].content, "");
|
|
9265
9879
|
}
|
|
9266
|
-
/** The markdown-it instance LoomaEditor imports Markdown with, shared block vocabulary included. */
|
|
9267
|
-
var loomaMarkdown = withSharedBlocks(new MarkdownIt({
|
|
9268
|
-
html: true,
|
|
9269
|
-
linkify: false,
|
|
9270
|
-
typographer: false
|
|
9271
|
-
}));
|
|
9272
9880
|
/**
|
|
9273
|
-
*
|
|
9881
|
+
* A markdown-it instance for Looma Markdown: raw HTML and GFM autolinks on, with `loomaMarkdown`.
|
|
9882
|
+
* @contract Each call returns a new instance that callers may extend.
|
|
9883
|
+
*/
|
|
9884
|
+
function createLoomaMarkdownIt() {
|
|
9885
|
+
return new MarkdownIt({
|
|
9886
|
+
html: true,
|
|
9887
|
+
linkify: true,
|
|
9888
|
+
typographer: false
|
|
9889
|
+
}).use(loomaMarkdown$1);
|
|
9890
|
+
}
|
|
9891
|
+
//#endregion
|
|
9892
|
+
//#region src/editor/extensions/markdown.ts
|
|
9893
|
+
/** The markdown-it instance LoomaEditor reads Looma Markdown with. */
|
|
9894
|
+
var loomaMarkdown = createLoomaMarkdownIt();
|
|
9895
|
+
/**
|
|
9896
|
+
* Renders Looma Markdown as the HTML the editor parses.
|
|
9274
9897
|
* @contract The result is not sanitized; pass it to the editor's `setContent`, whose schema
|
|
9275
|
-
* keeps only content it knows.
|
|
9898
|
+
* keeps only content it knows. Front matter renders nothing.
|
|
9276
9899
|
*/
|
|
9277
9900
|
function loomaMarkdownToHtml(source) {
|
|
9278
|
-
return loomaMarkdown.render(source)
|
|
9901
|
+
return loomaMarkdown.render(source);
|
|
9279
9902
|
}
|
|
9903
|
+
var CONTAINERS = /* @__PURE__ */ new Set([
|
|
9904
|
+
"loomaCallout",
|
|
9905
|
+
"loomaExpand",
|
|
9906
|
+
"loomaFigure",
|
|
9907
|
+
"loomaTabs"
|
|
9908
|
+
]);
|
|
9280
9909
|
var containerDepth = (node) => {
|
|
9281
9910
|
let depth = 0;
|
|
9282
9911
|
node.descendants((child) => {
|
|
9283
|
-
if (
|
|
9912
|
+
if (!CONTAINERS.has(child.type.name)) return true;
|
|
9913
|
+
depth = Math.max(depth, 1 + containerDepth(child));
|
|
9284
9914
|
return false;
|
|
9285
9915
|
});
|
|
9286
9916
|
return depth;
|
|
9287
9917
|
};
|
|
9288
|
-
|
|
9289
|
-
|
|
9918
|
+
var endLine = (state) => state.flushClose(1);
|
|
9919
|
+
function writeContainer(state, node, opening, content = () => state.renderContent(node)) {
|
|
9920
|
+
const marker = loomaContainerMarker(containerDepth(node));
|
|
9290
9921
|
state.write(`${marker} ${opening}`.trimEnd());
|
|
9291
9922
|
state.ensureNewLine();
|
|
9292
|
-
|
|
9923
|
+
content();
|
|
9924
|
+
endLine(state);
|
|
9293
9925
|
state.write(marker);
|
|
9294
9926
|
state.closeBlock(node);
|
|
9295
9927
|
}
|
|
9296
9928
|
var defaults = defaultMarkdownSerializer.nodes;
|
|
9929
|
+
var label = (text) => text.replace(/[\\\]]/g, "\\$&");
|
|
9930
|
+
var ALIGNMENT = {
|
|
9931
|
+
left: ":---",
|
|
9932
|
+
center: ":---:",
|
|
9933
|
+
right: "---:"
|
|
9934
|
+
};
|
|
9297
9935
|
var NODES = {
|
|
9298
9936
|
paragraph: defaults.paragraph,
|
|
9299
|
-
text:
|
|
9937
|
+
text: (state, node, parent, index) => {
|
|
9938
|
+
const marker = /^:{3,}[ \t]*([A-Za-z][\w-]*)?/.exec(node.text ?? "");
|
|
9939
|
+
if ((marker ? !marker[1] || LOOMA_CONTAINER_NAMES.has(marker[1].toLowerCase()) : /^::toc(?![\w-])/i.test(node.text ?? "")) && state.atBlank()) state.write("\\");
|
|
9940
|
+
defaults.text(state, node, parent, index);
|
|
9941
|
+
},
|
|
9300
9942
|
blockquote: defaults.blockquote,
|
|
9301
|
-
heading: defaults.heading,
|
|
9302
9943
|
horizontalRule: defaults.horizontal_rule,
|
|
9303
|
-
bulletList:
|
|
9944
|
+
bulletList: (state, node) => state.renderList(node, " ", () => "- "),
|
|
9304
9945
|
orderedList: (state, node) => {
|
|
9305
9946
|
const start = Number(node.attrs.start ?? 1);
|
|
9306
9947
|
state.renderList(node, " ", (index) => `${start + index}. `);
|
|
9307
9948
|
},
|
|
9308
9949
|
listItem: defaults.list_item,
|
|
9950
|
+
taskList: (state, node) => state.renderList(node, " ", () => "- "),
|
|
9951
|
+
taskItem: (state, node) => {
|
|
9952
|
+
state.write(node.attrs.checked ? "[x] " : "[ ] ");
|
|
9953
|
+
state.renderContent(node);
|
|
9954
|
+
},
|
|
9309
9955
|
hardBreak: defaults.hard_break,
|
|
9310
9956
|
codeBlock: (state, node) => {
|
|
9311
|
-
const
|
|
9312
|
-
|
|
9313
|
-
state.write(`${fence}${node.attrs.language ?? ""}\n`);
|
|
9957
|
+
const fence = loomaCodeFence(node.textContent);
|
|
9958
|
+
state.write(`${fence}${node.attrs.language ?? ""}${node.attrs.meta ? ` ${node.attrs.meta}` : ""}\n`);
|
|
9314
9959
|
state.text(node.textContent, false);
|
|
9315
9960
|
state.ensureNewLine();
|
|
9316
9961
|
state.write(fence);
|
|
9317
9962
|
state.closeBlock(node);
|
|
9318
9963
|
},
|
|
9964
|
+
image: (state, node, parent, index) => {
|
|
9965
|
+
defaults.image(state, node, parent, index);
|
|
9966
|
+
const { width, height, placement, responsive } = node.attrs;
|
|
9967
|
+
state.write(formatLoomaAttributes({
|
|
9968
|
+
width,
|
|
9969
|
+
height,
|
|
9970
|
+
placement: placement && placement !== "block" ? placement : null,
|
|
9971
|
+
responsive: responsive === true
|
|
9972
|
+
}));
|
|
9973
|
+
if (node.isBlock) state.closeBlock(node);
|
|
9974
|
+
},
|
|
9319
9975
|
loomaCallout: (state, node) => writeContainer(state, node, `${node.attrs.tone} ${node.attrs.heading ?? ""}`),
|
|
9320
|
-
loomaExpand: (state, node) => writeContainer(state, node, `details ${node.attrs.summary}${node.attrs.open ? " {open}" : ""}`)
|
|
9976
|
+
loomaExpand: (state, node) => writeContainer(state, node, `details ${node.attrs.summary}${node.attrs.open ? " {open}" : ""}`),
|
|
9977
|
+
loomaFigure: (state, node) => {
|
|
9978
|
+
const caption = node.lastChild;
|
|
9979
|
+
const marker = loomaContainerMarker(containerDepth(node));
|
|
9980
|
+
state.write(`${marker} figure`);
|
|
9981
|
+
if (caption.content.size) {
|
|
9982
|
+
state.write(" ");
|
|
9983
|
+
state.renderInline(caption, false);
|
|
9984
|
+
}
|
|
9985
|
+
state.ensureNewLine();
|
|
9986
|
+
state.render(node.firstChild, node, 0);
|
|
9987
|
+
endLine(state);
|
|
9988
|
+
state.write(marker);
|
|
9989
|
+
state.closeBlock(node);
|
|
9990
|
+
},
|
|
9991
|
+
loomaFigcaption: (state, node) => state.renderInline(node),
|
|
9992
|
+
loomaTabs: (state, node) => {
|
|
9993
|
+
const name = node.attrs.codeGroup ? "code-group" : "tabs";
|
|
9994
|
+
const bare = node.attrs.codeGroup && Array.from({ length: node.childCount }, (_, index) => node.child(index)).every((tab) => {
|
|
9995
|
+
const code = tab.firstChild;
|
|
9996
|
+
if (tab.childCount !== 1 || code?.type.name !== "codeBlock") return false;
|
|
9997
|
+
const { title, language } = parseLoomaFenceInfo(`${code.attrs.language ?? ""} ${code.attrs.meta ?? ""}`);
|
|
9998
|
+
return tab.attrs.label === (title ?? language ?? "Code");
|
|
9999
|
+
});
|
|
10000
|
+
writeContainer(state, node, `${name} ${node.attrs.group && node.attrs.group !== name ? node.attrs.group : ""}`, () => node.forEach((tab, _offset, index) => {
|
|
10001
|
+
if (index) endLine(state);
|
|
10002
|
+
if (!bare) {
|
|
10003
|
+
state.write(`== ${tab.attrs.label}`);
|
|
10004
|
+
state.ensureNewLine();
|
|
10005
|
+
}
|
|
10006
|
+
state.renderContent(tab);
|
|
10007
|
+
}));
|
|
10008
|
+
},
|
|
10009
|
+
loomaTab: (state, node) => state.renderContent(node),
|
|
10010
|
+
loomaTableOfContents: (state, node) => {
|
|
10011
|
+
const { format, depth } = node.attrs;
|
|
10012
|
+
state.write(`::toc${formatLoomaAttributes({
|
|
10013
|
+
depth: depth !== 3 ? depth : null,
|
|
10014
|
+
format: format !== "plain" ? format : null
|
|
10015
|
+
})}`);
|
|
10016
|
+
state.closeBlock(node);
|
|
10017
|
+
},
|
|
10018
|
+
loomaChip: (state, node) => {
|
|
10019
|
+
const tone = normalizeLoomaChipTone(node.attrs.color);
|
|
10020
|
+
state.write(`:chip[${label(String(node.attrs.label ?? ""))}]${formatLoomaAttributes({ tone: tone !== "neutral" ? tone : null })}`);
|
|
10021
|
+
},
|
|
10022
|
+
mention: (state, node) => {
|
|
10023
|
+
const id = String(node.attrs.id ?? "");
|
|
10024
|
+
state.write(`:mention[${label(String(node.attrs.label ?? id))}]${formatLoomaAttributes({ id }, { hashId: false })}`);
|
|
10025
|
+
}
|
|
9321
10026
|
};
|
|
9322
10027
|
var MARKS = {
|
|
9323
10028
|
bold: defaultMarkdownSerializer.marks.strong,
|
|
@@ -9341,31 +10046,96 @@ var MARKS = {
|
|
|
9341
10046
|
mixable: true
|
|
9342
10047
|
}
|
|
9343
10048
|
};
|
|
10049
|
+
function customHeadingIds(doc) {
|
|
10050
|
+
const headings = [];
|
|
10051
|
+
doc.descendants((node) => {
|
|
10052
|
+
if (node.type.name === "heading") headings.push({
|
|
10053
|
+
text: node.textContent.trim(),
|
|
10054
|
+
id: typeof node.attrs.id === "string" && node.attrs.id ? node.attrs.id : void 0
|
|
10055
|
+
});
|
|
10056
|
+
});
|
|
10057
|
+
const marked = /* @__PURE__ */ new Set();
|
|
10058
|
+
for (;;) {
|
|
10059
|
+
const derived = assignLoomaHeadingIds(headings.map(({ text, id }, index) => ({
|
|
10060
|
+
text,
|
|
10061
|
+
id: marked.has(index) ? id : void 0
|
|
10062
|
+
})));
|
|
10063
|
+
const next = headings.findIndex(({ id }, index) => id && !marked.has(index) && derived[index] !== id);
|
|
10064
|
+
if (next < 0) return marked;
|
|
10065
|
+
marked.add(next);
|
|
10066
|
+
}
|
|
10067
|
+
}
|
|
9344
10068
|
/**
|
|
9345
|
-
* Writes an editor document as Markdown,
|
|
9346
|
-
*
|
|
9347
|
-
*
|
|
9348
|
-
*
|
|
9349
|
-
* Marks without a Markdown form other than underline and highlight are dropped.
|
|
10069
|
+
* Writes an editor document as Looma Markdown, the form `loomaMarkdownToHtml` and Markupress read.
|
|
10070
|
+
* @contract Every node in the default preset has a Markdown form. A table that fits GFM is a GFM
|
|
10071
|
+
* table and any other is HTML. Content from extensions outside the preset is written as its saved
|
|
10072
|
+
* HTML, and marks without a Markdown form other than underline and highlight are dropped.
|
|
9350
10073
|
*/
|
|
9351
10074
|
function loomaMarkdownFromDoc(doc) {
|
|
9352
|
-
const
|
|
10075
|
+
const schema = doc.type.schema;
|
|
10076
|
+
const dom = DOMSerializer.fromSchema(schema);
|
|
9353
10077
|
const html = (node) => {
|
|
9354
10078
|
const holder = document.createElement("div");
|
|
9355
10079
|
holder.append(dom.serializeNode(node));
|
|
9356
10080
|
return holder.innerHTML;
|
|
9357
10081
|
};
|
|
9358
|
-
const
|
|
9359
|
-
|
|
10082
|
+
const custom = customHeadingIds(doc);
|
|
10083
|
+
let heading = 0;
|
|
10084
|
+
const nodes = {
|
|
10085
|
+
heading: (state, node) => {
|
|
10086
|
+
state.write(`${"#".repeat(node.attrs.level)} `);
|
|
10087
|
+
state.renderInline(node, false);
|
|
10088
|
+
if (custom.has(heading++)) state.write(` {#${node.attrs.id}}`);
|
|
10089
|
+
else {
|
|
10090
|
+
const out = state;
|
|
10091
|
+
out.out = out.out.replace(/([ \t])\{([^{}]*\})$/, "$1\\{$2");
|
|
10092
|
+
}
|
|
10093
|
+
state.closeBlock(node);
|
|
10094
|
+
},
|
|
10095
|
+
table: (state, node) => {
|
|
10096
|
+
state.write(gfmTable(node) ?? `<table><tbody>${Array.from({ length: node.childCount }, (_, index) => html(node.child(index))).join("")}</tbody></table>`.replace(/ (?:colspan|rowspan)="1"/g, ""));
|
|
10097
|
+
state.closeBlock(node);
|
|
10098
|
+
}
|
|
10099
|
+
};
|
|
10100
|
+
for (const name of Object.keys(schema.nodes)) nodes[name] ??= NODES[name] ?? ((state, node) => {
|
|
9360
10101
|
state.write(html(node));
|
|
9361
10102
|
if (node.isBlock) state.closeBlock(node);
|
|
9362
10103
|
});
|
|
9363
10104
|
const marks = {};
|
|
9364
|
-
for (const name of Object.keys(
|
|
10105
|
+
for (const name of Object.keys(schema.marks)) marks[name] = MARKS[name] ?? {
|
|
9365
10106
|
open: "",
|
|
9366
10107
|
close: ""
|
|
9367
10108
|
};
|
|
9368
|
-
|
|
10109
|
+
const serializer = new MarkdownSerializer(nodes, marks);
|
|
10110
|
+
const inline = (paragraph) => serializer.serialize(schema.topNodeType.create(null, paragraph)).replace(/\|/g, "\\|");
|
|
10111
|
+
function gfmTable(table) {
|
|
10112
|
+
const rows = Array.from({ length: table.childCount }, (_, index) => table.child(index));
|
|
10113
|
+
const width = rows[0]?.childCount ?? 0;
|
|
10114
|
+
const alignments = [];
|
|
10115
|
+
const lines = [];
|
|
10116
|
+
for (const [rowIndex, row] of rows.entries()) {
|
|
10117
|
+
if (row.childCount !== width) return null;
|
|
10118
|
+
const cells = [];
|
|
10119
|
+
for (let column = 0; column < width; column++) {
|
|
10120
|
+
const cell = row.child(column);
|
|
10121
|
+
const { colspan, rowspan, colwidth, backgroundColor, textAlign } = cell.attrs;
|
|
10122
|
+
const paragraph = cell.firstChild;
|
|
10123
|
+
if (cell.type.name === "tableHeader" !== (rowIndex === 0) || colspan !== 1 || rowspan !== 1 || colwidth || backgroundColor || cell.childCount !== 1 || paragraph?.type.name !== "paragraph") return null;
|
|
10124
|
+
let hasBreak = false;
|
|
10125
|
+
paragraph.descendants((child) => {
|
|
10126
|
+
if (child.type.name === "hardBreak") hasBreak = true;
|
|
10127
|
+
});
|
|
10128
|
+
if (hasBreak) return null;
|
|
10129
|
+
if (rowIndex === 0) alignments.push(textAlign ?? null);
|
|
10130
|
+
else if ((textAlign ?? null) !== alignments[column]) return null;
|
|
10131
|
+
cells.push(inline(paragraph));
|
|
10132
|
+
}
|
|
10133
|
+
lines.push(`| ${cells.join(" | ")} |`);
|
|
10134
|
+
if (rowIndex === 0) lines.push(`| ${alignments.map((alignment) => ALIGNMENT[alignment] ?? "---").join(" | ")} |`);
|
|
10135
|
+
}
|
|
10136
|
+
return width ? lines.join("\n") : null;
|
|
10137
|
+
}
|
|
10138
|
+
return serializer.serialize(doc, { tightLists: true });
|
|
9369
10139
|
}
|
|
9370
10140
|
//#endregion
|
|
9371
10141
|
//#region src/editor/extensions/smart-paste.ts
|
|
@@ -9391,12 +10161,14 @@ function looksLikeMarkdown(value) {
|
|
|
9391
10161
|
"blockquote_open",
|
|
9392
10162
|
"fence",
|
|
9393
10163
|
"table_open",
|
|
9394
|
-
"looma_container_open"
|
|
10164
|
+
"looma_container_open",
|
|
10165
|
+
"looma_toc"
|
|
9395
10166
|
].includes(token.type)) return true;
|
|
9396
|
-
return token.type === "inline" && Boolean(token.children?.some((child) => [
|
|
9397
|
-
"link_open",
|
|
10167
|
+
return token.type === "inline" && Boolean(token.children?.some((child) => child.type === "link_open" && child.markup !== "linkify" || [
|
|
9398
10168
|
"image",
|
|
9399
|
-
"strong_open"
|
|
10169
|
+
"strong_open",
|
|
10170
|
+
"looma_chip",
|
|
10171
|
+
"looma_mention"
|
|
9400
10172
|
].includes(child.type)));
|
|
9401
10173
|
});
|
|
9402
10174
|
}
|
|
@@ -9411,7 +10183,7 @@ function hasRichClipboardStructure(value) {
|
|
|
9411
10183
|
if (body.querySelector("[data-pm-slice]") && !body.querySelector("pre")) return true;
|
|
9412
10184
|
return body.querySelectorAll("p").length > 1;
|
|
9413
10185
|
}
|
|
9414
|
-
function parseHtmlSlice(schema, value) {
|
|
10186
|
+
function parseHtmlSlice(schema, value, fromHtml = false) {
|
|
9415
10187
|
const parsed = new globalThis.DOMParser().parseFromString(value, "text/html");
|
|
9416
10188
|
if (parsed.body.style.display === "none") return Slice.empty;
|
|
9417
10189
|
for (const element of parsed.body.querySelectorAll("[style]")) if (element.style.display === "none") element.remove();
|
|
@@ -9422,7 +10194,7 @@ function parseHtmlSlice(schema, value) {
|
|
|
9422
10194
|
if (/\s$/.test(child.textContent) || /^\s/.test(next.textContent)) continue;
|
|
9423
10195
|
wrapper.insertBefore(parsed.createTextNode(" "), next);
|
|
9424
10196
|
}
|
|
9425
|
-
for (const pre of parsed.body.querySelectorAll("pre")) {
|
|
10197
|
+
for (const pre of fromHtml ? parsed.body.querySelectorAll("pre") : []) {
|
|
9426
10198
|
let last = pre;
|
|
9427
10199
|
while (last.lastChild) last = last.lastChild;
|
|
9428
10200
|
if (last.nodeType === Node.TEXT_NODE) last.textContent = last.textContent?.replace(/\r?\n$/, "") ?? "";
|
|
@@ -9439,28 +10211,24 @@ function parseHtmlSlice(schema, value) {
|
|
|
9439
10211
|
"template"
|
|
9440
10212
|
],
|
|
9441
10213
|
FORBID_ATTR: ["style"],
|
|
9442
|
-
ADD_TAGS: ["ui-callout"],
|
|
9443
|
-
ADD_ATTR: [
|
|
10214
|
+
ADD_TAGS: ["ui-callout", "ui-tabs"],
|
|
10215
|
+
ADD_ATTR: [
|
|
10216
|
+
"tone",
|
|
10217
|
+
"heading",
|
|
10218
|
+
"group"
|
|
10219
|
+
],
|
|
9444
10220
|
RETURN_DOM_FRAGMENT: true
|
|
9445
10221
|
}));
|
|
9446
10222
|
if (!container.textContent?.trim() && !container.querySelector("img, hr")) return Slice.empty;
|
|
9447
10223
|
return DOMParser.fromSchema(schema).parseSlice(container, { preserveWhitespace: false });
|
|
9448
10224
|
}
|
|
9449
|
-
function markdownFrontmatter(value) {
|
|
9450
|
-
const match = /^---[ \t]*\r?\n([\s\S]*?)\r?\n(?:---|\.\.\.)[ \t]*(?:\r?\n|$)/.exec(value.slice(0, 8192));
|
|
9451
|
-
if (!match || !/^[A-Za-z][\w-]*:[ \t]*\S/m.test(match[1])) return null;
|
|
9452
|
-
return {
|
|
9453
|
-
source: match[0].trimEnd(),
|
|
9454
|
-
body: value.slice(match[0].length)
|
|
9455
|
-
};
|
|
9456
|
-
}
|
|
9457
10225
|
function escapeHtmlText(value) {
|
|
9458
10226
|
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
9459
10227
|
}
|
|
9460
10228
|
function documentSlice(schema, plainText) {
|
|
9461
|
-
const frontmatter =
|
|
10229
|
+
const frontmatter = splitLoomaFrontmatter(plainText);
|
|
9462
10230
|
if (frontmatter || looksLikeMarkdown(plainText)) return parseHtmlSlice(schema, frontmatter ? `<pre><code class="language-yaml">${escapeHtmlText(frontmatter.source)}</code></pre>${loomaMarkdown.render(frontmatter.body)}` : loomaMarkdown.render(plainText));
|
|
9463
|
-
if (looksLikeDocumentHtml(plainText)) return parseHtmlSlice(schema, plainText);
|
|
10231
|
+
if (looksLikeDocumentHtml(plainText)) return parseHtmlSlice(schema, plainText, true);
|
|
9464
10232
|
return null;
|
|
9465
10233
|
}
|
|
9466
10234
|
/**
|
|
@@ -9992,6 +10760,16 @@ function setActiveTableCellBackground(editor, backgroundColor) {
|
|
|
9992
10760
|
* Uses the Vanilla JS Tiptap API; apps provide @tiptap/core and Looma ships the preset extensions.
|
|
9993
10761
|
*/
|
|
9994
10762
|
var LoomaCodeBlock = CodeBlockLowlight.extend({
|
|
10763
|
+
addAttributes() {
|
|
10764
|
+
return {
|
|
10765
|
+
...this.parent?.(),
|
|
10766
|
+
meta: {
|
|
10767
|
+
default: null,
|
|
10768
|
+
parseHTML: (element) => element.querySelector("code")?.getAttribute("data-meta") || element.getAttribute("data-meta") || null,
|
|
10769
|
+
renderHTML: () => ({})
|
|
10770
|
+
}
|
|
10771
|
+
};
|
|
10772
|
+
},
|
|
9995
10773
|
renderHTML({ node, HTMLAttributes }) {
|
|
9996
10774
|
return [
|
|
9997
10775
|
"pre",
|
|
@@ -10000,6 +10778,7 @@ var LoomaCodeBlock = CodeBlockLowlight.extend({
|
|
|
10000
10778
|
"code",
|
|
10001
10779
|
{
|
|
10002
10780
|
class: node.attrs.language ? `${this.options.languageClassPrefix}${node.attrs.language}` : null,
|
|
10781
|
+
"data-meta": node.attrs.meta || null,
|
|
10003
10782
|
spellcheck: "false"
|
|
10004
10783
|
},
|
|
10005
10784
|
0
|
|
@@ -10075,7 +10854,10 @@ function getDefaultEditorExtensions(options = {}) {
|
|
|
10075
10854
|
Heading.configure({ levels: [
|
|
10076
10855
|
1,
|
|
10077
10856
|
2,
|
|
10078
|
-
3
|
|
10857
|
+
3,
|
|
10858
|
+
4,
|
|
10859
|
+
5,
|
|
10860
|
+
6
|
|
10079
10861
|
] }),
|
|
10080
10862
|
BulletList,
|
|
10081
10863
|
OrderedList,
|
|
@@ -10085,6 +10867,9 @@ function getDefaultEditorExtensions(options = {}) {
|
|
|
10085
10867
|
Blockquote,
|
|
10086
10868
|
calloutNodeView ? LoomaCallout.extend({ addNodeView: () => calloutNodeView }) : LoomaCallout,
|
|
10087
10869
|
LoomaChip,
|
|
10870
|
+
LoomaTabs,
|
|
10871
|
+
LoomaTab,
|
|
10872
|
+
...imageInline ? [] : [LoomaFigure, LoomaFigcaption],
|
|
10088
10873
|
LoomaExpand.configure({ nodeView: expandNodeView ?? null }),
|
|
10089
10874
|
LoomaTableOfContents.configure({ nodeView: tableOfContentsNodeView ?? null }),
|
|
10090
10875
|
dividerNodeView ? HorizontalRule.extend({ addNodeView: () => dividerNodeView }) : HorizontalRule,
|
|
@@ -11076,4 +11861,4 @@ function createLoomaSlashCommandExtension(options = {}) {
|
|
|
11076
11861
|
});
|
|
11077
11862
|
}
|
|
11078
11863
|
//#endregion
|
|
11079
|
-
export { filterLoomaMentionItems as A,
|
|
11864
|
+
export { LoomaActiveBlockPluginKey as $, filterLoomaMentionItems as A, LoomaFigcaption as B, loomaMarkdownToHtml as C, createLoomaMentionExtension as D, LoomaMentionSuggestionPluginKey as E, LoomaTableOfContents as F, LoomaChip as G, LoomaExpand as H, getTableOfContentsEntries as I, LOOMA_CALLOUT_TONES as J, normalizeLoomaChipColor as K, normalizeTableOfContentsConfig as L, LoomaImage as M, normalizeImageDimension as N, DEFAULT_MENTION_RESULT_LIMIT as O, normalizeImagePlacement as P, LoomaActiveBlock as Q, LoomaTab as R, loomaMarkdownFromDoc as S, siteRelativeHref as T, revealLoomaExpandAt as U, LoomaFigure as V, LOOMA_CHIP_COLORS as W, LOOMA_ACTIVE_BLOCK_CLASS as X, LOOMA_ACTIVE_BLOCK_BLUR_GRACE_MS as Y, LOOMA_ACTIVE_BLOCK_TYPING_IDLE_MS as Z, getActiveTableCellAlignment as _, getActiveTableUiState as a, setActiveTableCellBackground as b, insertTableAtRange as c, LoomaTableKit as d, getDefaultEditorExtensions as f, LoomaTableHeader as g, LoomaTableCell as h, getDefaultSlashCommands as i, normalizeMentionResultLimit as j, MAX_MENTION_RESULT_LIMIT as k, normalizeActiveTableColumnWidths as l, LoomaTable as m, createLoomaSlashCommandExtension as n, handleTableAction as o, getLoomaTableExtensions as p, LoomaCallout as q, filterLoomaSlashCommands as r, handleTableOverlayAction as s, LoomaSlashCommand as t, shouldShowTextFormattingToolbar as u, getActiveTableCellBackground as v, createSiteRelativeLinks as w, LoomaSmartPaste as x, setActiveTableCellAlignment as y, LoomaTabs as z };
|