@threadlabs/looma 0.28.16 → 0.28.18
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 +431 -58
- 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 +5 -6
- 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.js +26 -2
- package/markdown/index.d.ts +20 -4
- package/markdown/index.js +41 -8
- package/markdown/markdown-it.d.ts +10 -2
- package/markdown/markdown-it.js +16 -13
- package/package.json +2 -1
- package/vue/editor/index.js +61 -19
|
@@ -242,6 +242,25 @@ function parseLoomaAttributes(source) {
|
|
|
242
242
|
return read && read.end === source.length ? read.attributes : null;
|
|
243
243
|
}
|
|
244
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
|
+
/**
|
|
245
264
|
* The anchor a heading's text derives: NFC, lowercase, keeping letters, numbers and combining
|
|
246
265
|
* marks, with every other run of characters one hyphen.
|
|
247
266
|
* @failure Text with no letters or numbers, such as only emoji, derives `heading`.
|
|
@@ -290,15 +309,46 @@ function parseLoomaFenceInfo(info) {
|
|
|
290
309
|
...numbers ? { lineNumbers: numbers[1] === void 0 } : {}
|
|
291
310
|
};
|
|
292
311
|
}
|
|
312
|
+
var MAPPING_KEY = /^(?:[^\s#:'"\-?,[\]{}&*!|>%@`][^:\n]*?|"[^"\n]*"|'[^'\n]*')[ \t]*:(?:[ \t]|$)/;
|
|
313
|
+
var NOTATION = /[ \t]*(?:\/\/|#|--|<!--|\{?\/\*)[ \t]*\[!code highlight(?::(\d+))?\][ \t]*(?:-->|\*\/\}?)?[ \t]*$/;
|
|
314
|
+
/**
|
|
315
|
+
* Applies code notation: a line ending in a `[!code highlight]` comment is highlighted and loses
|
|
316
|
+
* the comment, `[!code highlight:3]` covers three lines, and a marker alone on its line is removed
|
|
317
|
+
* with the line and applies to the lines after it.
|
|
318
|
+
* @contract Returns the code a reader sees and its highlighted line numbers, from 1, ascending.
|
|
319
|
+
* ponytail: matched on source lines, so a string that ends in the marker counts too.
|
|
320
|
+
*/
|
|
321
|
+
function applyLoomaCodeNotation(code) {
|
|
322
|
+
const kept = [];
|
|
323
|
+
const lines = /* @__PURE__ */ new Set();
|
|
324
|
+
for (const line of code.split("\n")) {
|
|
325
|
+
const match = NOTATION.exec(line);
|
|
326
|
+
if (!match) {
|
|
327
|
+
kept.push(line);
|
|
328
|
+
continue;
|
|
329
|
+
}
|
|
330
|
+
const rest = line.slice(0, match.index);
|
|
331
|
+
const own = rest.trim() !== "";
|
|
332
|
+
if (own) kept.push(rest);
|
|
333
|
+
for (let count = 0; count < Number(match[1] ?? 1); count++) lines.add(kept.length + (own ? 0 : 1) + count);
|
|
334
|
+
}
|
|
335
|
+
return {
|
|
336
|
+
code: kept.join("\n"),
|
|
337
|
+
lines: [...lines].sort((a, b) => a - b)
|
|
338
|
+
};
|
|
339
|
+
}
|
|
293
340
|
/**
|
|
294
341
|
* Splits YAML front matter from the start of a document.
|
|
295
|
-
* @contract Front matter opens with a `---` line at the very start
|
|
296
|
-
* line, and
|
|
342
|
+
* @contract Front matter opens with a `---` line at the very start and closes at a `---` or `...`
|
|
343
|
+
* line, and what lies between is a YAML mapping: its first line that is not blank or a comment
|
|
344
|
+
* starts with a key, whose value may be a scalar, a list or a mapping. `source` is the block as
|
|
345
|
+
* written, delimiters included.
|
|
297
346
|
* @failure Returns null when the document has none.
|
|
298
347
|
*/
|
|
299
348
|
function splitLoomaFrontmatter(document) {
|
|
300
349
|
const match = /^---[ \t]*\r?\n([\s\S]*?)\r?\n(?:---|\.\.\.)[ \t]*(?:\r?\n|$)/.exec(document.slice(0, 8192));
|
|
301
|
-
|
|
350
|
+
const first = match?.[1].split(/\r?\n/).find((line) => line.trim() && !line.trimStart().startsWith("#"));
|
|
351
|
+
if (!match || !first || !MAPPING_KEY.test(first)) return null;
|
|
302
352
|
return {
|
|
303
353
|
source: match[0].trimEnd(),
|
|
304
354
|
yaml: match[1],
|
|
@@ -625,6 +675,144 @@ var LoomaExpand = Node$1.create({
|
|
|
625
675
|
}
|
|
626
676
|
});
|
|
627
677
|
//#endregion
|
|
678
|
+
//#region src/editor/extensions/figure.ts
|
|
679
|
+
/**
|
|
680
|
+
* An image with a caption, saved as `<figure><img><figcaption>` and written in Looma Markdown as
|
|
681
|
+
* `::: figure Caption`. The caption is inline content the author edits in place.
|
|
682
|
+
* @invariant A figure holds exactly one block image and one caption; an empty caption stays empty.
|
|
683
|
+
*/
|
|
684
|
+
var LoomaFigure = Node$1.create({
|
|
685
|
+
name: "loomaFigure",
|
|
686
|
+
group: "block",
|
|
687
|
+
content: "image loomaFigcaption",
|
|
688
|
+
defining: true,
|
|
689
|
+
parseHTML() {
|
|
690
|
+
return [{
|
|
691
|
+
tag: "figure",
|
|
692
|
+
getAttrs: (element) => element.querySelector(":scope > img") ? null : false
|
|
693
|
+
}];
|
|
694
|
+
},
|
|
695
|
+
renderHTML() {
|
|
696
|
+
return ["figure", 0];
|
|
697
|
+
}
|
|
698
|
+
});
|
|
699
|
+
/**
|
|
700
|
+
* A figure's caption: inline content under its image.
|
|
701
|
+
* @invariant It exists only as the second child of `LoomaFigure`.
|
|
702
|
+
*/
|
|
703
|
+
var LoomaFigcaption = Node$1.create({
|
|
704
|
+
name: "loomaFigcaption",
|
|
705
|
+
content: "inline*",
|
|
706
|
+
defining: true,
|
|
707
|
+
parseHTML() {
|
|
708
|
+
return [{ tag: "figcaption" }];
|
|
709
|
+
},
|
|
710
|
+
renderHTML() {
|
|
711
|
+
return ["figcaption", 0];
|
|
712
|
+
}
|
|
713
|
+
});
|
|
714
|
+
//#endregion
|
|
715
|
+
//#region src/editor/extensions/tabs.ts
|
|
716
|
+
/**
|
|
717
|
+
* Tabs or a code group, kept whole so Looma Markdown's `::: tabs` and `::: code-group` survive
|
|
718
|
+
* editing. Saved as `<ui-tabs group>` with a `<section aria-label>` per panel.
|
|
719
|
+
* @contract While editing, panels show one after another under their labels; the Tabs component
|
|
720
|
+
* draws them only where the saved HTML renders.
|
|
721
|
+
*/
|
|
722
|
+
var LoomaTabs = Node$1.create({
|
|
723
|
+
name: "loomaTabs",
|
|
724
|
+
group: "block",
|
|
725
|
+
content: "loomaTab+",
|
|
726
|
+
defining: true,
|
|
727
|
+
isolating: true,
|
|
728
|
+
addAttributes() {
|
|
729
|
+
return {
|
|
730
|
+
group: {
|
|
731
|
+
default: "",
|
|
732
|
+
parseHTML: (element) => element.getAttribute("group") ?? "",
|
|
733
|
+
renderHTML: (attributes) => attributes.group ? { group: attributes.group } : {}
|
|
734
|
+
},
|
|
735
|
+
codeGroup: {
|
|
736
|
+
default: false,
|
|
737
|
+
parseHTML: (element) => element.classList.contains("code-group"),
|
|
738
|
+
renderHTML: (attributes) => attributes.codeGroup ? { class: "code-group" } : {}
|
|
739
|
+
}
|
|
740
|
+
};
|
|
741
|
+
},
|
|
742
|
+
parseHTML() {
|
|
743
|
+
return [{ tag: "ui-tabs" }];
|
|
744
|
+
},
|
|
745
|
+
renderHTML({ HTMLAttributes }) {
|
|
746
|
+
return [
|
|
747
|
+
"ui-tabs",
|
|
748
|
+
HTMLAttributes,
|
|
749
|
+
0
|
|
750
|
+
];
|
|
751
|
+
},
|
|
752
|
+
addNodeView() {
|
|
753
|
+
return () => {
|
|
754
|
+
const dom = document.createElement("div");
|
|
755
|
+
dom.dataset.loomaTabs = "";
|
|
756
|
+
return {
|
|
757
|
+
dom,
|
|
758
|
+
contentDOM: dom
|
|
759
|
+
};
|
|
760
|
+
};
|
|
761
|
+
}
|
|
762
|
+
});
|
|
763
|
+
/**
|
|
764
|
+
* One panel of `LoomaTabs`, labelled by `label`.
|
|
765
|
+
* @invariant It exists only inside `LoomaTabs`; its label is the tab Tabs shows and the `== Label` line Markdown writes.
|
|
766
|
+
*/
|
|
767
|
+
var LoomaTab = Node$1.create({
|
|
768
|
+
name: "loomaTab",
|
|
769
|
+
content: "block+",
|
|
770
|
+
defining: true,
|
|
771
|
+
isolating: true,
|
|
772
|
+
addAttributes() {
|
|
773
|
+
return { label: {
|
|
774
|
+
default: "",
|
|
775
|
+
parseHTML: (element) => element.getAttribute("aria-label") ?? "",
|
|
776
|
+
renderHTML: (attributes) => ({ "aria-label": attributes.label })
|
|
777
|
+
} };
|
|
778
|
+
},
|
|
779
|
+
parseHTML() {
|
|
780
|
+
return [{ tag: "ui-tabs > section" }];
|
|
781
|
+
},
|
|
782
|
+
renderHTML({ HTMLAttributes }) {
|
|
783
|
+
return [
|
|
784
|
+
"section",
|
|
785
|
+
HTMLAttributes,
|
|
786
|
+
0
|
|
787
|
+
];
|
|
788
|
+
},
|
|
789
|
+
addNodeView() {
|
|
790
|
+
return ({ node }) => {
|
|
791
|
+
let current = node;
|
|
792
|
+
const dom = document.createElement("section");
|
|
793
|
+
const label = document.createElement("div");
|
|
794
|
+
label.contentEditable = "false";
|
|
795
|
+
label.dataset.loomaTabLabel = "";
|
|
796
|
+
label.textContent = node.attrs.label;
|
|
797
|
+
const contentDOM = document.createElement("div");
|
|
798
|
+
dom.setAttribute("aria-label", node.attrs.label);
|
|
799
|
+
dom.append(label, contentDOM);
|
|
800
|
+
return {
|
|
801
|
+
dom,
|
|
802
|
+
contentDOM,
|
|
803
|
+
update(next) {
|
|
804
|
+
if (next.type !== current.type) return false;
|
|
805
|
+
current = next;
|
|
806
|
+
label.textContent = next.attrs.label;
|
|
807
|
+
dom.setAttribute("aria-label", next.attrs.label);
|
|
808
|
+
return true;
|
|
809
|
+
},
|
|
810
|
+
ignoreMutation: (mutation) => mutation.type !== "selection" && !contentDOM.contains(mutation.target)
|
|
811
|
+
};
|
|
812
|
+
};
|
|
813
|
+
}
|
|
814
|
+
});
|
|
815
|
+
//#endregion
|
|
628
816
|
//#region src/editor/extensions/table-of-contents.ts
|
|
629
817
|
/** Normalize loaded configuration without persisting unsupported presentation values. */
|
|
630
818
|
function normalizeTableOfContentsConfig(attributes) {
|
|
@@ -671,7 +859,17 @@ function tableOfContentsTree(entries) {
|
|
|
671
859
|
}
|
|
672
860
|
return roots;
|
|
673
861
|
}
|
|
674
|
-
|
|
862
|
+
var derivedFrom = (id, text) => {
|
|
863
|
+
const slug = loomaHeadingSlug(text);
|
|
864
|
+
return id === slug || id.startsWith(`${slug}-`) && /^\d+$/.test(id.slice(slug.length + 1));
|
|
865
|
+
};
|
|
866
|
+
/**
|
|
867
|
+
* Keeps heading anchors on Looma Markdown's rule. An anchor derived from its heading's text
|
|
868
|
+
* (as it read before this change) follows the text; any other, including `looma-heading-N` anchors
|
|
869
|
+
* saved before this rule, is a custom anchor and stays, so links to it keep working. With
|
|
870
|
+
* a table of contents every heading gets an anchor; without one, only headings that have one.
|
|
871
|
+
*/
|
|
872
|
+
function anchorTransaction(state, previous) {
|
|
675
873
|
let hasToc = false;
|
|
676
874
|
const headings = [];
|
|
677
875
|
state.doc.descendants((node, position) => {
|
|
@@ -681,24 +879,28 @@ function anchorTransaction(state) {
|
|
|
681
879
|
position
|
|
682
880
|
});
|
|
683
881
|
});
|
|
684
|
-
if (!
|
|
685
|
-
const
|
|
686
|
-
|
|
882
|
+
if (!headings.length) return null;
|
|
883
|
+
const before = /* @__PURE__ */ new Map();
|
|
884
|
+
previous?.descendants((node) => {
|
|
885
|
+
if (node.type.name === "heading" && typeof node.attrs.id === "string") before.set(node.attrs.id, node.textContent.trim());
|
|
886
|
+
});
|
|
887
|
+
const custom = (node) => {
|
|
888
|
+
const id = node.attrs.id;
|
|
889
|
+
if (typeof id !== "string" || !id || /\s/.test(id)) return void 0;
|
|
890
|
+
return derivedFrom(id, before.get(id) ?? node.textContent.trim()) ? void 0 : id;
|
|
891
|
+
};
|
|
892
|
+
const ids = assignLoomaHeadingIds(headings.map(({ node }) => ({
|
|
893
|
+
text: node.textContent.trim(),
|
|
894
|
+
id: custom(node)
|
|
895
|
+
})));
|
|
687
896
|
const transaction = state.tr;
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
transaction.setNodeMarkup(position, void 0, {
|
|
696
|
-
...node.attrs,
|
|
697
|
-
id
|
|
698
|
-
});
|
|
699
|
-
}
|
|
700
|
-
used.add(id);
|
|
701
|
-
}
|
|
897
|
+
headings.forEach(({ node, position }, index) => {
|
|
898
|
+
if (node.attrs.id === ids[index] || !hasToc && !node.attrs.id) return;
|
|
899
|
+
transaction.setNodeMarkup(position, void 0, {
|
|
900
|
+
...node.attrs,
|
|
901
|
+
id: ids[index]
|
|
902
|
+
});
|
|
903
|
+
});
|
|
702
904
|
return transaction.docChanged ? transaction : null;
|
|
703
905
|
}
|
|
704
906
|
var LoomaHeadingAnchors = Extension.create({
|
|
@@ -714,7 +916,7 @@ var LoomaHeadingAnchors = Extension.create({
|
|
|
714
916
|
if (transaction) this.editor.view.dispatch(transaction.setMeta("addToHistory", false));
|
|
715
917
|
},
|
|
716
918
|
addProseMirrorPlugins() {
|
|
717
|
-
return [new Plugin({ appendTransaction: (transactions,
|
|
919
|
+
return [new Plugin({ appendTransaction: (transactions, oldState, state) => transactions.some((transaction) => transaction.docChanged) ? anchorTransaction(state, oldState.doc) : null })];
|
|
718
920
|
}
|
|
719
921
|
});
|
|
720
922
|
function contentsHtml(doc, config) {
|
|
@@ -9332,7 +9534,7 @@ var TAB_LABEL = /^==[ \t]+(\S.*?)[ \t]*$/;
|
|
|
9332
9534
|
var OPEN_FLAG = /[ \t]*\{open\}$/;
|
|
9333
9535
|
var ALERT = /^\[!(NOTE|TIP|IMPORTANT|WARNING|CAUTION)\][ \t]*(?:\n|$)/i;
|
|
9334
9536
|
var LEAF = /^::([A-Za-z][\w-]*)(\{.*\})?[ \t]*$/;
|
|
9335
|
-
var TASK = /^\[([ xX])\][ \t]
|
|
9537
|
+
var TASK = /^\[([ xX])\](?:[ \t]+|$)/;
|
|
9336
9538
|
var TOC_FORMATS = /* @__PURE__ */ new Set([
|
|
9337
9539
|
"plain",
|
|
9338
9540
|
"bulleted",
|
|
@@ -9524,7 +9726,10 @@ function taskLists(state) {
|
|
|
9524
9726
|
inline.content = inline.content.replace(TASK, "");
|
|
9525
9727
|
item.attrSet("data-type", "taskItem");
|
|
9526
9728
|
item.attrSet("data-checked", String(checked));
|
|
9527
|
-
|
|
9729
|
+
inline.meta = {
|
|
9730
|
+
...inline.meta,
|
|
9731
|
+
task: checked
|
|
9732
|
+
};
|
|
9528
9733
|
}
|
|
9529
9734
|
}
|
|
9530
9735
|
}
|
|
@@ -9566,6 +9771,11 @@ function attributes(state) {
|
|
|
9566
9771
|
const token = tokens[index];
|
|
9567
9772
|
if (token.type !== "inline" || !token.children) continue;
|
|
9568
9773
|
const children = token.children;
|
|
9774
|
+
if (typeof token.meta?.task === "boolean") {
|
|
9775
|
+
const html = (content) => Object.assign(new state.Token("html_inline", "", 0), { content });
|
|
9776
|
+
children.unshift(html(`<label><input type="checkbox" disabled=""${token.meta.task ? " checked=\"\"" : ""}> `));
|
|
9777
|
+
children.push(html("</label>"));
|
|
9778
|
+
}
|
|
9569
9779
|
for (let child = children.length - 3; child >= 0; child--) {
|
|
9570
9780
|
const text = children[child + 1];
|
|
9571
9781
|
if (children[child].type !== "link_open" || children[child].markup !== "linkify" || /^(?:[a-z][\w+.-]*:|www\.)/i.test(text.content)) continue;
|
|
@@ -9642,7 +9852,7 @@ function tableOfContents(tokens, format, depth, escape) {
|
|
|
9642
9852
|
* @contract Renders the canonical HTML, which is the HTML LoomaEditor saves. It is not sanitized.
|
|
9643
9853
|
* Linkify and raw HTML are instance options; `createLoomaMarkdownIt` turns both on.
|
|
9644
9854
|
*/
|
|
9645
|
-
function loomaMarkdown$1(md) {
|
|
9855
|
+
function loomaMarkdown$1(md, { codeNotation = "apply" } = {}) {
|
|
9646
9856
|
const escape = md.utils.escapeHtml;
|
|
9647
9857
|
md.block.ruler.before("table", "looma_frontmatter", frontmatter);
|
|
9648
9858
|
md.block.ruler.before("fence", "looma_container", container, { alt: [
|
|
@@ -9693,15 +9903,10 @@ function loomaMarkdown$1(md) {
|
|
|
9693
9903
|
const id = String(attributes.id);
|
|
9694
9904
|
return `<span data-type="mention" class="looma-mention" data-id="${escape(id)}" data-label="${escape(label || id)}">@${escape(label || id)}</span>`;
|
|
9695
9905
|
};
|
|
9696
|
-
const listItem = rules.list_item_open ?? ((tokens, index, options, _env, self) => self.renderToken(tokens, index, options));
|
|
9697
|
-
rules.list_item_open = (tokens, index, options, env, self) => {
|
|
9698
|
-
const html = listItem(tokens, index, options, env, self);
|
|
9699
|
-
const task = tokens[index].meta;
|
|
9700
|
-
return task && typeof task.checked === "boolean" ? `${html}<input type="checkbox" disabled=""${task.checked ? " checked=\"\"" : ""}>` : html;
|
|
9701
|
-
};
|
|
9702
9906
|
const code = (content, info) => {
|
|
9703
9907
|
const { language, meta } = parseLoomaFenceInfo(info);
|
|
9704
|
-
|
|
9908
|
+
const source = content.replace(/\n$/, "");
|
|
9909
|
+
return `<pre><code${language ? ` class="language-${escape(language)}"` : ""}${meta ? ` data-meta="${escape(meta)}"` : ""}>${escape(codeNotation === "keep" ? source : applyLoomaCodeNotation(source).code)}</code></pre>\n`;
|
|
9705
9910
|
};
|
|
9706
9911
|
rules.fence = (tokens, index) => code(tokens[index].content, tokens[index].info);
|
|
9707
9912
|
rules.code_block = (tokens, index) => code(tokens[index].content, "");
|
|
@@ -9710,17 +9915,17 @@ function loomaMarkdown$1(md) {
|
|
|
9710
9915
|
* A markdown-it instance for Looma Markdown: raw HTML and GFM autolinks on, with `loomaMarkdown`.
|
|
9711
9916
|
* @contract Each call returns a new instance that callers may extend.
|
|
9712
9917
|
*/
|
|
9713
|
-
function createLoomaMarkdownIt() {
|
|
9918
|
+
function createLoomaMarkdownIt(options = {}) {
|
|
9714
9919
|
return new MarkdownIt({
|
|
9715
9920
|
html: true,
|
|
9716
9921
|
linkify: true,
|
|
9717
9922
|
typographer: false
|
|
9718
|
-
}).use(loomaMarkdown$1);
|
|
9923
|
+
}).use(loomaMarkdown$1, options);
|
|
9719
9924
|
}
|
|
9720
9925
|
//#endregion
|
|
9721
9926
|
//#region src/editor/extensions/markdown.ts
|
|
9722
|
-
/** The markdown-it instance LoomaEditor reads Looma Markdown with. */
|
|
9723
|
-
var loomaMarkdown = createLoomaMarkdownIt();
|
|
9927
|
+
/** The markdown-it instance LoomaEditor reads Looma Markdown with; code keeps its notation as source. */
|
|
9928
|
+
var loomaMarkdown = createLoomaMarkdownIt({ codeNotation: "keep" });
|
|
9724
9929
|
/**
|
|
9725
9930
|
* Renders Looma Markdown as the HTML the editor parses.
|
|
9726
9931
|
* @contract The result is not sanitized; pass it to the editor's `setContent`, whose schema
|
|
@@ -9729,29 +9934,46 @@ var loomaMarkdown = createLoomaMarkdownIt();
|
|
|
9729
9934
|
function loomaMarkdownToHtml(source) {
|
|
9730
9935
|
return loomaMarkdown.render(source);
|
|
9731
9936
|
}
|
|
9937
|
+
var CONTAINERS = /* @__PURE__ */ new Set([
|
|
9938
|
+
"loomaCallout",
|
|
9939
|
+
"loomaExpand",
|
|
9940
|
+
"loomaFigure",
|
|
9941
|
+
"loomaTabs"
|
|
9942
|
+
]);
|
|
9732
9943
|
var containerDepth = (node) => {
|
|
9733
9944
|
let depth = 0;
|
|
9734
9945
|
node.descendants((child) => {
|
|
9735
|
-
if (
|
|
9946
|
+
if (!CONTAINERS.has(child.type.name)) return true;
|
|
9947
|
+
depth = Math.max(depth, 1 + containerDepth(child));
|
|
9736
9948
|
return false;
|
|
9737
9949
|
});
|
|
9738
9950
|
return depth;
|
|
9739
9951
|
};
|
|
9740
|
-
|
|
9952
|
+
var endLine = (state) => state.flushClose(1);
|
|
9953
|
+
function writeContainer(state, node, opening, content = () => state.renderContent(node)) {
|
|
9741
9954
|
const marker = loomaContainerMarker(containerDepth(node));
|
|
9742
9955
|
state.write(`${marker} ${opening}`.trimEnd());
|
|
9743
9956
|
state.ensureNewLine();
|
|
9744
|
-
|
|
9745
|
-
state
|
|
9957
|
+
content();
|
|
9958
|
+
endLine(state);
|
|
9746
9959
|
state.write(marker);
|
|
9747
9960
|
state.closeBlock(node);
|
|
9748
9961
|
}
|
|
9749
9962
|
var defaults = defaultMarkdownSerializer.nodes;
|
|
9963
|
+
var label = (text) => text.replace(/[\\\]]/g, "\\$&");
|
|
9964
|
+
var ALIGNMENT = {
|
|
9965
|
+
left: ":---",
|
|
9966
|
+
center: ":---:",
|
|
9967
|
+
right: "---:"
|
|
9968
|
+
};
|
|
9750
9969
|
var NODES = {
|
|
9751
9970
|
paragraph: defaults.paragraph,
|
|
9752
|
-
text:
|
|
9971
|
+
text: (state, node, parent, index) => {
|
|
9972
|
+
const marker = /^:{3,}[ \t]*([A-Za-z][\w-]*)?/.exec(node.text ?? "");
|
|
9973
|
+
if ((marker ? !marker[1] || LOOMA_CONTAINER_NAMES.has(marker[1].toLowerCase()) : /^::toc(?![\w-])/i.test(node.text ?? "")) && state.atBlank()) state.write("\\");
|
|
9974
|
+
defaults.text(state, node, parent, index);
|
|
9975
|
+
},
|
|
9753
9976
|
blockquote: defaults.blockquote,
|
|
9754
|
-
heading: defaults.heading,
|
|
9755
9977
|
horizontalRule: defaults.horizontal_rule,
|
|
9756
9978
|
bulletList: (state, node) => state.renderList(node, " ", () => "- "),
|
|
9757
9979
|
orderedList: (state, node) => {
|
|
@@ -9759,17 +9981,82 @@ var NODES = {
|
|
|
9759
9981
|
state.renderList(node, " ", (index) => `${start + index}. `);
|
|
9760
9982
|
},
|
|
9761
9983
|
listItem: defaults.list_item,
|
|
9984
|
+
taskList: (state, node) => state.renderList(node, " ", () => "- "),
|
|
9985
|
+
taskItem: (state, node) => {
|
|
9986
|
+
state.write(node.attrs.checked ? "[x] " : "[ ] ");
|
|
9987
|
+
state.renderContent(node);
|
|
9988
|
+
},
|
|
9762
9989
|
hardBreak: defaults.hard_break,
|
|
9763
9990
|
codeBlock: (state, node) => {
|
|
9764
9991
|
const fence = loomaCodeFence(node.textContent);
|
|
9765
|
-
state.write(`${fence}${node.attrs.language ?? ""}\n`);
|
|
9992
|
+
state.write(`${fence}${node.attrs.language ?? ""}${node.attrs.meta ? ` ${node.attrs.meta}` : ""}\n`);
|
|
9766
9993
|
state.text(node.textContent, false);
|
|
9767
9994
|
state.ensureNewLine();
|
|
9768
9995
|
state.write(fence);
|
|
9769
9996
|
state.closeBlock(node);
|
|
9770
9997
|
},
|
|
9998
|
+
image: (state, node, parent, index) => {
|
|
9999
|
+
defaults.image(state, node, parent, index);
|
|
10000
|
+
const { width, height, placement, responsive } = node.attrs;
|
|
10001
|
+
state.write(formatLoomaAttributes({
|
|
10002
|
+
width,
|
|
10003
|
+
height,
|
|
10004
|
+
placement: placement && placement !== "block" ? placement : null,
|
|
10005
|
+
responsive: responsive === true
|
|
10006
|
+
}));
|
|
10007
|
+
if (node.isBlock) state.closeBlock(node);
|
|
10008
|
+
},
|
|
9771
10009
|
loomaCallout: (state, node) => writeContainer(state, node, `${node.attrs.tone} ${node.attrs.heading ?? ""}`),
|
|
9772
|
-
loomaExpand: (state, node) => writeContainer(state, node, `details ${node.attrs.summary}${node.attrs.open ? " {open}" : ""}`)
|
|
10010
|
+
loomaExpand: (state, node) => writeContainer(state, node, `details ${node.attrs.summary}${node.attrs.open ? " {open}" : ""}`),
|
|
10011
|
+
loomaFigure: (state, node) => {
|
|
10012
|
+
const caption = node.lastChild;
|
|
10013
|
+
const marker = loomaContainerMarker(containerDepth(node));
|
|
10014
|
+
state.write(`${marker} figure`);
|
|
10015
|
+
if (caption.content.size) {
|
|
10016
|
+
state.write(" ");
|
|
10017
|
+
state.renderInline(caption, false);
|
|
10018
|
+
}
|
|
10019
|
+
state.ensureNewLine();
|
|
10020
|
+
state.render(node.firstChild, node, 0);
|
|
10021
|
+
endLine(state);
|
|
10022
|
+
state.write(marker);
|
|
10023
|
+
state.closeBlock(node);
|
|
10024
|
+
},
|
|
10025
|
+
loomaFigcaption: (state, node) => state.renderInline(node),
|
|
10026
|
+
loomaTabs: (state, node) => {
|
|
10027
|
+
const name = node.attrs.codeGroup ? "code-group" : "tabs";
|
|
10028
|
+
const bare = node.attrs.codeGroup && Array.from({ length: node.childCount }, (_, index) => node.child(index)).every((tab) => {
|
|
10029
|
+
const code = tab.firstChild;
|
|
10030
|
+
if (tab.childCount !== 1 || code?.type.name !== "codeBlock") return false;
|
|
10031
|
+
const { title, language } = parseLoomaFenceInfo(`${code.attrs.language ?? ""} ${code.attrs.meta ?? ""}`);
|
|
10032
|
+
return tab.attrs.label === (title ?? language ?? "Code");
|
|
10033
|
+
});
|
|
10034
|
+
writeContainer(state, node, `${name} ${node.attrs.group && node.attrs.group !== name ? node.attrs.group : ""}`, () => node.forEach((tab, _offset, index) => {
|
|
10035
|
+
if (index) endLine(state);
|
|
10036
|
+
if (!bare) {
|
|
10037
|
+
state.write(`== ${tab.attrs.label}`);
|
|
10038
|
+
state.ensureNewLine();
|
|
10039
|
+
}
|
|
10040
|
+
state.renderContent(tab);
|
|
10041
|
+
}));
|
|
10042
|
+
},
|
|
10043
|
+
loomaTab: (state, node) => state.renderContent(node),
|
|
10044
|
+
loomaTableOfContents: (state, node) => {
|
|
10045
|
+
const { format, depth } = node.attrs;
|
|
10046
|
+
state.write(`::toc${formatLoomaAttributes({
|
|
10047
|
+
depth: depth !== 3 ? depth : null,
|
|
10048
|
+
format: format !== "plain" ? format : null
|
|
10049
|
+
})}`);
|
|
10050
|
+
state.closeBlock(node);
|
|
10051
|
+
},
|
|
10052
|
+
loomaChip: (state, node) => {
|
|
10053
|
+
const tone = normalizeLoomaChipTone(node.attrs.color);
|
|
10054
|
+
state.write(`:chip[${label(String(node.attrs.label ?? ""))}]${formatLoomaAttributes({ tone: tone !== "neutral" ? tone : null })}`);
|
|
10055
|
+
},
|
|
10056
|
+
mention: (state, node) => {
|
|
10057
|
+
const id = String(node.attrs.id ?? "");
|
|
10058
|
+
state.write(`:mention[${label(String(node.attrs.label ?? id))}]${formatLoomaAttributes({ id }, { hashId: false })}`);
|
|
10059
|
+
}
|
|
9773
10060
|
};
|
|
9774
10061
|
var MARKS = {
|
|
9775
10062
|
bold: defaultMarkdownSerializer.marks.strong,
|
|
@@ -9793,31 +10080,96 @@ var MARKS = {
|
|
|
9793
10080
|
mixable: true
|
|
9794
10081
|
}
|
|
9795
10082
|
};
|
|
10083
|
+
function customHeadingIds(doc) {
|
|
10084
|
+
const headings = [];
|
|
10085
|
+
doc.descendants((node) => {
|
|
10086
|
+
if (node.type.name === "heading") headings.push({
|
|
10087
|
+
text: node.textContent.trim(),
|
|
10088
|
+
id: typeof node.attrs.id === "string" && node.attrs.id ? node.attrs.id : void 0
|
|
10089
|
+
});
|
|
10090
|
+
});
|
|
10091
|
+
const marked = /* @__PURE__ */ new Set();
|
|
10092
|
+
for (;;) {
|
|
10093
|
+
const derived = assignLoomaHeadingIds(headings.map(({ text, id }, index) => ({
|
|
10094
|
+
text,
|
|
10095
|
+
id: marked.has(index) ? id : void 0
|
|
10096
|
+
})));
|
|
10097
|
+
const next = headings.findIndex(({ id }, index) => id && !marked.has(index) && derived[index] !== id);
|
|
10098
|
+
if (next < 0) return marked;
|
|
10099
|
+
marked.add(next);
|
|
10100
|
+
}
|
|
10101
|
+
}
|
|
9796
10102
|
/**
|
|
9797
|
-
* Writes an editor document as Looma Markdown,
|
|
9798
|
-
*
|
|
9799
|
-
*
|
|
9800
|
-
*
|
|
9801
|
-
* Marks without a Markdown form other than underline and highlight are dropped.
|
|
10103
|
+
* Writes an editor document as Looma Markdown, the form `loomaMarkdownToHtml` and Markupress read.
|
|
10104
|
+
* @contract Every node in the default preset has a Markdown form. A table that fits GFM is a GFM
|
|
10105
|
+
* table and any other is HTML. Content from extensions outside the preset is written as its saved
|
|
10106
|
+
* HTML, and marks without a Markdown form other than underline and highlight are dropped.
|
|
9802
10107
|
*/
|
|
9803
10108
|
function loomaMarkdownFromDoc(doc) {
|
|
9804
|
-
const
|
|
10109
|
+
const schema = doc.type.schema;
|
|
10110
|
+
const dom = DOMSerializer.fromSchema(schema);
|
|
9805
10111
|
const html = (node) => {
|
|
9806
10112
|
const holder = document.createElement("div");
|
|
9807
10113
|
holder.append(dom.serializeNode(node));
|
|
9808
10114
|
return holder.innerHTML;
|
|
9809
10115
|
};
|
|
9810
|
-
const
|
|
9811
|
-
|
|
10116
|
+
const custom = customHeadingIds(doc);
|
|
10117
|
+
let heading = 0;
|
|
10118
|
+
const nodes = {
|
|
10119
|
+
heading: (state, node) => {
|
|
10120
|
+
state.write(`${"#".repeat(node.attrs.level)} `);
|
|
10121
|
+
state.renderInline(node, false);
|
|
10122
|
+
if (custom.has(heading++)) state.write(` {#${node.attrs.id}}`);
|
|
10123
|
+
else {
|
|
10124
|
+
const out = state;
|
|
10125
|
+
out.out = out.out.replace(/([ \t])\{([^{}]*\})$/, "$1\\{$2");
|
|
10126
|
+
}
|
|
10127
|
+
state.closeBlock(node);
|
|
10128
|
+
},
|
|
10129
|
+
table: (state, node) => {
|
|
10130
|
+
state.write(gfmTable(node) ?? `<table><tbody>${Array.from({ length: node.childCount }, (_, index) => html(node.child(index))).join("")}</tbody></table>`.replace(/ (?:colspan|rowspan)="1"/g, ""));
|
|
10131
|
+
state.closeBlock(node);
|
|
10132
|
+
}
|
|
10133
|
+
};
|
|
10134
|
+
for (const name of Object.keys(schema.nodes)) nodes[name] ??= NODES[name] ?? ((state, node) => {
|
|
9812
10135
|
state.write(html(node));
|
|
9813
10136
|
if (node.isBlock) state.closeBlock(node);
|
|
9814
10137
|
});
|
|
9815
10138
|
const marks = {};
|
|
9816
|
-
for (const name of Object.keys(
|
|
10139
|
+
for (const name of Object.keys(schema.marks)) marks[name] = MARKS[name] ?? {
|
|
9817
10140
|
open: "",
|
|
9818
10141
|
close: ""
|
|
9819
10142
|
};
|
|
9820
|
-
|
|
10143
|
+
const serializer = new MarkdownSerializer(nodes, marks);
|
|
10144
|
+
const inline = (paragraph) => serializer.serialize(schema.topNodeType.create(null, paragraph)).replace(/\|/g, "\\|");
|
|
10145
|
+
function gfmTable(table) {
|
|
10146
|
+
const rows = Array.from({ length: table.childCount }, (_, index) => table.child(index));
|
|
10147
|
+
const width = rows[0]?.childCount ?? 0;
|
|
10148
|
+
const alignments = [];
|
|
10149
|
+
const lines = [];
|
|
10150
|
+
for (const [rowIndex, row] of rows.entries()) {
|
|
10151
|
+
if (row.childCount !== width) return null;
|
|
10152
|
+
const cells = [];
|
|
10153
|
+
for (let column = 0; column < width; column++) {
|
|
10154
|
+
const cell = row.child(column);
|
|
10155
|
+
const { colspan, rowspan, colwidth, backgroundColor, textAlign } = cell.attrs;
|
|
10156
|
+
const paragraph = cell.firstChild;
|
|
10157
|
+
if (cell.type.name === "tableHeader" !== (rowIndex === 0) || colspan !== 1 || rowspan !== 1 || colwidth || backgroundColor || cell.childCount !== 1 || paragraph?.type.name !== "paragraph") return null;
|
|
10158
|
+
let hasBreak = false;
|
|
10159
|
+
paragraph.descendants((child) => {
|
|
10160
|
+
if (child.type.name === "hardBreak") hasBreak = true;
|
|
10161
|
+
});
|
|
10162
|
+
if (hasBreak) return null;
|
|
10163
|
+
if (rowIndex === 0) alignments.push(textAlign ?? null);
|
|
10164
|
+
else if ((textAlign ?? null) !== alignments[column]) return null;
|
|
10165
|
+
cells.push(inline(paragraph));
|
|
10166
|
+
}
|
|
10167
|
+
lines.push(`| ${cells.join(" | ")} |`);
|
|
10168
|
+
if (rowIndex === 0) lines.push(`| ${alignments.map((alignment) => ALIGNMENT[alignment] ?? "---").join(" | ")} |`);
|
|
10169
|
+
}
|
|
10170
|
+
return width ? lines.join("\n") : null;
|
|
10171
|
+
}
|
|
10172
|
+
return serializer.serialize(doc, { tightLists: true });
|
|
9821
10173
|
}
|
|
9822
10174
|
//#endregion
|
|
9823
10175
|
//#region src/editor/extensions/smart-paste.ts
|
|
@@ -9893,8 +10245,12 @@ function parseHtmlSlice(schema, value, fromHtml = false) {
|
|
|
9893
10245
|
"template"
|
|
9894
10246
|
],
|
|
9895
10247
|
FORBID_ATTR: ["style"],
|
|
9896
|
-
ADD_TAGS: ["ui-callout"],
|
|
9897
|
-
ADD_ATTR: [
|
|
10248
|
+
ADD_TAGS: ["ui-callout", "ui-tabs"],
|
|
10249
|
+
ADD_ATTR: [
|
|
10250
|
+
"tone",
|
|
10251
|
+
"heading",
|
|
10252
|
+
"group"
|
|
10253
|
+
],
|
|
9898
10254
|
RETURN_DOM_FRAGMENT: true
|
|
9899
10255
|
}));
|
|
9900
10256
|
if (!container.textContent?.trim() && !container.querySelector("img, hr")) return Slice.empty;
|
|
@@ -10438,6 +10794,16 @@ function setActiveTableCellBackground(editor, backgroundColor) {
|
|
|
10438
10794
|
* Uses the Vanilla JS Tiptap API; apps provide @tiptap/core and Looma ships the preset extensions.
|
|
10439
10795
|
*/
|
|
10440
10796
|
var LoomaCodeBlock = CodeBlockLowlight.extend({
|
|
10797
|
+
addAttributes() {
|
|
10798
|
+
return {
|
|
10799
|
+
...this.parent?.(),
|
|
10800
|
+
meta: {
|
|
10801
|
+
default: null,
|
|
10802
|
+
parseHTML: (element) => element.querySelector("code")?.getAttribute("data-meta") || element.getAttribute("data-meta") || null,
|
|
10803
|
+
renderHTML: () => ({})
|
|
10804
|
+
}
|
|
10805
|
+
};
|
|
10806
|
+
},
|
|
10441
10807
|
renderHTML({ node, HTMLAttributes }) {
|
|
10442
10808
|
return [
|
|
10443
10809
|
"pre",
|
|
@@ -10446,6 +10812,7 @@ var LoomaCodeBlock = CodeBlockLowlight.extend({
|
|
|
10446
10812
|
"code",
|
|
10447
10813
|
{
|
|
10448
10814
|
class: node.attrs.language ? `${this.options.languageClassPrefix}${node.attrs.language}` : null,
|
|
10815
|
+
"data-meta": node.attrs.meta || null,
|
|
10449
10816
|
spellcheck: "false"
|
|
10450
10817
|
},
|
|
10451
10818
|
0
|
|
@@ -10521,7 +10888,10 @@ function getDefaultEditorExtensions(options = {}) {
|
|
|
10521
10888
|
Heading.configure({ levels: [
|
|
10522
10889
|
1,
|
|
10523
10890
|
2,
|
|
10524
|
-
3
|
|
10891
|
+
3,
|
|
10892
|
+
4,
|
|
10893
|
+
5,
|
|
10894
|
+
6
|
|
10525
10895
|
] }),
|
|
10526
10896
|
BulletList,
|
|
10527
10897
|
OrderedList,
|
|
@@ -10531,6 +10901,9 @@ function getDefaultEditorExtensions(options = {}) {
|
|
|
10531
10901
|
Blockquote,
|
|
10532
10902
|
calloutNodeView ? LoomaCallout.extend({ addNodeView: () => calloutNodeView }) : LoomaCallout,
|
|
10533
10903
|
LoomaChip,
|
|
10904
|
+
LoomaTabs,
|
|
10905
|
+
LoomaTab,
|
|
10906
|
+
...imageInline ? [] : [LoomaFigure, LoomaFigcaption],
|
|
10534
10907
|
LoomaExpand.configure({ nodeView: expandNodeView ?? null }),
|
|
10535
10908
|
LoomaTableOfContents.configure({ nodeView: tableOfContentsNodeView ?? null }),
|
|
10536
10909
|
dividerNodeView ? HorizontalRule.extend({ addNodeView: () => dividerNodeView }) : HorizontalRule,
|
|
@@ -11522,4 +11895,4 @@ function createLoomaSlashCommandExtension(options = {}) {
|
|
|
11522
11895
|
});
|
|
11523
11896
|
}
|
|
11524
11897
|
//#endregion
|
|
11525
|
-
export { filterLoomaMentionItems as A,
|
|
11898
|
+
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 };
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { Node } from "@tiptap/core";
|
|
2
|
+
/**
|
|
3
|
+
* An image with a caption, saved as `<figure><img><figcaption>` and written in Looma Markdown as
|
|
4
|
+
* `::: figure Caption`. The caption is inline content the author edits in place.
|
|
5
|
+
* @invariant A figure holds exactly one block image and one caption; an empty caption stays empty.
|
|
6
|
+
*/
|
|
7
|
+
export declare const LoomaFigure: Node<any, any>;
|
|
8
|
+
/**
|
|
9
|
+
* A figure's caption: inline content under its image.
|
|
10
|
+
* @invariant It exists only as the second child of `LoomaFigure`.
|
|
11
|
+
*/
|
|
12
|
+
export declare const LoomaFigcaption: Node<any, any>;
|
|
@@ -10,6 +10,8 @@ export { LOOMA_ACTIVE_BLOCK_BLUR_GRACE_MS, LOOMA_ACTIVE_BLOCK_CLASS, LOOMA_ACTIV
|
|
|
10
10
|
export { LoomaCallout, LOOMA_CALLOUT_TONES, type LoomaCalloutTone, } from "./callout.js";
|
|
11
11
|
export { LoomaChip, LOOMA_CHIP_COLORS, normalizeLoomaChipColor, type LoomaChipColor } from "./chip.js";
|
|
12
12
|
export { LoomaExpand, revealLoomaExpandAt, type LoomaExpandAttributes } from "./expand.js";
|
|
13
|
+
export { LoomaFigcaption, LoomaFigure } from "./figure.js";
|
|
14
|
+
export { LoomaTab, LoomaTabs } from "./tabs.js";
|
|
13
15
|
export { LoomaTableOfContents, getTableOfContentsEntries, normalizeTableOfContentsConfig, type LoomaTableOfContentsConfig, type LoomaTableOfContentsEntry, type LoomaTableOfContentsFormat, } from "./table-of-contents.js";
|
|
14
16
|
export { getDefaultEditorExtensions, getLoomaTableExtensions, LoomaTableKit, type DefaultEditorExtensionsOptions, type LoomaCodeLanguages, } from "./preset.js";
|
|
15
17
|
export { LoomaSmartPaste } from "./smart-paste.js";
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import { n as TABLE_CELL_BACKGROUND_PRESETS } from "../chunks/table-backgrounds.js";
|
|
2
|
-
import { A as filterLoomaMentionItems, B as
|
|
3
|
-
export { DEFAULT_MENTION_RESULT_LIMIT, LOOMA_ACTIVE_BLOCK_BLUR_GRACE_MS, LOOMA_ACTIVE_BLOCK_CLASS, LOOMA_ACTIVE_BLOCK_TYPING_IDLE_MS, LOOMA_CALLOUT_TONES, LOOMA_CHIP_COLORS, LoomaActiveBlock, LoomaActiveBlockPluginKey, LoomaCallout, LoomaChip, LoomaExpand, LoomaImage, LoomaMentionSuggestionPluginKey, LoomaSlashCommand, LoomaSmartPaste, LoomaTable, LoomaTableCell, LoomaTableHeader, LoomaTableKit, LoomaTableOfContents, MAX_MENTION_RESULT_LIMIT, TABLE_CELL_BACKGROUND_PRESETS, createLoomaMentionExtension, createLoomaSlashCommandExtension, createSiteRelativeLinks, filterLoomaMentionItems, filterLoomaSlashCommands, getActiveTableCellAlignment, getActiveTableCellBackground, getActiveTableUiState, getDefaultEditorExtensions, getDefaultSlashCommands, getLoomaTableExtensions, getTableOfContentsEntries, handleTableAction, handleTableOverlayAction, insertTableAtRange, loomaMarkdownFromDoc, loomaMarkdownToHtml, normalizeActiveTableColumnWidths, normalizeImageDimension, normalizeImagePlacement, normalizeLoomaChipColor, normalizeMentionResultLimit, normalizeTableOfContentsConfig, revealLoomaExpandAt, setActiveTableCellAlignment, setActiveTableCellBackground, shouldShowTextFormattingToolbar, siteRelativeHref };
|
|
2
|
+
import { $ as LoomaActiveBlockPluginKey, A as filterLoomaMentionItems, B as LoomaFigcaption, C as loomaMarkdownToHtml, D as createLoomaMentionExtension, E as LoomaMentionSuggestionPluginKey, F as LoomaTableOfContents, G as LoomaChip, H as LoomaExpand, I as getTableOfContentsEntries, J as LOOMA_CALLOUT_TONES, K as normalizeLoomaChipColor, L as normalizeTableOfContentsConfig, M as LoomaImage, N as normalizeImageDimension, O as DEFAULT_MENTION_RESULT_LIMIT, P as normalizeImagePlacement, Q as LoomaActiveBlock, R as LoomaTab, S as loomaMarkdownFromDoc, T as siteRelativeHref, U as revealLoomaExpandAt, V as LoomaFigure, W as LOOMA_CHIP_COLORS, X as LOOMA_ACTIVE_BLOCK_CLASS, Y as LOOMA_ACTIVE_BLOCK_BLUR_GRACE_MS, Z as LOOMA_ACTIVE_BLOCK_TYPING_IDLE_MS, _ as getActiveTableCellAlignment, a as getActiveTableUiState, b as setActiveTableCellBackground, c as insertTableAtRange, d as LoomaTableKit, f as getDefaultEditorExtensions, g as LoomaTableHeader, h as LoomaTableCell, i as getDefaultSlashCommands, j as normalizeMentionResultLimit, k as MAX_MENTION_RESULT_LIMIT, l as normalizeActiveTableColumnWidths, m as LoomaTable, n as createLoomaSlashCommandExtension, o as handleTableAction, p as getLoomaTableExtensions, q as LoomaCallout, r as filterLoomaSlashCommands, s as handleTableOverlayAction, t as LoomaSlashCommand, u as shouldShowTextFormattingToolbar, v as getActiveTableCellBackground, w as createSiteRelativeLinks, x as LoomaSmartPaste, y as setActiveTableCellAlignment, z as LoomaTabs } from "../chunks/extensions.js";
|
|
3
|
+
export { DEFAULT_MENTION_RESULT_LIMIT, LOOMA_ACTIVE_BLOCK_BLUR_GRACE_MS, LOOMA_ACTIVE_BLOCK_CLASS, LOOMA_ACTIVE_BLOCK_TYPING_IDLE_MS, LOOMA_CALLOUT_TONES, LOOMA_CHIP_COLORS, LoomaActiveBlock, LoomaActiveBlockPluginKey, LoomaCallout, LoomaChip, LoomaExpand, LoomaFigcaption, LoomaFigure, LoomaImage, LoomaMentionSuggestionPluginKey, LoomaSlashCommand, LoomaSmartPaste, LoomaTab, LoomaTable, LoomaTableCell, LoomaTableHeader, LoomaTableKit, LoomaTableOfContents, LoomaTabs, MAX_MENTION_RESULT_LIMIT, TABLE_CELL_BACKGROUND_PRESETS, createLoomaMentionExtension, createLoomaSlashCommandExtension, createSiteRelativeLinks, filterLoomaMentionItems, filterLoomaSlashCommands, getActiveTableCellAlignment, getActiveTableCellBackground, getActiveTableUiState, getDefaultEditorExtensions, getDefaultSlashCommands, getLoomaTableExtensions, getTableOfContentsEntries, handleTableAction, handleTableOverlayAction, insertTableAtRange, loomaMarkdownFromDoc, loomaMarkdownToHtml, normalizeActiveTableColumnWidths, normalizeImageDimension, normalizeImagePlacement, normalizeLoomaChipColor, normalizeMentionResultLimit, normalizeTableOfContentsConfig, revealLoomaExpandAt, setActiveTableCellAlignment, setActiveTableCellBackground, shouldShowTextFormattingToolbar, siteRelativeHref };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type Node as ProseMirrorNode } from "@tiptap/pm/model";
|
|
2
|
-
/** The markdown-it instance LoomaEditor reads Looma Markdown with. */
|
|
2
|
+
/** The markdown-it instance LoomaEditor reads Looma Markdown with; code keeps its notation as source. */
|
|
3
3
|
export declare const loomaMarkdown: import("markdown-it").default;
|
|
4
4
|
/**
|
|
5
5
|
* Renders Looma Markdown as the HTML the editor parses.
|
|
@@ -8,10 +8,9 @@ export declare const loomaMarkdown: import("markdown-it").default;
|
|
|
8
8
|
*/
|
|
9
9
|
export declare function loomaMarkdownToHtml(source: string): string;
|
|
10
10
|
/**
|
|
11
|
-
* Writes an editor document as Looma Markdown,
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* Marks without a Markdown form other than underline and highlight are dropped.
|
|
11
|
+
* Writes an editor document as Looma Markdown, the form `loomaMarkdownToHtml` and Markupress read.
|
|
12
|
+
* @contract Every node in the default preset has a Markdown form. A table that fits GFM is a GFM
|
|
13
|
+
* table and any other is HTML. Content from extensions outside the preset is written as its saved
|
|
14
|
+
* HTML, and marks without a Markdown form other than underline and highlight are dropped.
|
|
16
15
|
*/
|
|
17
16
|
export declare function loomaMarkdownFromDoc(doc: ProseMirrorNode): string;
|
|
@@ -10,7 +10,7 @@ export interface LoomaTableOfContentsConfig {
|
|
|
10
10
|
/**
|
|
11
11
|
* One current heading for navigation or host projections.
|
|
12
12
|
* @lifecycle Positions belong to the supplied document; recompute after edits.
|
|
13
|
-
*
|
|
13
|
+
* An ID derived from a heading's text follows the text; a custom ID stays with its heading.
|
|
14
14
|
*/
|
|
15
15
|
export interface LoomaTableOfContentsEntry {
|
|
16
16
|
id: string;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { Node } from "@tiptap/core";
|
|
2
|
+
/**
|
|
3
|
+
* Tabs or a code group, kept whole so Looma Markdown's `::: tabs` and `::: code-group` survive
|
|
4
|
+
* editing. Saved as `<ui-tabs group>` with a `<section aria-label>` per panel.
|
|
5
|
+
* @contract While editing, panels show one after another under their labels; the Tabs component
|
|
6
|
+
* draws them only where the saved HTML renders.
|
|
7
|
+
*/
|
|
8
|
+
export declare const LoomaTabs: Node<any, any>;
|
|
9
|
+
/**
|
|
10
|
+
* One panel of `LoomaTabs`, labelled by `label`.
|
|
11
|
+
* @invariant It exists only inside `LoomaTabs`; its label is the tab Tabs shows and the `== Label` line Markdown writes.
|
|
12
|
+
*/
|
|
13
|
+
export declare const LoomaTab: Node<any, any>;
|
package/editor/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { n as TABLE_CELL_BACKGROUND_PRESETS, t as TABLE_CELL_BACKGROUND_OPTIONS } from "./chunks/table-backgrounds.js";
|
|
2
2
|
import { n as resolveTableCellAt, t as measureTableOverlayGeometry } from "./chunks/ui.js";
|
|
3
|
-
import { A as filterLoomaMentionItems, B as
|
|
3
|
+
import { $ as LoomaActiveBlockPluginKey, A as filterLoomaMentionItems, B as LoomaFigcaption, C as loomaMarkdownToHtml, D as createLoomaMentionExtension, E as LoomaMentionSuggestionPluginKey, F as LoomaTableOfContents, G as LoomaChip, H as LoomaExpand, I as getTableOfContentsEntries, J as LOOMA_CALLOUT_TONES, K as normalizeLoomaChipColor, L as normalizeTableOfContentsConfig, M as LoomaImage, N as normalizeImageDimension, O as DEFAULT_MENTION_RESULT_LIMIT, P as normalizeImagePlacement, Q as LoomaActiveBlock, R as LoomaTab, S as loomaMarkdownFromDoc, T as siteRelativeHref, U as revealLoomaExpandAt, V as LoomaFigure, W as LOOMA_CHIP_COLORS, X as LOOMA_ACTIVE_BLOCK_CLASS, Y as LOOMA_ACTIVE_BLOCK_BLUR_GRACE_MS, Z as LOOMA_ACTIVE_BLOCK_TYPING_IDLE_MS, _ as getActiveTableCellAlignment, a as getActiveTableUiState, b as setActiveTableCellBackground, c as insertTableAtRange, d as LoomaTableKit, f as getDefaultEditorExtensions, g as LoomaTableHeader, h as LoomaTableCell, i as getDefaultSlashCommands, j as normalizeMentionResultLimit, k as MAX_MENTION_RESULT_LIMIT, l as normalizeActiveTableColumnWidths, m as LoomaTable, n as createLoomaSlashCommandExtension, o as handleTableAction, p as getLoomaTableExtensions, q as LoomaCallout, r as filterLoomaSlashCommands, s as handleTableOverlayAction, t as LoomaSlashCommand, u as shouldShowTextFormattingToolbar, v as getActiveTableCellBackground, w as createSiteRelativeLinks, x as LoomaSmartPaste, y as setActiveTableCellAlignment, z as LoomaTabs } from "./chunks/extensions.js";
|
|
4
4
|
import { AlignCenter, AlignLeft, AlignRight, ArrowLeft, Bell, Bold, BookUser, Braces, Calculator, Check, ChevronDown, ChevronLeft, ChevronRight, CircleCheck, CircleDot, CircleQuestionMark, CircleX, CodeXml, Columns3, Compass, CreditCard, Ellipsis, Eraser, ExternalLink, FileText, Folder, GripHorizontal, GripVertical, Heading1, Heading2, Heading3, Highlighter, History, Image, Info, Italic, LayoutDashboard, Lightbulb, Link, List, ListOrdered, ListTodo, LoaderCircle, Merge, MessageSquareWarning, Minus, NotebookPen, PaintBucket, PanelBottom, PanelLeft, PanelRight, PanelTop, Pencil, Pilcrow, Plus, Quote, Receipt, Redo2, Rows3, Search, Settings, Split, Strikethrough, Table2, Tag, Trash2, TriangleAlert, Truck, Underline, Undo2, Unlink, UserRound, Users, X } from "lucide";
|
|
5
5
|
//#region src/editor/icons.ts
|
|
6
6
|
/** The opinionated Lucide set used by Looma's shipped interaction surfaces. */
|
|
@@ -123,4 +123,4 @@ function getVisualViewportRect(owner = window) {
|
|
|
123
123
|
};
|
|
124
124
|
}
|
|
125
125
|
//#endregion
|
|
126
|
-
export { DEFAULT_MENTION_RESULT_LIMIT, LOOMA_ACTIVE_BLOCK_BLUR_GRACE_MS, LOOMA_ACTIVE_BLOCK_CLASS, LOOMA_ACTIVE_BLOCK_TYPING_IDLE_MS, LOOMA_CALLOUT_TONES, LOOMA_CHIP_COLORS, LOOMA_ICONS, LoomaActiveBlock, LoomaActiveBlockPluginKey, LoomaCallout, LoomaChip, LoomaExpand, LoomaImage, LoomaMentionSuggestionPluginKey, LoomaSlashCommand, LoomaSmartPaste, LoomaTable, LoomaTableCell, LoomaTableHeader, LoomaTableKit, LoomaTableOfContents, MAX_MENTION_RESULT_LIMIT, TABLE_CELL_BACKGROUND_OPTIONS, TABLE_CELL_BACKGROUND_PRESETS, createLoomaMentionExtension, createLoomaSlashCommandExtension, createSiteRelativeLinks, filterLoomaMentionItems, filterLoomaSlashCommands, getActiveTableCellAlignment, getActiveTableCellBackground, getActiveTableUiState, getDefaultEditorExtensions, getDefaultSlashCommands, getLoomaTableExtensions, getTableOfContentsEntries, getVisualViewportRect, handleTableAction, handleTableOverlayAction, insertTableAtRange, loomaIconMarkup, loomaMarkdownFromDoc, loomaMarkdownToHtml, measureTableOverlayGeometry, normalizeActiveTableColumnWidths, normalizeImageDimension, normalizeImagePlacement, normalizeLoomaChipColor, normalizeMentionResultLimit, normalizeTableOfContentsConfig, resolveTableCellAt, revealLoomaExpandAt, setActiveTableCellAlignment, setActiveTableCellBackground, shouldShowTextFormattingToolbar, siteRelativeHref };
|
|
126
|
+
export { DEFAULT_MENTION_RESULT_LIMIT, LOOMA_ACTIVE_BLOCK_BLUR_GRACE_MS, LOOMA_ACTIVE_BLOCK_CLASS, LOOMA_ACTIVE_BLOCK_TYPING_IDLE_MS, LOOMA_CALLOUT_TONES, LOOMA_CHIP_COLORS, LOOMA_ICONS, LoomaActiveBlock, LoomaActiveBlockPluginKey, LoomaCallout, LoomaChip, LoomaExpand, LoomaFigcaption, LoomaFigure, LoomaImage, LoomaMentionSuggestionPluginKey, LoomaSlashCommand, LoomaSmartPaste, LoomaTab, LoomaTable, LoomaTableCell, LoomaTableHeader, LoomaTableKit, LoomaTableOfContents, LoomaTabs, MAX_MENTION_RESULT_LIMIT, TABLE_CELL_BACKGROUND_OPTIONS, TABLE_CELL_BACKGROUND_PRESETS, createLoomaMentionExtension, createLoomaSlashCommandExtension, createSiteRelativeLinks, filterLoomaMentionItems, filterLoomaSlashCommands, getActiveTableCellAlignment, getActiveTableCellBackground, getActiveTableUiState, getDefaultEditorExtensions, getDefaultSlashCommands, getLoomaTableExtensions, getTableOfContentsEntries, getVisualViewportRect, handleTableAction, handleTableOverlayAction, insertTableAtRange, loomaIconMarkup, loomaMarkdownFromDoc, loomaMarkdownToHtml, measureTableOverlayGeometry, normalizeActiveTableColumnWidths, normalizeImageDimension, normalizeImagePlacement, normalizeLoomaChipColor, normalizeMentionResultLimit, normalizeTableOfContentsConfig, resolveTableCellAt, revealLoomaExpandAt, setActiveTableCellAlignment, setActiveTableCellBackground, shouldShowTextFormattingToolbar, siteRelativeHref };
|
package/markdown/fixtures.js
CHANGED
|
@@ -14,6 +14,8 @@ var files = /* #__PURE__ */ Object.assign({
|
|
|
14
14
|
"../../markdown-fixtures/callouts/open-in-title.md": "::: tip Keep {open}\nA callout title keeps its braces.\n:::\n",
|
|
15
15
|
"../../markdown-fixtures/callouts/titled.html": "<ui-callout tone=\"tip\" heading=\"Before you start\">\n<p>Install <strong>first</strong>.</p>\n</ui-callout>\n",
|
|
16
16
|
"../../markdown-fixtures/callouts/titled.md": "::: tip Before you start\nInstall **first**.\n:::\n",
|
|
17
|
+
"../../markdown-fixtures/chips/escaped-label.html": "<p><span data-looma-chip data-label=\"Draft ] v2\" data-color=\"red\">Draft ] v2</span></p>\n",
|
|
18
|
+
"../../markdown-fixtures/chips/escaped-label.md": ":chip[Draft \\] v2]{tone=red}\n",
|
|
17
19
|
"../../markdown-fixtures/chips/tones.html": "<p>Status <span data-looma-chip data-label=\"Ready\" data-color=\"green\">Ready</span> and <span data-looma-chip data-label=\"Draft\" data-color=\"neutral\">Draft</span>.</p>\n",
|
|
18
20
|
"../../markdown-fixtures/chips/tones.md": "Status :chip[Ready]{tone=green} and :chip[Draft].\n",
|
|
19
21
|
"../../markdown-fixtures/code-blocks/fences.canonical.md": "```ts\nconst answer = 42;\n```\n\n```\nplain text\n```\n\n```\nindented\n```\n",
|
|
@@ -21,10 +23,14 @@ var files = /* #__PURE__ */ Object.assign({
|
|
|
21
23
|
"../../markdown-fixtures/code-blocks/fences.md": "```ts\nconst answer = 42;\n```\n\n```\nplain text\n```\n\n indented\n",
|
|
22
24
|
"../../markdown-fixtures/code-blocks/long-fence.html": "<pre><code class=\"language-md\">```ts\nconst a = 1;\n```</code></pre>\n",
|
|
23
25
|
"../../markdown-fixtures/code-blocks/long-fence.md": "````md\n```ts\nconst a = 1;\n```\n````\n",
|
|
24
|
-
"../../markdown-fixtures/code-meta/notation.html": "<pre><code class=\"language-ts\">const
|
|
26
|
+
"../../markdown-fixtures/code-meta/notation-alone.html": "<pre><code class=\"language-ts\">const host = "localhost";\nconst port = 8080;</code></pre>\n",
|
|
27
|
+
"../../markdown-fixtures/code-meta/notation-alone.md": "```ts\n// [!code highlight:2]\nconst host = \"localhost\";\nconst port = 8080;\n```\n",
|
|
28
|
+
"../../markdown-fixtures/code-meta/notation.html": "<pre><code class=\"language-ts\">const port = 8080;</code></pre>\n",
|
|
25
29
|
"../../markdown-fixtures/code-meta/notation.md": "```ts\nconst port = 8080; // [!code highlight]\n```\n",
|
|
26
30
|
"../../markdown-fixtures/code-meta/title-lines.html": "<pre><code class=\"language-ts\" data-meta=\"title="config.ts" {1,3-4} :line-numbers\">export default {\n name: "docs",\n base: "/docs/",\n};</code></pre>\n",
|
|
27
31
|
"../../markdown-fixtures/code-meta/title-lines.md": "```ts title=\"config.ts\" {1,3-4} :line-numbers\nexport default {\n name: \"docs\",\n base: \"/docs/\",\n};\n```\n",
|
|
32
|
+
"../../markdown-fixtures/containers/escaped-marker.html": "<p>::: tip is text here</p>\n",
|
|
33
|
+
"../../markdown-fixtures/containers/escaped-marker.md": "\\::: tip is text here\n",
|
|
28
34
|
"../../markdown-fixtures/containers/fence-inside.html": "<ui-callout tone=\"tip\">\n<pre><code class=\"language-md\">:::</code></pre>\n</ui-callout>\n",
|
|
29
35
|
"../../markdown-fixtures/containers/fence-inside.md": "::: tip\n```md\n:::\n```\n:::\n",
|
|
30
36
|
"../../markdown-fixtures/containers/nesting.canonical.md": ":::: details More\n::: warning\nNested.\n:::\n::::\n",
|
|
@@ -43,8 +49,18 @@ var files = /* #__PURE__ */ Object.assign({
|
|
|
43
49
|
"../../markdown-fixtures/details/open.md": "::: details Show the full log {open}\nThe build finished in 42 seconds.\n:::\n",
|
|
44
50
|
"../../markdown-fixtures/figures/caption.html": "<figure>\n<img src=\"./pipeline.svg\" alt=\"The build pipeline\">\n<figcaption>The build, from <em>source</em> to release</figcaption>\n</figure>\n",
|
|
45
51
|
"../../markdown-fixtures/figures/caption.md": "::: figure The build, from *source* to release\n\n:::\n",
|
|
52
|
+
"../../markdown-fixtures/front-matter/list-only.canonical.md": "# Notes\n",
|
|
53
|
+
"../../markdown-fixtures/front-matter/list-only.html": "<h1 id=\"notes\">Notes</h1>\n",
|
|
54
|
+
"../../markdown-fixtures/front-matter/list-only.md": "---\naliases:\n - /old/\n - /older/\n---\n\n# Notes\n",
|
|
55
|
+
"../../markdown-fixtures/front-matter/mapping-only.canonical.md": "# Notes\n",
|
|
56
|
+
"../../markdown-fixtures/front-matter/mapping-only.html": "<h1 id=\"notes\">Notes</h1>\n",
|
|
57
|
+
"../../markdown-fixtures/front-matter/mapping-only.md": "---\nhead:\n meta:\n robots: noindex\n---\n\n# Notes\n",
|
|
58
|
+
"../../markdown-fixtures/front-matter/metadata.canonical.md": "# Notes\n",
|
|
46
59
|
"../../markdown-fixtures/front-matter/metadata.html": "<h1 id=\"notes\">Notes</h1>\n",
|
|
47
60
|
"../../markdown-fixtures/front-matter/metadata.md": "---\ntitle: Release notes\n---\n\n# Notes\n",
|
|
61
|
+
"../../markdown-fixtures/front-matter/nested.canonical.md": "# Notes\n",
|
|
62
|
+
"../../markdown-fixtures/front-matter/nested.html": "<h1 id=\"notes\">Notes</h1>\n",
|
|
63
|
+
"../../markdown-fixtures/front-matter/nested.md": "---\n# Page settings\nsidebar:\n position: 2\n items:\n - label: Intro\n href: /intro/\ntitle: Notes\n---\n\n# Notes\n",
|
|
48
64
|
"../../markdown-fixtures/heading-ids/accented.html": "<h2 id=\"über-uns\">Über uns</h2>\n<h2 id=\"café-crème\">Café crème</h2>\n",
|
|
49
65
|
"../../markdown-fixtures/heading-ids/accented.md": "## Über uns\n\n## Café crème\n",
|
|
50
66
|
"../../markdown-fixtures/heading-ids/cjk.html": "<h2 id=\"はじめに\">はじめに</h2>\n<h2 id=\"設定-と-使い方\">設定 と 使い方</h2>\n",
|
|
@@ -57,6 +73,8 @@ var files = /* #__PURE__ */ Object.assign({
|
|
|
57
73
|
"../../markdown-fixtures/heading-ids/emoji-only.md": "## 🚀\n\n## 🎉 🎉\n",
|
|
58
74
|
"../../markdown-fixtures/heading-ids/generated.html": "<h2 id=\"getting-started\">Getting started</h2>\n<h2 id=\"getting-started-1\">Getting started</h2>\n<h2 id=\"über-code-more\">Über <code>code</code> & more</h2>\n",
|
|
59
75
|
"../../markdown-fixtures/heading-ids/generated.md": "## Getting started\n\n## Getting started\n\n## Über `code` & more\n",
|
|
76
|
+
"../../markdown-fixtures/heading-ids/literal-braces.html": "<h2 id=\"price-usd\">Price {#usd}</h2>\n",
|
|
77
|
+
"../../markdown-fixtures/heading-ids/literal-braces.md": "## Price \\{#usd}\n",
|
|
60
78
|
"../../markdown-fixtures/headings/levels.html": "<h1 id=\"one\">One</h1>\n<h2 id=\"two\">Two</h2>\n<h3 id=\"three\">Three</h3>\n<h4 id=\"four\">Four</h4>\n<h5 id=\"five\">Five</h5>\n<h6 id=\"six\">Six</h6>\n",
|
|
61
79
|
"../../markdown-fixtures/headings/levels.md": "# One\n\n## Two\n\n### Three\n\n#### Four\n\n##### Five\n\n###### Six\n",
|
|
62
80
|
"../../markdown-fixtures/image-attributes/placed.html": "<img src=\"./pipeline.svg\" alt=\"Pipeline\" width=\"640\" height=\"360\" data-placement=\"wrap-left\" data-looma-responsive>\n",
|
|
@@ -69,14 +87,20 @@ var files = /* #__PURE__ */ Object.assign({
|
|
|
69
87
|
"../../markdown-fixtures/mentions/person.md": "Thanks, :mention[Ada Lovelace]{id=u1}.\n",
|
|
70
88
|
"../../markdown-fixtures/tables/gfm.html": "<table>\n<thead>\n<tr>\n<th>Package</th>\n<th style=\"text-align:right\">Version</th>\n<th style=\"text-align:center\">Notes</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>looma</td>\n<td style=\"text-align:right\">0.28</td>\n<td style=\"text-align:center\">Components</td>\n</tr>\n</tbody>\n</table>\n",
|
|
71
89
|
"../../markdown-fixtures/tables/gfm.md": "| Package | Version | Notes |\n| --- | ---: | :---: |\n| looma | 0.28 | Components |\n",
|
|
90
|
+
"../../markdown-fixtures/tables/left-aligned.html": "<table>\n<thead>\n<tr>\n<th style=\"text-align:left\">Name</th>\n<th>Notes</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td style=\"text-align:left\">a | b</td>\n<td><code>x | y</code></td>\n</tr>\n</tbody>\n</table>\n",
|
|
91
|
+
"../../markdown-fixtures/tables/left-aligned.md": "| Name | Notes |\n| :--- | --- |\n| a \\| b | `x \\| y` |\n",
|
|
92
|
+
"../../markdown-fixtures/tables/rich.canonical.md": "<table><tbody><tr><th><p>Status</p></th><th><p>Owner</p></th></tr><tr><td style=\"background-color: rgb(220, 252, 231);\"><p>Done</p><ul><li><p>Shipped</p></li></ul></td><td style=\"text-align: center;\"><p>Ada</p></td></tr></tbody></table>\n",
|
|
72
93
|
"../../markdown-fixtures/tables/rich.html": "<table><tbody><tr><th><p>Status</p></th><th><p>Owner</p></th></tr><tr><td style=\"background-color: #dcfce7\"><p>Done</p><ul><li><p>Shipped</p></li></ul></td><td colspan=\"1\" rowspan=\"1\" style=\"text-align: center\"><p>Ada</p></td></tr></tbody></table>\n",
|
|
73
94
|
"../../markdown-fixtures/tables/rich.md": "<table><tbody><tr><th><p>Status</p></th><th><p>Owner</p></th></tr><tr><td style=\"background-color: #dcfce7\"><p>Done</p><ul><li><p>Shipped</p></li></ul></td><td colspan=\"1\" rowspan=\"1\" style=\"text-align: center\"><p>Ada</p></td></tr></tbody></table>\n",
|
|
74
95
|
"../../markdown-fixtures/tabs/code-group.html": "<ui-tabs class=\"code-group\" group=\"code-group\">\n<section aria-label=\"looma.config.js\">\n<pre><code class=\"language-js\" data-meta=\"title="looma.config.js"\">export default { theme: "light" };</code></pre>\n</section>\n<section aria-label=\"ts\">\n<pre><code class=\"language-ts\">export default { theme: "light" } satisfies Config;</code></pre>\n</section>\n</ui-tabs>\n",
|
|
75
96
|
"../../markdown-fixtures/tabs/code-group.md": "::: code-group\n```js title=\"looma.config.js\"\nexport default { theme: \"light\" };\n```\n```ts\nexport default { theme: \"light\" } satisfies Config;\n```\n:::\n",
|
|
76
97
|
"../../markdown-fixtures/tabs/labelled.html": "<ui-tabs group=\"install\">\n<section aria-label=\"npm\">\n<pre><code class=\"language-sh\">npm install @threadlabs/looma</code></pre>\n</section>\n<section aria-label=\"pnpm\">\n<pre><code class=\"language-sh\">pnpm add @threadlabs/looma</code></pre>\n</section>\n</ui-tabs>\n",
|
|
77
98
|
"../../markdown-fixtures/tabs/labelled.md": "::: tabs install\n== npm\n```sh\nnpm install @threadlabs/looma\n```\n== pnpm\n```sh\npnpm add @threadlabs/looma\n```\n:::\n",
|
|
78
|
-
"../../markdown-fixtures/task-lists/checked.html": "<ul data-type=\"taskList\">\n<li data-type=\"taskItem\" data-checked=\"true\"><input type=\"checkbox\" disabled checked>Read the changelog</li>\n<li data-type=\"taskItem\" data-checked=\"false\"><input type=\"checkbox\" disabled>Run the migration</li>\n</ul>\n",
|
|
99
|
+
"../../markdown-fixtures/task-lists/checked.html": "<ul data-type=\"taskList\">\n<li data-type=\"taskItem\" data-checked=\"true\"><label><input type=\"checkbox\" disabled checked> Read the changelog</label></li>\n<li data-type=\"taskItem\" data-checked=\"false\"><label><input type=\"checkbox\" disabled> Run the migration</label></li>\n</ul>\n",
|
|
79
100
|
"../../markdown-fixtures/task-lists/checked.md": "- [x] Read the changelog\n- [ ] Run the migration\n",
|
|
101
|
+
"../../markdown-fixtures/task-lists/loose.canonical.md": "- [x] Read the changelog\n\n Then tell the team.\n- [ ] Run the migration\n",
|
|
102
|
+
"../../markdown-fixtures/task-lists/loose.html": "<ul data-type=\"taskList\">\n<li data-type=\"taskItem\" data-checked=\"true\">\n<p><label><input type=\"checkbox\" disabled checked> Read the changelog</label></p>\n<p>Then tell the team.</p>\n</li>\n<li data-type=\"taskItem\" data-checked=\"false\">\n<p><label><input type=\"checkbox\" disabled> Run the migration</label></p>\n</li>\n</ul>\n",
|
|
103
|
+
"../../markdown-fixtures/task-lists/loose.md": "- [x] Read the changelog\n\n Then tell the team.\n\n- [ ] Run the migration\n",
|
|
80
104
|
"../../markdown-fixtures/task-lists/mixed.canonical.md": "- \\[x\\] Done\n- Not a task\n",
|
|
81
105
|
"../../markdown-fixtures/task-lists/mixed.html": "<ul>\n<li>[x] Done</li>\n<li>Not a task</li>\n</ul>\n",
|
|
82
106
|
"../../markdown-fixtures/task-lists/mixed.md": "- [x] Done\n- Not a task\n",
|
package/markdown/index.d.ts
CHANGED
|
@@ -56,9 +56,12 @@ export declare function parseLoomaAttributes(source: string): LoomaAttributes |
|
|
|
56
56
|
/**
|
|
57
57
|
* Writes attributes as a brace list, `id` first as `#id`, then the rest in order.
|
|
58
58
|
* @contract `true` writes a bare key; values are quoted only when they need it. `false`, null and
|
|
59
|
-
* undefined values are left out, and an empty set writes an empty string.
|
|
59
|
+
* undefined values are left out, and an empty set writes an empty string. An ID that `#` cannot
|
|
60
|
+
* carry, or any ID when `hashId` is false, is written `id=…`.
|
|
60
61
|
*/
|
|
61
|
-
export declare function formatLoomaAttributes(attributes: Readonly<Record<string, string | number | boolean | null | undefined
|
|
62
|
+
export declare function formatLoomaAttributes(attributes: Readonly<Record<string, string | number | boolean | null | undefined>>, { hashId }?: {
|
|
63
|
+
hashId?: boolean | undefined;
|
|
64
|
+
}): string;
|
|
62
65
|
/**
|
|
63
66
|
* The anchor a heading's text derives: NFC, lowercase, keeping letters, numbers and combining
|
|
64
67
|
* marks, with every other run of characters one hyphen.
|
|
@@ -89,10 +92,23 @@ export interface LoomaFenceInfo {
|
|
|
89
92
|
}
|
|
90
93
|
/** Reads a fence's info string: ```` ```ts title="a.ts" {1,3-5} :line-numbers ````. */
|
|
91
94
|
export declare function parseLoomaFenceInfo(info: string): LoomaFenceInfo;
|
|
95
|
+
/**
|
|
96
|
+
* Applies code notation: a line ending in a `[!code highlight]` comment is highlighted and loses
|
|
97
|
+
* the comment, `[!code highlight:3]` covers three lines, and a marker alone on its line is removed
|
|
98
|
+
* with the line and applies to the lines after it.
|
|
99
|
+
* @contract Returns the code a reader sees and its highlighted line numbers, from 1, ascending.
|
|
100
|
+
* ponytail: matched on source lines, so a string that ends in the marker counts too.
|
|
101
|
+
*/
|
|
102
|
+
export declare function applyLoomaCodeNotation(code: string): {
|
|
103
|
+
code: string;
|
|
104
|
+
lines: number[];
|
|
105
|
+
};
|
|
92
106
|
/**
|
|
93
107
|
* Splits YAML front matter from the start of a document.
|
|
94
|
-
* @contract Front matter opens with a `---` line at the very start
|
|
95
|
-
* line, and
|
|
108
|
+
* @contract Front matter opens with a `---` line at the very start and closes at a `---` or `...`
|
|
109
|
+
* line, and what lies between is a YAML mapping: its first line that is not blank or a comment
|
|
110
|
+
* starts with a key, whose value may be a scalar, a list or a mapping. `source` is the block as
|
|
111
|
+
* written, delimiters included.
|
|
96
112
|
* @failure Returns null when the document has none.
|
|
97
113
|
*/
|
|
98
114
|
export declare function splitLoomaFrontmatter(document: string): {
|
package/markdown/index.js
CHANGED
|
@@ -139,13 +139,15 @@ function parseLoomaAttributes(source) {
|
|
|
139
139
|
/**
|
|
140
140
|
* Writes attributes as a brace list, `id` first as `#id`, then the rest in order.
|
|
141
141
|
* @contract `true` writes a bare key; values are quoted only when they need it. `false`, null and
|
|
142
|
-
* undefined values are left out, and an empty set writes an empty string.
|
|
142
|
+
* undefined values are left out, and an empty set writes an empty string. An ID that `#` cannot
|
|
143
|
+
* carry, or any ID when `hashId` is false, is written `id=…`.
|
|
143
144
|
*/
|
|
144
|
-
function formatLoomaAttributes(attributes) {
|
|
145
|
+
function formatLoomaAttributes(attributes, { hashId = true } = {}) {
|
|
145
146
|
const items = [];
|
|
146
147
|
const { id, ...rest } = attributes;
|
|
147
|
-
|
|
148
|
-
|
|
148
|
+
const hashed = hashId && typeof id === "string" && /^[\w-]+$/.test(id);
|
|
149
|
+
if (hashed) items.push(`#${id}`);
|
|
150
|
+
for (const [key, value] of Object.entries(hashed ? rest : attributes)) if (value === true) items.push(key);
|
|
149
151
|
else if (value === false || value === null || value === void 0) continue;
|
|
150
152
|
else {
|
|
151
153
|
const text = String(value);
|
|
@@ -202,15 +204,46 @@ function parseLoomaFenceInfo(info) {
|
|
|
202
204
|
...numbers ? { lineNumbers: numbers[1] === void 0 } : {}
|
|
203
205
|
};
|
|
204
206
|
}
|
|
207
|
+
var MAPPING_KEY = /^(?:[^\s#:'"\-?,[\]{}&*!|>%@`][^:\n]*?|"[^"\n]*"|'[^'\n]*')[ \t]*:(?:[ \t]|$)/;
|
|
208
|
+
var NOTATION = /[ \t]*(?:\/\/|#|--|<!--|\{?\/\*)[ \t]*\[!code highlight(?::(\d+))?\][ \t]*(?:-->|\*\/\}?)?[ \t]*$/;
|
|
209
|
+
/**
|
|
210
|
+
* Applies code notation: a line ending in a `[!code highlight]` comment is highlighted and loses
|
|
211
|
+
* the comment, `[!code highlight:3]` covers three lines, and a marker alone on its line is removed
|
|
212
|
+
* with the line and applies to the lines after it.
|
|
213
|
+
* @contract Returns the code a reader sees and its highlighted line numbers, from 1, ascending.
|
|
214
|
+
* ponytail: matched on source lines, so a string that ends in the marker counts too.
|
|
215
|
+
*/
|
|
216
|
+
function applyLoomaCodeNotation(code) {
|
|
217
|
+
const kept = [];
|
|
218
|
+
const lines = /* @__PURE__ */ new Set();
|
|
219
|
+
for (const line of code.split("\n")) {
|
|
220
|
+
const match = NOTATION.exec(line);
|
|
221
|
+
if (!match) {
|
|
222
|
+
kept.push(line);
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
const rest = line.slice(0, match.index);
|
|
226
|
+
const own = rest.trim() !== "";
|
|
227
|
+
if (own) kept.push(rest);
|
|
228
|
+
for (let count = 0; count < Number(match[1] ?? 1); count++) lines.add(kept.length + (own ? 0 : 1) + count);
|
|
229
|
+
}
|
|
230
|
+
return {
|
|
231
|
+
code: kept.join("\n"),
|
|
232
|
+
lines: [...lines].sort((a, b) => a - b)
|
|
233
|
+
};
|
|
234
|
+
}
|
|
205
235
|
/**
|
|
206
236
|
* Splits YAML front matter from the start of a document.
|
|
207
|
-
* @contract Front matter opens with a `---` line at the very start
|
|
208
|
-
* line, and
|
|
237
|
+
* @contract Front matter opens with a `---` line at the very start and closes at a `---` or `...`
|
|
238
|
+
* line, and what lies between is a YAML mapping: its first line that is not blank or a comment
|
|
239
|
+
* starts with a key, whose value may be a scalar, a list or a mapping. `source` is the block as
|
|
240
|
+
* written, delimiters included.
|
|
209
241
|
* @failure Returns null when the document has none.
|
|
210
242
|
*/
|
|
211
243
|
function splitLoomaFrontmatter(document) {
|
|
212
244
|
const match = /^---[ \t]*\r?\n([\s\S]*?)\r?\n(?:---|\.\.\.)[ \t]*(?:\r?\n|$)/.exec(document.slice(0, 8192));
|
|
213
|
-
|
|
245
|
+
const first = match?.[1].split(/\r?\n/).find((line) => line.trim() && !line.trimStart().startsWith("#"));
|
|
246
|
+
if (!match || !first || !MAPPING_KEY.test(first)) return null;
|
|
214
247
|
return {
|
|
215
248
|
source: match[0].trimEnd(),
|
|
216
249
|
yaml: match[1],
|
|
@@ -227,4 +260,4 @@ function loomaCodeFence(code) {
|
|
|
227
260
|
return "`".repeat(Math.max(3, longest + 1));
|
|
228
261
|
}
|
|
229
262
|
//#endregion
|
|
230
|
-
export { LOOMA_BADGE_TONES, LOOMA_CALLOUT_TONES, LOOMA_CALLOUT_TONE_ALIASES, LOOMA_CHIP_TONES, LOOMA_CONTAINER_NAMES, LOOMA_MEANING_TONES, assignLoomaHeadingIds, formatLoomaAttributes, loomaCodeFence, loomaContainerMarker, loomaHeadingSlug, normalizeLoomaCalloutTone, normalizeLoomaChipTone, parseLoomaAttributes, parseLoomaFenceInfo, readLoomaAttributes, splitLoomaFrontmatter };
|
|
263
|
+
export { LOOMA_BADGE_TONES, LOOMA_CALLOUT_TONES, LOOMA_CALLOUT_TONE_ALIASES, LOOMA_CHIP_TONES, LOOMA_CONTAINER_NAMES, LOOMA_MEANING_TONES, applyLoomaCodeNotation, assignLoomaHeadingIds, formatLoomaAttributes, loomaCodeFence, loomaContainerMarker, loomaHeadingSlug, normalizeLoomaCalloutTone, normalizeLoomaChipTone, parseLoomaAttributes, parseLoomaFenceInfo, readLoomaAttributes, splitLoomaFrontmatter };
|
|
@@ -1,13 +1,21 @@
|
|
|
1
1
|
import MarkdownIt from "markdown-it";
|
|
2
|
+
/** How a Looma Markdown instance renders. */
|
|
3
|
+
export interface LoomaMarkdownOptions {
|
|
4
|
+
/**
|
|
5
|
+
* `apply` (default) renders code as a reader sees it, without its `[!code highlight]` comments;
|
|
6
|
+
* `keep` keeps them in the code, for an editor that edits the source.
|
|
7
|
+
*/
|
|
8
|
+
codeNotation?: "apply" | "keep";
|
|
9
|
+
}
|
|
2
10
|
/**
|
|
3
11
|
* Adds Looma Markdown to a markdown-it instance: containers, alerts, task lists, the table of
|
|
4
12
|
* contents, chips, mentions, brace attributes, heading anchors, code meta and front matter.
|
|
5
13
|
* @contract Renders the canonical HTML, which is the HTML LoomaEditor saves. It is not sanitized.
|
|
6
14
|
* Linkify and raw HTML are instance options; `createLoomaMarkdownIt` turns both on.
|
|
7
15
|
*/
|
|
8
|
-
export declare function loomaMarkdown(md: MarkdownIt): void;
|
|
16
|
+
export declare function loomaMarkdown(md: MarkdownIt, { codeNotation }?: LoomaMarkdownOptions): void;
|
|
9
17
|
/**
|
|
10
18
|
* A markdown-it instance for Looma Markdown: raw HTML and GFM autolinks on, with `loomaMarkdown`.
|
|
11
19
|
* @contract Each call returns a new instance that callers may extend.
|
|
12
20
|
*/
|
|
13
|
-
export declare function createLoomaMarkdownIt(): MarkdownIt;
|
|
21
|
+
export declare function createLoomaMarkdownIt(options?: LoomaMarkdownOptions): MarkdownIt;
|
package/markdown/markdown-it.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { LOOMA_CONTAINER_NAMES, assignLoomaHeadingIds, normalizeLoomaCalloutTone, normalizeLoomaChipTone, parseLoomaAttributes, parseLoomaFenceInfo, readLoomaAttributes, splitLoomaFrontmatter } from "./index.js";
|
|
1
|
+
import { LOOMA_CONTAINER_NAMES, applyLoomaCodeNotation, assignLoomaHeadingIds, normalizeLoomaCalloutTone, normalizeLoomaChipTone, parseLoomaAttributes, parseLoomaFenceInfo, readLoomaAttributes, splitLoomaFrontmatter } from "./index.js";
|
|
2
2
|
import MarkdownIt from "markdown-it";
|
|
3
3
|
//#region src/markdown/markdown-it.ts
|
|
4
4
|
var CONTAINER_OPEN = /^(:{3,})[ \t]*([A-Za-z][\w-]*)(?:[ \t]+(.*?))?[ \t]*$/;
|
|
@@ -8,7 +8,7 @@ var TAB_LABEL = /^==[ \t]+(\S.*?)[ \t]*$/;
|
|
|
8
8
|
var OPEN_FLAG = /[ \t]*\{open\}$/;
|
|
9
9
|
var ALERT = /^\[!(NOTE|TIP|IMPORTANT|WARNING|CAUTION)\][ \t]*(?:\n|$)/i;
|
|
10
10
|
var LEAF = /^::([A-Za-z][\w-]*)(\{.*\})?[ \t]*$/;
|
|
11
|
-
var TASK = /^\[([ xX])\][ \t]
|
|
11
|
+
var TASK = /^\[([ xX])\](?:[ \t]+|$)/;
|
|
12
12
|
var TOC_FORMATS = /* @__PURE__ */ new Set([
|
|
13
13
|
"plain",
|
|
14
14
|
"bulleted",
|
|
@@ -200,7 +200,10 @@ function taskLists(state) {
|
|
|
200
200
|
inline.content = inline.content.replace(TASK, "");
|
|
201
201
|
item.attrSet("data-type", "taskItem");
|
|
202
202
|
item.attrSet("data-checked", String(checked));
|
|
203
|
-
|
|
203
|
+
inline.meta = {
|
|
204
|
+
...inline.meta,
|
|
205
|
+
task: checked
|
|
206
|
+
};
|
|
204
207
|
}
|
|
205
208
|
}
|
|
206
209
|
}
|
|
@@ -242,6 +245,11 @@ function attributes(state) {
|
|
|
242
245
|
const token = tokens[index];
|
|
243
246
|
if (token.type !== "inline" || !token.children) continue;
|
|
244
247
|
const children = token.children;
|
|
248
|
+
if (typeof token.meta?.task === "boolean") {
|
|
249
|
+
const html = (content) => Object.assign(new state.Token("html_inline", "", 0), { content });
|
|
250
|
+
children.unshift(html(`<label><input type="checkbox" disabled=""${token.meta.task ? " checked=\"\"" : ""}> `));
|
|
251
|
+
children.push(html("</label>"));
|
|
252
|
+
}
|
|
245
253
|
for (let child = children.length - 3; child >= 0; child--) {
|
|
246
254
|
const text = children[child + 1];
|
|
247
255
|
if (children[child].type !== "link_open" || children[child].markup !== "linkify" || /^(?:[a-z][\w+.-]*:|www\.)/i.test(text.content)) continue;
|
|
@@ -318,7 +326,7 @@ function tableOfContents(tokens, format, depth, escape) {
|
|
|
318
326
|
* @contract Renders the canonical HTML, which is the HTML LoomaEditor saves. It is not sanitized.
|
|
319
327
|
* Linkify and raw HTML are instance options; `createLoomaMarkdownIt` turns both on.
|
|
320
328
|
*/
|
|
321
|
-
function loomaMarkdown(md) {
|
|
329
|
+
function loomaMarkdown(md, { codeNotation = "apply" } = {}) {
|
|
322
330
|
const escape = md.utils.escapeHtml;
|
|
323
331
|
md.block.ruler.before("table", "looma_frontmatter", frontmatter);
|
|
324
332
|
md.block.ruler.before("fence", "looma_container", container, { alt: [
|
|
@@ -369,15 +377,10 @@ function loomaMarkdown(md) {
|
|
|
369
377
|
const id = String(attributes.id);
|
|
370
378
|
return `<span data-type="mention" class="looma-mention" data-id="${escape(id)}" data-label="${escape(label || id)}">@${escape(label || id)}</span>`;
|
|
371
379
|
};
|
|
372
|
-
const listItem = rules.list_item_open ?? ((tokens, index, options, _env, self) => self.renderToken(tokens, index, options));
|
|
373
|
-
rules.list_item_open = (tokens, index, options, env, self) => {
|
|
374
|
-
const html = listItem(tokens, index, options, env, self);
|
|
375
|
-
const task = tokens[index].meta;
|
|
376
|
-
return task && typeof task.checked === "boolean" ? `${html}<input type="checkbox" disabled=""${task.checked ? " checked=\"\"" : ""}>` : html;
|
|
377
|
-
};
|
|
378
380
|
const code = (content, info) => {
|
|
379
381
|
const { language, meta } = parseLoomaFenceInfo(info);
|
|
380
|
-
|
|
382
|
+
const source = content.replace(/\n$/, "");
|
|
383
|
+
return `<pre><code${language ? ` class="language-${escape(language)}"` : ""}${meta ? ` data-meta="${escape(meta)}"` : ""}>${escape(codeNotation === "keep" ? source : applyLoomaCodeNotation(source).code)}</code></pre>\n`;
|
|
381
384
|
};
|
|
382
385
|
rules.fence = (tokens, index) => code(tokens[index].content, tokens[index].info);
|
|
383
386
|
rules.code_block = (tokens, index) => code(tokens[index].content, "");
|
|
@@ -386,12 +389,12 @@ function loomaMarkdown(md) {
|
|
|
386
389
|
* A markdown-it instance for Looma Markdown: raw HTML and GFM autolinks on, with `loomaMarkdown`.
|
|
387
390
|
* @contract Each call returns a new instance that callers may extend.
|
|
388
391
|
*/
|
|
389
|
-
function createLoomaMarkdownIt() {
|
|
392
|
+
function createLoomaMarkdownIt(options = {}) {
|
|
390
393
|
return new MarkdownIt({
|
|
391
394
|
html: true,
|
|
392
395
|
linkify: true,
|
|
393
396
|
typographer: false
|
|
394
|
-
}).use(loomaMarkdown);
|
|
397
|
+
}).use(loomaMarkdown, options);
|
|
395
398
|
}
|
|
396
399
|
//#endregion
|
|
397
400
|
export { createLoomaMarkdownIt, loomaMarkdown };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@threadlabs/looma",
|
|
3
|
-
"version": "0.28.
|
|
3
|
+
"version": "0.28.18",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Looma components as HTML, Vue, and plain DOM, with design tokens.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -171,6 +171,7 @@
|
|
|
171
171
|
"@tiptap/vue-3": "^2.11.5",
|
|
172
172
|
"@types/markdown-it": "14.2.0",
|
|
173
173
|
"@vitest/browser": "~3.2.6",
|
|
174
|
+
"axe-core": "^4.10.3",
|
|
174
175
|
"jsdom": "^27.0.0"
|
|
175
176
|
},
|
|
176
177
|
"engines": {
|
package/vue/editor/index.js
CHANGED
|
@@ -1840,6 +1840,34 @@ function formatEditorShortcut(keys, apple) {
|
|
|
1840
1840
|
].filter(Boolean).join("+");
|
|
1841
1841
|
}
|
|
1842
1842
|
var isApplePlatform = () => typeof navigator !== "undefined" && /mac|iphone|ipad|ipod/i.test(navigator.userAgentData?.platform || navigator.platform || navigator.userAgent);
|
|
1843
|
+
/**
|
|
1844
|
+
* The anchor a heading's text derives: NFC, lowercase, keeping letters, numbers and combining
|
|
1845
|
+
* marks, with every other run of characters one hyphen.
|
|
1846
|
+
* @failure Text with no letters or numbers, such as only emoji, derives `heading`.
|
|
1847
|
+
*/
|
|
1848
|
+
function loomaHeadingSlug(text) {
|
|
1849
|
+
return text.normalize("NFC").toLowerCase().replace(/[^\p{L}\p{N}\p{M}]+/gu, "-").replace(/^-+|-+$/g, "") || "heading";
|
|
1850
|
+
}
|
|
1851
|
+
/**
|
|
1852
|
+
* Gives every heading of a document its anchor, in document order.
|
|
1853
|
+
* @contract A custom ID is kept unless an earlier heading already has it. Any other heading takes
|
|
1854
|
+
* its slug, or the slug's first free suffix (`-1`, `-2`…), never one a later custom ID reserves.
|
|
1855
|
+
*/
|
|
1856
|
+
function assignLoomaHeadingIds(headings) {
|
|
1857
|
+
const reserved = new Set(headings.map(({ id }) => id).filter((id) => Boolean(id)));
|
|
1858
|
+
const used = /* @__PURE__ */ new Set();
|
|
1859
|
+
return headings.map(({ text, id: custom }) => {
|
|
1860
|
+
let id = custom || loomaHeadingSlug(text);
|
|
1861
|
+
if (custom ? used.has(id) : used.has(id) || reserved.has(id)) {
|
|
1862
|
+
const stem = id;
|
|
1863
|
+
let suffix = 1;
|
|
1864
|
+
while (used.has(`${stem}-${suffix}`) || reserved.has(`${stem}-${suffix}`)) suffix++;
|
|
1865
|
+
id = `${stem}-${suffix}`;
|
|
1866
|
+
}
|
|
1867
|
+
used.add(id);
|
|
1868
|
+
return id;
|
|
1869
|
+
});
|
|
1870
|
+
}
|
|
1843
1871
|
//#endregion
|
|
1844
1872
|
//#region src/editor/extensions/expand.ts
|
|
1845
1873
|
/**
|
|
@@ -2049,7 +2077,17 @@ function tableOfContentsTree(entries) {
|
|
|
2049
2077
|
}
|
|
2050
2078
|
return roots;
|
|
2051
2079
|
}
|
|
2052
|
-
|
|
2080
|
+
var derivedFrom = (id, text) => {
|
|
2081
|
+
const slug = loomaHeadingSlug(text);
|
|
2082
|
+
return id === slug || id.startsWith(`${slug}-`) && /^\d+$/.test(id.slice(slug.length + 1));
|
|
2083
|
+
};
|
|
2084
|
+
/**
|
|
2085
|
+
* Keeps heading anchors on Looma Markdown's rule. An anchor derived from its heading's text
|
|
2086
|
+
* (as it read before this change) follows the text; any other, including `looma-heading-N` anchors
|
|
2087
|
+
* saved before this rule, is a custom anchor and stays, so links to it keep working. With
|
|
2088
|
+
* a table of contents every heading gets an anchor; without one, only headings that have one.
|
|
2089
|
+
*/
|
|
2090
|
+
function anchorTransaction(state, previous) {
|
|
2053
2091
|
let hasToc = false;
|
|
2054
2092
|
const headings = [];
|
|
2055
2093
|
state.doc.descendants((node, position) => {
|
|
@@ -2059,24 +2097,28 @@ function anchorTransaction(state) {
|
|
|
2059
2097
|
position
|
|
2060
2098
|
});
|
|
2061
2099
|
});
|
|
2062
|
-
if (!
|
|
2063
|
-
const
|
|
2064
|
-
|
|
2100
|
+
if (!headings.length) return null;
|
|
2101
|
+
const before = /* @__PURE__ */ new Map();
|
|
2102
|
+
previous?.descendants((node) => {
|
|
2103
|
+
if (node.type.name === "heading" && typeof node.attrs.id === "string") before.set(node.attrs.id, node.textContent.trim());
|
|
2104
|
+
});
|
|
2105
|
+
const custom = (node) => {
|
|
2106
|
+
const id = node.attrs.id;
|
|
2107
|
+
if (typeof id !== "string" || !id || /\s/.test(id)) return void 0;
|
|
2108
|
+
return derivedFrom(id, before.get(id) ?? node.textContent.trim()) ? void 0 : id;
|
|
2109
|
+
};
|
|
2110
|
+
const ids = assignLoomaHeadingIds(headings.map(({ node }) => ({
|
|
2111
|
+
text: node.textContent.trim(),
|
|
2112
|
+
id: custom(node)
|
|
2113
|
+
})));
|
|
2065
2114
|
const transaction = state.tr;
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
transaction.setNodeMarkup(position, void 0, {
|
|
2074
|
-
...node.attrs,
|
|
2075
|
-
id
|
|
2076
|
-
});
|
|
2077
|
-
}
|
|
2078
|
-
used.add(id);
|
|
2079
|
-
}
|
|
2115
|
+
headings.forEach(({ node, position }, index) => {
|
|
2116
|
+
if (node.attrs.id === ids[index] || !hasToc && !node.attrs.id) return;
|
|
2117
|
+
transaction.setNodeMarkup(position, void 0, {
|
|
2118
|
+
...node.attrs,
|
|
2119
|
+
id: ids[index]
|
|
2120
|
+
});
|
|
2121
|
+
});
|
|
2080
2122
|
return transaction.docChanged ? transaction : null;
|
|
2081
2123
|
}
|
|
2082
2124
|
var LoomaHeadingAnchors = Extension.create({
|
|
@@ -2092,7 +2134,7 @@ var LoomaHeadingAnchors = Extension.create({
|
|
|
2092
2134
|
if (transaction) this.editor.view.dispatch(transaction.setMeta("addToHistory", false));
|
|
2093
2135
|
},
|
|
2094
2136
|
addProseMirrorPlugins() {
|
|
2095
|
-
return [new Plugin({ appendTransaction: (transactions,
|
|
2137
|
+
return [new Plugin({ appendTransaction: (transactions, oldState, state) => transactions.some((transaction) => transaction.docChanged) ? anchorTransaction(state, oldState.doc) : null })];
|
|
2096
2138
|
}
|
|
2097
2139
|
});
|
|
2098
2140
|
function contentsHtml(doc, config) {
|