@threadlabs/looma 0.30.7 → 0.30.8

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.
@@ -40,7 +40,9 @@ beginning of a line or after whitespace; embedded at signs remain literal.
40
40
  Headings, text formatting, links, images, lists, tasks, quotes, code, callouts,
41
41
  disclosures, figures, tabs, chips, mentions and tables map to their semantic AST
42
42
  nodes. Mention targets become `ref`; chips keep their `tone`; merged table cells
43
- keep numeric spans and block bodies.
43
+ keep numeric spans and block bodies. A footnote reference is the inline node
44
+ `footnote` with its label as text, and the notes are a `footnotes` container
45
+ holding a `footnote` container per note, titled by its label.
44
46
 
45
47
  Table cell presentation uses string attributes: `textAlign`, `backgroundColor`
46
48
  and the adapter profile's `columnWidths` (comma-separated pixel widths, such as
@@ -475,6 +475,134 @@ var LoomaFigcaption = Node$1.create({
475
475
  }
476
476
  });
477
477
  //#endregion
478
+ //#region src/editor/extensions/footnote.ts
479
+ var labelFrom = (href, prefix) => href?.startsWith(`#${prefix}`) ? href.slice(prefix.length + 1) : null;
480
+ /**
481
+ * A footnote reference, `[^label]`, saved as `<sup><a data-footnote-ref>`.
482
+ * @invariant `label` names its note. `number` and `occurrence` are the note's number and this
483
+ * reference's place among the note's references, as the document had them when it loaded.
484
+ */
485
+ var LoomaFootnoteReference = Node$1.create({
486
+ name: "loomaFootnoteReference",
487
+ group: "inline",
488
+ inline: true,
489
+ atom: true,
490
+ selectable: true,
491
+ addAttributes() {
492
+ const link = (element) => element.querySelector(":scope > a[data-footnote-ref]");
493
+ const label = (element) => labelFrom(link(element)?.getAttribute("href"), "fn:") ?? "";
494
+ return {
495
+ label: {
496
+ default: "",
497
+ parseHTML: label,
498
+ renderHTML: () => ({})
499
+ },
500
+ number: {
501
+ default: null,
502
+ parseHTML: (element) => Number(element.textContent) || null,
503
+ renderHTML: () => ({})
504
+ },
505
+ occurrence: {
506
+ default: 1,
507
+ parseHTML: (element) => Number(link(element)?.id.slice(`fnref:${label(element)}:`.length)) || 1,
508
+ renderHTML: () => ({})
509
+ }
510
+ };
511
+ },
512
+ parseHTML() {
513
+ return [{
514
+ tag: "sup",
515
+ priority: 1100,
516
+ getAttrs: (element) => element.querySelector(":scope > a[data-footnote-ref]") ? null : false
517
+ }];
518
+ },
519
+ renderHTML({ node }) {
520
+ const { label, number, occurrence } = node.attrs;
521
+ return ["sup", [
522
+ "a",
523
+ {
524
+ href: `#fn:${label}`,
525
+ id: `fnref:${label}${occurrence > 1 ? `:${occurrence}` : ""}`,
526
+ "data-footnote-ref": "",
527
+ "aria-describedby": "footnote:label"
528
+ },
529
+ String(number ?? label)
530
+ ]];
531
+ },
532
+ renderText({ node }) {
533
+ return `[^${node.attrs.label}]`;
534
+ }
535
+ });
536
+ /**
537
+ * The document's notes, saved as `<section data-footnotes>` with its `<ol>`.
538
+ * @invariant It holds only footnotes; Markdown writes it as definitions after the text.
539
+ */
540
+ var LoomaFootnotes = Node$1.create({
541
+ name: "loomaFootnotes",
542
+ group: "block",
543
+ content: "loomaFootnote+",
544
+ isolating: true,
545
+ parseHTML() {
546
+ return [{
547
+ tag: "section[data-footnotes]",
548
+ contentElement: "ol"
549
+ }];
550
+ },
551
+ renderHTML() {
552
+ return [
553
+ "section",
554
+ {
555
+ "data-footnotes": "",
556
+ class: "footnotes"
557
+ },
558
+ [
559
+ "h2",
560
+ {
561
+ id: "footnote:label",
562
+ class: "sr-only"
563
+ },
564
+ "Footnotes"
565
+ ],
566
+ ["ol", 0]
567
+ ];
568
+ }
569
+ });
570
+ /**
571
+ * One note, `[^label]: …`, saved as `<li id="fn:label">`.
572
+ * @invariant It exists only inside `LoomaFootnotes`. Its back-links are presentation that Looma
573
+ * Markdown renders, so the editor reads past them and saves none.
574
+ */
575
+ var LoomaFootnote = Node$1.create({
576
+ name: "loomaFootnote",
577
+ content: "block+",
578
+ defining: true,
579
+ addAttributes() {
580
+ return { label: {
581
+ default: "",
582
+ parseHTML: (element) => labelFrom(`#${element.id}`, "fn:") ?? "",
583
+ renderHTML: () => ({})
584
+ } };
585
+ },
586
+ parseHTML() {
587
+ return [{
588
+ tag: "li",
589
+ priority: 1100,
590
+ context: "loomaFootnotes/"
591
+ }, {
592
+ tag: "a[data-footnote-backref]",
593
+ priority: 1100,
594
+ ignore: true
595
+ }];
596
+ },
597
+ renderHTML({ node }) {
598
+ return [
599
+ "li",
600
+ { id: `fn:${node.attrs.label}` },
601
+ 0
602
+ ];
603
+ }
604
+ });
605
+ //#endregion
478
606
  //#region src/editor/extensions/tabs.ts
479
607
  /**
480
608
  * Tabs or a code group, kept whole so Looma Markdown's `::: tabs` and `::: code-group` survive
@@ -9728,6 +9856,119 @@ function applyImageAttributes(image, attributes) {
9728
9856
  if (typeof placement === "string" && PLACEMENTS.has(placement) && placement !== "block") image.attrSet("data-placement", placement);
9729
9857
  if (attributes.responsive === true) image.attrSet("data-looma-responsive", "");
9730
9858
  }
9859
+ var FOOTNOTE_DEFINITION = /^\[\^([^\s\]]+)\]:/;
9860
+ var FOOTNOTE_REFERENCE = /^\[\^([^\s\]]+)\]/;
9861
+ function footnoteDefinition(state, startLine, endLine, silent) {
9862
+ if (state.sCount[startLine] - state.blkIndent >= 4) return false;
9863
+ const start = state.bMarks[startLine] + state.tShift[startLine];
9864
+ const match = FOOTNOTE_DEFINITION.exec(state.src.slice(start, state.eMarks[startLine]));
9865
+ if (!match) return false;
9866
+ if (silent) return true;
9867
+ (state.env.footnotes ??= /* @__PURE__ */ new Set()).add(match[1]);
9868
+ const open = state.push("looma_footnote_open", "li", 1);
9869
+ Object.assign(open, {
9870
+ meta: { label: match[1] },
9871
+ block: true,
9872
+ map: [startLine, startLine]
9873
+ });
9874
+ const saved = {
9875
+ bMark: state.bMarks[startLine],
9876
+ tShift: state.tShift[startLine],
9877
+ sCount: state.sCount[startLine],
9878
+ parentType: state.parentType
9879
+ };
9880
+ let position = start + match[0].length;
9881
+ const contentStart = position;
9882
+ let offset = state.sCount[startLine] + match[0].length;
9883
+ const initial = offset;
9884
+ while (position < state.eMarks[startLine] && (state.src[position] === " " || state.src[position] === " ")) {
9885
+ offset += state.src[position] === " " ? 4 - (offset + state.bsCount[startLine]) % 4 : 1;
9886
+ position++;
9887
+ }
9888
+ state.tShift[startLine] = position - contentStart;
9889
+ state.sCount[startLine] = offset - initial;
9890
+ state.bMarks[startLine] = contentStart;
9891
+ state.blkIndent += 4;
9892
+ state.parentType = "footnote";
9893
+ if (state.sCount[startLine] < state.blkIndent) state.sCount[startLine] += state.blkIndent;
9894
+ state.md.block.tokenize(state, startLine, endLine);
9895
+ state.parentType = saved.parentType;
9896
+ state.blkIndent -= 4;
9897
+ state.tShift[startLine] = saved.tShift;
9898
+ state.sCount[startLine] = saved.sCount;
9899
+ state.bMarks[startLine] = saved.bMark;
9900
+ open.map[1] = state.line;
9901
+ state.push("looma_footnote_close", "li", -1).block = true;
9902
+ return true;
9903
+ }
9904
+ function footnoteReference(state, silent) {
9905
+ if (state.src.charCodeAt(state.pos) !== 91 || state.src.charCodeAt(state.pos + 1) !== 94) return false;
9906
+ const match = FOOTNOTE_REFERENCE.exec(state.src.slice(state.pos, state.posMax));
9907
+ if (!match || !state.env.footnotes?.has(match[1])) return false;
9908
+ if (!silent) state.push("looma_footnote_ref", "sup", 0).meta = { label: match[1] };
9909
+ state.pos += match[0].length;
9910
+ return true;
9911
+ }
9912
+ function footnotes(state) {
9913
+ const definitions = /* @__PURE__ */ new Map();
9914
+ const body = [];
9915
+ for (let index = 0; index < state.tokens.length; index++) {
9916
+ const token = state.tokens[index];
9917
+ if (token.type !== "looma_footnote_open") {
9918
+ body.push(token);
9919
+ continue;
9920
+ }
9921
+ let depth = 0, end = index;
9922
+ for (; end < state.tokens.length; end++) if (state.tokens[end].type === "looma_footnote_open") depth++;
9923
+ else if (state.tokens[end].type === "looma_footnote_close" && --depth === 0) break;
9924
+ const label = token.meta.label;
9925
+ if (!definitions.has(label)) definitions.set(label, state.tokens.slice(index, end + 1));
9926
+ index = end;
9927
+ }
9928
+ if (!definitions.size) return;
9929
+ const order = [];
9930
+ const counts = /* @__PURE__ */ new Map();
9931
+ const number = (tokens) => {
9932
+ for (const token of tokens) for (const child of token.children ?? []) {
9933
+ if (child.type !== "looma_footnote_ref") continue;
9934
+ const label = child.meta.label;
9935
+ if (!counts.has(label)) order.push(label);
9936
+ const occurrence = (counts.get(label) ?? 0) + 1;
9937
+ counts.set(label, occurrence);
9938
+ child.meta = {
9939
+ label,
9940
+ number: order.indexOf(label) + 1,
9941
+ occurrence
9942
+ };
9943
+ }
9944
+ };
9945
+ number(body);
9946
+ const notes = [];
9947
+ for (let index = 0; index < order.length; index++) {
9948
+ const label = order[index];
9949
+ const tokens = definitions.get(label);
9950
+ number(tokens);
9951
+ const backrefs = Object.assign(new state.Token("looma_footnote_backrefs", "", 0), { meta: {
9952
+ label,
9953
+ number: index + 1
9954
+ } });
9955
+ const last = tokens.length - 2;
9956
+ if (tokens[last].type === "paragraph_close") tokens.splice(last, 0, backrefs);
9957
+ else tokens.splice(last + 1, 0, new state.Token("paragraph_open", "p", 1), backrefs, new state.Token("paragraph_close", "p", -1));
9958
+ notes.push(...tokens);
9959
+ }
9960
+ if (!notes.length) {
9961
+ state.tokens = body;
9962
+ return;
9963
+ }
9964
+ for (const note of notes) if (note.type === "looma_footnote_open") note.meta.count = counts.get(note.meta.label);
9965
+ state.tokens = [
9966
+ ...body,
9967
+ Object.assign(new state.Token("looma_footnotes_open", "section", 1), { block: true }),
9968
+ ...notes,
9969
+ Object.assign(new state.Token("looma_footnotes_close", "section", -1), { block: true })
9970
+ ];
9971
+ }
9731
9972
  function tableOfContents(tokens, format, depth, escape) {
9732
9973
  const roots = [];
9733
9974
  const ancestors = [];
@@ -9753,7 +9994,7 @@ function tableOfContents(tokens, format, depth, escape) {
9753
9994
  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`;
9754
9995
  }
9755
9996
  /**
9756
- * Adds Looma Markdown to a markdown-it instance: containers, alerts, task lists, the table of
9997
+ * Adds Looma Markdown to a markdown-it instance: containers, alerts, task lists, footnotes, the table of
9757
9998
  * contents, chips, mentions, brace attributes, heading anchors, code meta and front matter.
9758
9999
  * @contract Renders the canonical HTML, which is the HTML LoomaEditor saves. It is not sanitized.
9759
10000
  * Linkify and raw HTML are instance options; `createLoomaMarkdownIt` turns both on.
@@ -9761,6 +10002,7 @@ function tableOfContents(tokens, format, depth, escape) {
9761
10002
  function loomaMarkdown$1(md, { codeNotation = "apply" } = {}) {
9762
10003
  const escape = md.utils.escapeHtml;
9763
10004
  md.block.ruler.before("table", "looma_frontmatter", frontmatter);
10005
+ md.block.ruler.before("reference", "looma_footnote", footnoteDefinition, { alt: ["paragraph", "reference"] });
9764
10006
  md.block.ruler.before("fence", "looma_container", container, { alt: [
9765
10007
  "paragraph",
9766
10008
  "reference",
@@ -9774,9 +10016,11 @@ function loomaMarkdown$1(md, { codeNotation = "apply" } = {}) {
9774
10016
  "list"
9775
10017
  ] });
9776
10018
  md.inline.ruler.before("link", "looma_directive", directive);
10019
+ md.inline.ruler.before("link", "looma_footnote_ref", footnoteReference);
9777
10020
  md.core.ruler.after("block", "looma_task_lists", taskLists);
9778
10021
  md.core.ruler.before("inline", "looma_alerts", alerts);
9779
10022
  md.core.ruler.before("text_join", "looma_attributes", attributes);
10023
+ md.core.ruler.after("inline", "looma_footnotes", footnotes);
9780
10024
  const rules = md.renderer.rules;
9781
10025
  rules.looma_frontmatter = () => "";
9782
10026
  rules.looma_container_open = (tokens, index) => {
@@ -9798,6 +10042,22 @@ function loomaMarkdown$1(md, { codeNotation = "apply" } = {}) {
9798
10042
  };
9799
10043
  rules.looma_tab_open = (tokens, index) => `<section aria-label="${escape(tokens[index].meta.label)}">\n`;
9800
10044
  rules.looma_tab_close = () => "</section>\n";
10045
+ rules.looma_footnote_ref = (tokens, index) => {
10046
+ const { label, number, occurrence } = tokens[index].meta;
10047
+ return `<sup><a href="#fn:${escape(label)}" id="fnref:${escape(label)}${occurrence > 1 ? `:${occurrence}` : ""}" data-footnote-ref="" aria-describedby="footnote:label">${number}</a></sup>`;
10048
+ };
10049
+ rules.looma_footnote_backrefs = (tokens, index) => {
10050
+ const { label, number } = tokens[index].meta;
10051
+ const count = tokens.find((token) => token.type === "looma_footnote_open" && token.meta.label === label)?.meta.count ?? 1;
10052
+ return Array.from({ length: count }, (_, occurrence) => {
10053
+ const suffix = occurrence ? `:${occurrence + 1}` : "";
10054
+ return ` <a href="#fnref:${escape(label)}${suffix}" data-footnote-backref="" aria-label="Back to reference ${number}${occurrence ? `-${occurrence + 1}` : ""}">↩${occurrence ? `<sup>${occurrence + 1}</sup>` : ""}</a>`;
10055
+ }).join("");
10056
+ };
10057
+ rules.looma_footnotes_open = () => "<section data-footnotes=\"\" class=\"footnotes\">\n<h2 id=\"footnote:label\" class=\"sr-only\">Footnotes</h2>\n<ol>\n";
10058
+ rules.looma_footnotes_close = () => "</ol>\n</section>\n";
10059
+ rules.looma_footnote_open = (tokens, index) => `<li id="fn:${escape(tokens[index].meta.label)}">\n`;
10060
+ rules.looma_footnote_close = () => "</li>\n";
9801
10061
  rules.looma_toc = (tokens, index) => tableOfContents(tokens, tokens[index].meta.format, tokens[index].meta.depth, escape);
9802
10062
  rules.looma_chip = (tokens, index) => {
9803
10063
  const { label, attributes } = tokens[index].meta;
@@ -9955,6 +10215,9 @@ var NODES = {
9955
10215
  })}`);
9956
10216
  state.closeBlock(node);
9957
10217
  },
10218
+ loomaFootnoteReference: (state, node) => state.write(`[^${node.attrs.label}]`),
10219
+ loomaFootnotes: (state, node) => state.renderContent(node),
10220
+ loomaFootnote: (state, node) => state.wrapBlock(" ", `[^${node.attrs.label}]: `, node, () => state.renderContent(node)),
9958
10221
  loomaChip: (state, node) => {
9959
10222
  const tone = normalizeLoomaChipTone(node.attrs.color);
9960
10223
  state.write(`:chip[${label(String(node.attrs.label ?? ""))}]${formatLoomaAttributes({ tone: tone !== "neutral" ? tone : null })}`);
@@ -10838,6 +11101,9 @@ function getDefaultEditorExtensions(options = {}) {
10838
11101
  ...imageInline ? [] : [LoomaFigure, LoomaFigcaption],
10839
11102
  LoomaExpand.configure({ nodeView: expandNodeView ?? null }),
10840
11103
  LoomaTableOfContents.configure({ nodeView: tableOfContentsNodeView ?? null }),
11104
+ LoomaFootnoteReference,
11105
+ LoomaFootnotes,
11106
+ LoomaFootnote,
10841
11107
  dividerNodeView ? HorizontalRule.extend({ addNodeView: () => dividerNodeView }) : HorizontalRule,
10842
11108
  HardBreak,
10843
11109
  History,
@@ -11827,4 +12093,4 @@ function createLoomaSlashCommandExtension(options = {}) {
11827
12093
  });
11828
12094
  }
11829
12095
  //#endregion
11830
- export { 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_ACTIVE_BLOCK_BLUR_GRACE_MS as J, normalizeLoomaChipColor as K, normalizeTableOfContentsConfig as L, LoomaImage as M, normalizeImageDimension as N, DEFAULT_MENTION_RESULT_LIMIT as O, normalizeImagePlacement as P, LoomaActiveBlockPluginKey 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_TYPING_IDLE_MS as X, LOOMA_ACTIVE_BLOCK_CLASS as Y, LoomaActiveBlock 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 };
12096
+ export { LOOMA_ACTIVE_BLOCK_TYPING_IDLE_MS as $, filterLoomaMentionItems as A, LoomaFootnote as B, loomaMarkdownToHtml as C, createLoomaMentionExtension as D, LoomaMentionSuggestionPluginKey as E, LoomaTableOfContents as F, LoomaExpand as G, LoomaFootnotes as H, getTableOfContentsEntries as I, LoomaChip as J, revealLoomaExpandAt as K, normalizeTableOfContentsConfig as L, LoomaImage as M, normalizeImageDimension as N, DEFAULT_MENTION_RESULT_LIMIT as O, normalizeImagePlacement as P, LOOMA_ACTIVE_BLOCK_CLASS as Q, LoomaTab as R, loomaMarkdownFromDoc as S, siteRelativeHref as T, LoomaFigcaption as U, LoomaFootnoteReference as V, LoomaFigure as W, LoomaCallout as X, normalizeLoomaChipColor as Y, LOOMA_ACTIVE_BLOCK_BLUR_GRACE_MS as Z, getActiveTableCellAlignment as _, getActiveTableUiState as a, setActiveTableCellBackground as b, insertTableAtRange as c, LoomaTableKit as d, LoomaActiveBlock as et, 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, LOOMA_CHIP_COLORS as q, filterLoomaSlashCommands as r, handleTableOverlayAction as s, LoomaSlashCommand as t, LoomaActiveBlockPluginKey as tt, shouldShowTextFormattingToolbar as u, getActiveTableCellBackground as v, createSiteRelativeLinks as w, LoomaSmartPaste as x, setActiveTableCellAlignment as y, LoomaTabs as z };
@@ -232,6 +232,7 @@ function toNode(node, path, context) {
232
232
  "mention",
233
233
  "loomaReference",
234
234
  "loomaChip",
235
+ "loomaFootnoteReference",
235
236
  "hardBreak"
236
237
  ].includes(node.type ?? "")) fail(path, "marks on a block node");
237
238
  for (const mark of [...node.marks ?? []].reverse()) {
@@ -395,6 +396,22 @@ function toBareNode(node, path, context) {
395
396
  value: string(attrs.label),
396
397
  attributes: { tone: string(attrs.color, "neutral") }
397
398
  };
399
+ case "loomaFootnoteReference":
400
+ fields(node, [
401
+ "label",
402
+ "number",
403
+ "occurrence"
404
+ ], path);
405
+ return {
406
+ type: "inline",
407
+ subType: "footnote",
408
+ value: string(attrs.label)
409
+ };
410
+ case "loomaFootnotes": return block("container", [], { subType: "footnotes" });
411
+ case "loomaFootnote": return block("container", ["label"], {
412
+ subType: "footnote",
413
+ title: title(string(attrs.label))
414
+ });
398
415
  case "loomaCallout": return block("container", ["tone", "heading"], {
399
416
  subType: string(attrs.tone, "note"),
400
417
  ...attrs.heading ? { title: title(string(attrs.heading)) } : {}
@@ -779,6 +796,14 @@ function fromNode(node, path, marks = []) {
779
796
  ...marks.length ? { marks } : {}
780
797
  }];
781
798
  }
799
+ if (node.subType === "footnote") {
800
+ check();
801
+ return [{
802
+ type: "loomaFootnoteReference",
803
+ attrs: { label: text(node) },
804
+ ...marks.length ? { marks } : {}
805
+ }];
806
+ }
782
807
  if (node.subType === "chip") {
783
808
  check(["tone"]);
784
809
  if (attrs.tone !== void 0 && !LOOMA_CHIP_TONES.includes(attrs.tone)) fail(path, "unsupported chip tone");
@@ -852,6 +877,14 @@ function fromNode(node, path, marks = []) {
852
877
  }]
853
878
  }];
854
879
  }
880
+ if (node.subType === "footnotes") {
881
+ check();
882
+ return wrap("loomaFootnotes");
883
+ }
884
+ if (node.subType === "footnote") {
885
+ check();
886
+ return wrap("loomaFootnote", { label });
887
+ }
855
888
  if (node.subType === "toc") {
856
889
  check(["depth", "format"]);
857
890
  if (node.title) fail(path, "table-of-contents title");
@@ -0,0 +1,18 @@
1
+ import { Node } from "@tiptap/core";
2
+ /**
3
+ * A footnote reference, `[^label]`, saved as `<sup><a data-footnote-ref>`.
4
+ * @invariant `label` names its note. `number` and `occurrence` are the note's number and this
5
+ * reference's place among the note's references, as the document had them when it loaded.
6
+ */
7
+ export declare const LoomaFootnoteReference: Node<any, any>;
8
+ /**
9
+ * The document's notes, saved as `<section data-footnotes>` with its `<ol>`.
10
+ * @invariant It holds only footnotes; Markdown writes it as definitions after the text.
11
+ */
12
+ export declare const LoomaFootnotes: Node<any, any>;
13
+ /**
14
+ * One note, `[^label]: …`, saved as `<li id="fn:label">`.
15
+ * @invariant It exists only inside `LoomaFootnotes`. Its back-links are presentation that Looma
16
+ * Markdown renders, so the editor reads past them and saves none.
17
+ */
18
+ export declare const LoomaFootnote: Node<any, any>;
@@ -11,6 +11,7 @@ export { LoomaCallout, LOOMA_CALLOUT_TONES, type LoomaCalloutTone, } from "./cal
11
11
  export { LoomaChip, LOOMA_CHIP_COLORS, normalizeLoomaChipColor, type LoomaChipColor } from "./chip.js";
12
12
  export { LoomaExpand, revealLoomaExpandAt, type LoomaExpandAttributes } from "./expand.js";
13
13
  export { LoomaFigcaption, LoomaFigure } from "./figure.js";
14
+ export { LoomaFootnote, LoomaFootnoteReference, LoomaFootnotes } from "./footnote.js";
14
15
  export { LoomaTab, LoomaTabs } from "./tabs.js";
15
16
  export { LoomaTableOfContents, getTableOfContentsEntries, normalizeTableOfContentsConfig, type LoomaTableOfContentsConfig, type LoomaTableOfContentsEntry, type LoomaTableOfContentsFormat, } from "./table-of-contents.js";
16
17
  export { getDefaultEditorExtensions, getLoomaTableExtensions, LoomaTableKit, type DefaultEditorExtensionsOptions, type LoomaCodeLanguages, } from "./preset.js";
@@ -1,4 +1,4 @@
1
1
  import { n as TABLE_CELL_BACKGROUND_PRESETS } from "../chunks/table-backgrounds.js";
2
- import { 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_ACTIVE_BLOCK_BLUR_GRACE_MS, K as normalizeLoomaChipColor, L as normalizeTableOfContentsConfig, M as LoomaImage, N as normalizeImageDimension, O as DEFAULT_MENTION_RESULT_LIMIT, P as normalizeImagePlacement, Q as LoomaActiveBlockPluginKey, 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_TYPING_IDLE_MS, Y as LOOMA_ACTIVE_BLOCK_CLASS, Z as LoomaActiveBlock, _ 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";
2
+ import { $ as LOOMA_ACTIVE_BLOCK_TYPING_IDLE_MS, A as filterLoomaMentionItems, B as LoomaFootnote, C as loomaMarkdownToHtml, D as createLoomaMentionExtension, E as LoomaMentionSuggestionPluginKey, F as LoomaTableOfContents, G as LoomaExpand, H as LoomaFootnotes, I as getTableOfContentsEntries, J as LoomaChip, K as revealLoomaExpandAt, L as normalizeTableOfContentsConfig, M as LoomaImage, N as normalizeImageDimension, O as DEFAULT_MENTION_RESULT_LIMIT, P as normalizeImagePlacement, Q as LOOMA_ACTIVE_BLOCK_CLASS, R as LoomaTab, S as loomaMarkdownFromDoc, T as siteRelativeHref, U as LoomaFigcaption, V as LoomaFootnoteReference, W as LoomaFigure, X as LoomaCallout, Y as normalizeLoomaChipColor, Z as LOOMA_ACTIVE_BLOCK_BLUR_GRACE_MS, _ as getActiveTableCellAlignment, a as getActiveTableUiState, b as setActiveTableCellBackground, c as insertTableAtRange, d as LoomaTableKit, et as LoomaActiveBlock, 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 LOOMA_CHIP_COLORS, r as filterLoomaSlashCommands, s as handleTableOverlayAction, t as LoomaSlashCommand, tt as LoomaActiveBlockPluginKey, u as shouldShowTextFormattingToolbar, v as getActiveTableCellBackground, w as createSiteRelativeLinks, x as LoomaSmartPaste, y as setActiveTableCellAlignment, z as LoomaTabs } from "../chunks/extensions.js";
3
3
  import { a as setReferenceDisplay, i as referenceDisplay, n as LoomaReferencePresentation, o as LOOMA_CALLOUT_TONES, r as getReferenceDisplay, t as LoomaReference } from "../chunks/reference-display.js";
4
- 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, LoomaReference, LoomaReferencePresentation, 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, getReferenceDisplay, getTableOfContentsEntries, handleTableAction, handleTableOverlayAction, insertTableAtRange, loomaMarkdownFromDoc, loomaMarkdownToHtml, normalizeActiveTableColumnWidths, normalizeImageDimension, normalizeImagePlacement, normalizeLoomaChipColor, normalizeMentionResultLimit, normalizeTableOfContentsConfig, referenceDisplay, revealLoomaExpandAt, setActiveTableCellAlignment, setActiveTableCellBackground, setReferenceDisplay, shouldShowTextFormattingToolbar, siteRelativeHref };
4
+ 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, LoomaFootnote, LoomaFootnoteReference, LoomaFootnotes, LoomaImage, LoomaMentionSuggestionPluginKey, LoomaReference, LoomaReferencePresentation, 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, getReferenceDisplay, getTableOfContentsEntries, handleTableAction, handleTableOverlayAction, insertTableAtRange, loomaMarkdownFromDoc, loomaMarkdownToHtml, normalizeActiveTableColumnWidths, normalizeImageDimension, normalizeImagePlacement, normalizeLoomaChipColor, normalizeMentionResultLimit, normalizeTableOfContentsConfig, referenceDisplay, revealLoomaExpandAt, setActiveTableCellAlignment, setActiveTableCellBackground, setReferenceDisplay, shouldShowTextFormattingToolbar, siteRelativeHref };
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 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_ACTIVE_BLOCK_BLUR_GRACE_MS, K as normalizeLoomaChipColor, L as normalizeTableOfContentsConfig, M as LoomaImage, N as normalizeImageDimension, O as DEFAULT_MENTION_RESULT_LIMIT, P as normalizeImagePlacement, Q as LoomaActiveBlockPluginKey, 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_TYPING_IDLE_MS, Y as LOOMA_ACTIVE_BLOCK_CLASS, Z as LoomaActiveBlock, _ 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
+ import { $ as LOOMA_ACTIVE_BLOCK_TYPING_IDLE_MS, A as filterLoomaMentionItems, B as LoomaFootnote, C as loomaMarkdownToHtml, D as createLoomaMentionExtension, E as LoomaMentionSuggestionPluginKey, F as LoomaTableOfContents, G as LoomaExpand, H as LoomaFootnotes, I as getTableOfContentsEntries, J as LoomaChip, K as revealLoomaExpandAt, L as normalizeTableOfContentsConfig, M as LoomaImage, N as normalizeImageDimension, O as DEFAULT_MENTION_RESULT_LIMIT, P as normalizeImagePlacement, Q as LOOMA_ACTIVE_BLOCK_CLASS, R as LoomaTab, S as loomaMarkdownFromDoc, T as siteRelativeHref, U as LoomaFigcaption, V as LoomaFootnoteReference, W as LoomaFigure, X as LoomaCallout, Y as normalizeLoomaChipColor, Z as LOOMA_ACTIVE_BLOCK_BLUR_GRACE_MS, _ as getActiveTableCellAlignment, a as getActiveTableUiState, b as setActiveTableCellBackground, c as insertTableAtRange, d as LoomaTableKit, et as LoomaActiveBlock, 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 LOOMA_CHIP_COLORS, r as filterLoomaSlashCommands, s as handleTableOverlayAction, t as LoomaSlashCommand, tt as LoomaActiveBlockPluginKey, 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 { a as setReferenceDisplay, i as referenceDisplay, n as LoomaReferencePresentation, o as LOOMA_CALLOUT_TONES, r as getReferenceDisplay, t as LoomaReference } from "./chunks/reference-display.js";
5
5
  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";
6
6
  //#region src/editor/icons.ts
@@ -124,4 +124,4 @@ function getVisualViewportRect(owner = window) {
124
124
  };
125
125
  }
126
126
  //#endregion
127
- 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, LoomaReference, LoomaReferencePresentation, 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, getReferenceDisplay, getTableOfContentsEntries, getVisualViewportRect, handleTableAction, handleTableOverlayAction, insertTableAtRange, loomaIconMarkup, loomaMarkdownFromDoc, loomaMarkdownToHtml, measureTableOverlayGeometry, normalizeActiveTableColumnWidths, normalizeImageDimension, normalizeImagePlacement, normalizeLoomaChipColor, normalizeMentionResultLimit, normalizeTableOfContentsConfig, referenceDisplay, resolveTableCellAt, revealLoomaExpandAt, setActiveTableCellAlignment, setActiveTableCellBackground, setReferenceDisplay, shouldShowTextFormattingToolbar, siteRelativeHref };
127
+ 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, LoomaFootnote, LoomaFootnoteReference, LoomaFootnotes, LoomaImage, LoomaMentionSuggestionPluginKey, LoomaReference, LoomaReferencePresentation, 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, getReferenceDisplay, getTableOfContentsEntries, getVisualViewportRect, handleTableAction, handleTableOverlayAction, insertTableAtRange, loomaIconMarkup, loomaMarkdownFromDoc, loomaMarkdownToHtml, measureTableOverlayGeometry, normalizeActiveTableColumnWidths, normalizeImageDimension, normalizeImagePlacement, normalizeLoomaChipColor, normalizeMentionResultLimit, normalizeTableOfContentsConfig, referenceDisplay, resolveTableCellAt, revealLoomaExpandAt, setActiveTableCellAlignment, setActiveTableCellBackground, setReferenceDisplay, shouldShowTextFormattingToolbar, siteRelativeHref };
@@ -51,6 +51,16 @@ var files = /* #__PURE__ */ Object.assign({
51
51
  "../../markdown-fixtures/details/open.md": "::: details Show the full log {open}\nThe build finished in 42 seconds.\n:::\n",
52
52
  "../../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",
53
53
  "../../markdown-fixtures/figures/caption.md": "::: figure The build, from *source* to release\n![The build pipeline](./pipeline.svg)\n:::\n",
54
+ "../../markdown-fixtures/footnotes/basic.html": "<p>Markdown renders tables<sup><a href=\"#fn:tables\" id=\"fnref:tables\" data-footnote-ref=\"\" aria-describedby=\"footnote:label\">1</a></sup> and task lists<sup><a href=\"#fn:1\" id=\"fnref:1\" data-footnote-ref=\"\" aria-describedby=\"footnote:label\">2</a></sup>.</p>\n<section data-footnotes=\"\" class=\"footnotes\">\n<h2 id=\"footnote:label\" class=\"sr-only\">Footnotes</h2>\n<ol>\n<li id=\"fn:tables\">\n<p>GitHub Flavored Markdown tables. <a href=\"#fnref:tables\" data-footnote-backref=\"\" aria-label=\"Back to reference 1\">↩</a></p>\n</li>\n<li id=\"fn:1\">\n<p>Each item has a checkbox. <a href=\"#fnref:1\" data-footnote-backref=\"\" aria-label=\"Back to reference 2\">↩</a></p>\n</li>\n</ol>\n</section>\n",
55
+ "../../markdown-fixtures/footnotes/basic.md": "Markdown renders tables[^tables] and task lists[^1].\n\n[^tables]: GitHub Flavored Markdown tables.\n\n[^1]: Each item has a checkbox.\n",
56
+ "../../markdown-fixtures/footnotes/blocks.canonical.md": "The build writes a log[^log].\n\n[^log]: The log holds every step.\n\n ```sh\n pnpm build --verbose\n ```\n\n It is kept for a week.\n",
57
+ "../../markdown-fixtures/footnotes/blocks.html": "<p>The build writes a log<sup><a href=\"#fn:log\" id=\"fnref:log\" data-footnote-ref=\"\" aria-describedby=\"footnote:label\">1</a></sup>.</p>\n<section data-footnotes=\"\" class=\"footnotes\">\n<h2 id=\"footnote:label\" class=\"sr-only\">Footnotes</h2>\n<ol>\n<li id=\"fn:log\">\n<p>The log holds every step.</p>\n<pre><code class=\"language-sh\">pnpm build --verbose</code></pre>\n<p>It is kept for a week. <a href=\"#fnref:log\" data-footnote-backref=\"\" aria-label=\"Back to reference 1\">↩</a></p>\n</li>\n</ol>\n</section>\n",
58
+ "../../markdown-fixtures/footnotes/blocks.md": "[^log]: The log holds every step.\n\n ```sh\n pnpm build --verbose\n ```\n\n It is kept for a week.\n\nThe build writes a log[^log].\n",
59
+ "../../markdown-fixtures/footnotes/repeated.html": "<p>The build<sup><a href=\"#fn:build\" id=\"fnref:build\" data-footnote-ref=\"\" aria-describedby=\"footnote:label\">1</a></sup> runs in CI, and every build<sup><a href=\"#fn:build\" id=\"fnref:build:2\" data-footnote-ref=\"\" aria-describedby=\"footnote:label\">1</a></sup> uploads its report<sup><a href=\"#fn:report\" id=\"fnref:report\" data-footnote-ref=\"\" aria-describedby=\"footnote:label\">2</a></sup>.</p>\n<section data-footnotes=\"\" class=\"footnotes\">\n<h2 id=\"footnote:label\" class=\"sr-only\">Footnotes</h2>\n<ol>\n<li id=\"fn:build\">\n<p>A clean install, then the tests. <a href=\"#fnref:build\" data-footnote-backref=\"\" aria-label=\"Back to reference 1\">↩</a> <a href=\"#fnref:build:2\" data-footnote-backref=\"\" aria-label=\"Back to reference 1-2\">↩<sup>2</sup></a></p>\n</li>\n<li id=\"fn:report\">\n<p>The coverage summary. <a href=\"#fnref:report\" data-footnote-backref=\"\" aria-label=\"Back to reference 2\">↩</a></p>\n</li>\n</ol>\n</section>\n",
60
+ "../../markdown-fixtures/footnotes/repeated.md": "The build[^build] runs in CI, and every build[^build] uploads its report[^report].\n\n[^build]: A clean install, then the tests.\n\n[^report]: The coverage summary.\n",
61
+ "../../markdown-fixtures/footnotes/unreferenced.canonical.md": "A note with no definition\\[^missing\\] stays text, and a definition nobody refers to renders nothing.\n",
62
+ "../../markdown-fixtures/footnotes/unreferenced.html": "<p>A note with no definition[^missing] stays text, and a definition nobody refers to renders nothing.</p>\n",
63
+ "../../markdown-fixtures/footnotes/unreferenced.md": "A note with no definition[^missing] stays text, and a definition nobody refers to renders nothing.\n\n[^unused]: Unused.\n",
54
64
  "../../markdown-fixtures/front-matter/list-only.canonical.md": "# Notes\n",
55
65
  "../../markdown-fixtures/front-matter/list-only.html": "<h1 id=\"notes\">Notes</h1>\n",
56
66
  "../../markdown-fixtures/front-matter/list-only.md": "---\naliases:\n - /old/\n - /older/\n---\n\n# Notes\n",
@@ -8,7 +8,7 @@ export interface LoomaMarkdownOptions {
8
8
  codeNotation?: "apply" | "keep";
9
9
  }
10
10
  /**
11
- * Adds Looma Markdown to a markdown-it instance: containers, alerts, task lists, the table of
11
+ * Adds Looma Markdown to a markdown-it instance: containers, alerts, task lists, footnotes, the table of
12
12
  * contents, chips, mentions, brace attributes, heading anchors, code meta and front matter.
13
13
  * @contract Renders the canonical HTML, which is the HTML LoomaEditor saves. It is not sanitized.
14
14
  * Linkify and raw HTML are instance options; `createLoomaMarkdownIt` turns both on.
@@ -296,6 +296,119 @@ function applyImageAttributes(image, attributes) {
296
296
  if (typeof placement === "string" && PLACEMENTS.has(placement) && placement !== "block") image.attrSet("data-placement", placement);
297
297
  if (attributes.responsive === true) image.attrSet("data-looma-responsive", "");
298
298
  }
299
+ var FOOTNOTE_DEFINITION = /^\[\^([^\s\]]+)\]:/;
300
+ var FOOTNOTE_REFERENCE = /^\[\^([^\s\]]+)\]/;
301
+ function footnoteDefinition(state, startLine, endLine, silent) {
302
+ if (state.sCount[startLine] - state.blkIndent >= 4) return false;
303
+ const start = state.bMarks[startLine] + state.tShift[startLine];
304
+ const match = FOOTNOTE_DEFINITION.exec(state.src.slice(start, state.eMarks[startLine]));
305
+ if (!match) return false;
306
+ if (silent) return true;
307
+ (state.env.footnotes ??= /* @__PURE__ */ new Set()).add(match[1]);
308
+ const open = state.push("looma_footnote_open", "li", 1);
309
+ Object.assign(open, {
310
+ meta: { label: match[1] },
311
+ block: true,
312
+ map: [startLine, startLine]
313
+ });
314
+ const saved = {
315
+ bMark: state.bMarks[startLine],
316
+ tShift: state.tShift[startLine],
317
+ sCount: state.sCount[startLine],
318
+ parentType: state.parentType
319
+ };
320
+ let position = start + match[0].length;
321
+ const contentStart = position;
322
+ let offset = state.sCount[startLine] + match[0].length;
323
+ const initial = offset;
324
+ while (position < state.eMarks[startLine] && (state.src[position] === " " || state.src[position] === " ")) {
325
+ offset += state.src[position] === " " ? 4 - (offset + state.bsCount[startLine]) % 4 : 1;
326
+ position++;
327
+ }
328
+ state.tShift[startLine] = position - contentStart;
329
+ state.sCount[startLine] = offset - initial;
330
+ state.bMarks[startLine] = contentStart;
331
+ state.blkIndent += 4;
332
+ state.parentType = "footnote";
333
+ if (state.sCount[startLine] < state.blkIndent) state.sCount[startLine] += state.blkIndent;
334
+ state.md.block.tokenize(state, startLine, endLine);
335
+ state.parentType = saved.parentType;
336
+ state.blkIndent -= 4;
337
+ state.tShift[startLine] = saved.tShift;
338
+ state.sCount[startLine] = saved.sCount;
339
+ state.bMarks[startLine] = saved.bMark;
340
+ open.map[1] = state.line;
341
+ state.push("looma_footnote_close", "li", -1).block = true;
342
+ return true;
343
+ }
344
+ function footnoteReference(state, silent) {
345
+ if (state.src.charCodeAt(state.pos) !== 91 || state.src.charCodeAt(state.pos + 1) !== 94) return false;
346
+ const match = FOOTNOTE_REFERENCE.exec(state.src.slice(state.pos, state.posMax));
347
+ if (!match || !state.env.footnotes?.has(match[1])) return false;
348
+ if (!silent) state.push("looma_footnote_ref", "sup", 0).meta = { label: match[1] };
349
+ state.pos += match[0].length;
350
+ return true;
351
+ }
352
+ function footnotes(state) {
353
+ const definitions = /* @__PURE__ */ new Map();
354
+ const body = [];
355
+ for (let index = 0; index < state.tokens.length; index++) {
356
+ const token = state.tokens[index];
357
+ if (token.type !== "looma_footnote_open") {
358
+ body.push(token);
359
+ continue;
360
+ }
361
+ let depth = 0, end = index;
362
+ for (; end < state.tokens.length; end++) if (state.tokens[end].type === "looma_footnote_open") depth++;
363
+ else if (state.tokens[end].type === "looma_footnote_close" && --depth === 0) break;
364
+ const label = token.meta.label;
365
+ if (!definitions.has(label)) definitions.set(label, state.tokens.slice(index, end + 1));
366
+ index = end;
367
+ }
368
+ if (!definitions.size) return;
369
+ const order = [];
370
+ const counts = /* @__PURE__ */ new Map();
371
+ const number = (tokens) => {
372
+ for (const token of tokens) for (const child of token.children ?? []) {
373
+ if (child.type !== "looma_footnote_ref") continue;
374
+ const label = child.meta.label;
375
+ if (!counts.has(label)) order.push(label);
376
+ const occurrence = (counts.get(label) ?? 0) + 1;
377
+ counts.set(label, occurrence);
378
+ child.meta = {
379
+ label,
380
+ number: order.indexOf(label) + 1,
381
+ occurrence
382
+ };
383
+ }
384
+ };
385
+ number(body);
386
+ const notes = [];
387
+ for (let index = 0; index < order.length; index++) {
388
+ const label = order[index];
389
+ const tokens = definitions.get(label);
390
+ number(tokens);
391
+ const backrefs = Object.assign(new state.Token("looma_footnote_backrefs", "", 0), { meta: {
392
+ label,
393
+ number: index + 1
394
+ } });
395
+ const last = tokens.length - 2;
396
+ if (tokens[last].type === "paragraph_close") tokens.splice(last, 0, backrefs);
397
+ else tokens.splice(last + 1, 0, new state.Token("paragraph_open", "p", 1), backrefs, new state.Token("paragraph_close", "p", -1));
398
+ notes.push(...tokens);
399
+ }
400
+ if (!notes.length) {
401
+ state.tokens = body;
402
+ return;
403
+ }
404
+ for (const note of notes) if (note.type === "looma_footnote_open") note.meta.count = counts.get(note.meta.label);
405
+ state.tokens = [
406
+ ...body,
407
+ Object.assign(new state.Token("looma_footnotes_open", "section", 1), { block: true }),
408
+ ...notes,
409
+ Object.assign(new state.Token("looma_footnotes_close", "section", -1), { block: true })
410
+ ];
411
+ }
299
412
  function tableOfContents(tokens, format, depth, escape) {
300
413
  const roots = [];
301
414
  const ancestors = [];
@@ -321,7 +434,7 @@ function tableOfContents(tokens, format, depth, escape) {
321
434
  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`;
322
435
  }
323
436
  /**
324
- * Adds Looma Markdown to a markdown-it instance: containers, alerts, task lists, the table of
437
+ * Adds Looma Markdown to a markdown-it instance: containers, alerts, task lists, footnotes, the table of
325
438
  * contents, chips, mentions, brace attributes, heading anchors, code meta and front matter.
326
439
  * @contract Renders the canonical HTML, which is the HTML LoomaEditor saves. It is not sanitized.
327
440
  * Linkify and raw HTML are instance options; `createLoomaMarkdownIt` turns both on.
@@ -329,6 +442,7 @@ function tableOfContents(tokens, format, depth, escape) {
329
442
  function loomaMarkdown(md, { codeNotation = "apply" } = {}) {
330
443
  const escape = md.utils.escapeHtml;
331
444
  md.block.ruler.before("table", "looma_frontmatter", frontmatter);
445
+ md.block.ruler.before("reference", "looma_footnote", footnoteDefinition, { alt: ["paragraph", "reference"] });
332
446
  md.block.ruler.before("fence", "looma_container", container, { alt: [
333
447
  "paragraph",
334
448
  "reference",
@@ -342,9 +456,11 @@ function loomaMarkdown(md, { codeNotation = "apply" } = {}) {
342
456
  "list"
343
457
  ] });
344
458
  md.inline.ruler.before("link", "looma_directive", directive);
459
+ md.inline.ruler.before("link", "looma_footnote_ref", footnoteReference);
345
460
  md.core.ruler.after("block", "looma_task_lists", taskLists);
346
461
  md.core.ruler.before("inline", "looma_alerts", alerts);
347
462
  md.core.ruler.before("text_join", "looma_attributes", attributes);
463
+ md.core.ruler.after("inline", "looma_footnotes", footnotes);
348
464
  const rules = md.renderer.rules;
349
465
  rules.looma_frontmatter = () => "";
350
466
  rules.looma_container_open = (tokens, index) => {
@@ -366,6 +482,22 @@ function loomaMarkdown(md, { codeNotation = "apply" } = {}) {
366
482
  };
367
483
  rules.looma_tab_open = (tokens, index) => `<section aria-label="${escape(tokens[index].meta.label)}">\n`;
368
484
  rules.looma_tab_close = () => "</section>\n";
485
+ rules.looma_footnote_ref = (tokens, index) => {
486
+ const { label, number, occurrence } = tokens[index].meta;
487
+ return `<sup><a href="#fn:${escape(label)}" id="fnref:${escape(label)}${occurrence > 1 ? `:${occurrence}` : ""}" data-footnote-ref="" aria-describedby="footnote:label">${number}</a></sup>`;
488
+ };
489
+ rules.looma_footnote_backrefs = (tokens, index) => {
490
+ const { label, number } = tokens[index].meta;
491
+ const count = tokens.find((token) => token.type === "looma_footnote_open" && token.meta.label === label)?.meta.count ?? 1;
492
+ return Array.from({ length: count }, (_, occurrence) => {
493
+ const suffix = occurrence ? `:${occurrence + 1}` : "";
494
+ return ` <a href="#fnref:${escape(label)}${suffix}" data-footnote-backref="" aria-label="Back to reference ${number}${occurrence ? `-${occurrence + 1}` : ""}">↩${occurrence ? `<sup>${occurrence + 1}</sup>` : ""}</a>`;
495
+ }).join("");
496
+ };
497
+ rules.looma_footnotes_open = () => "<section data-footnotes=\"\" class=\"footnotes\">\n<h2 id=\"footnote:label\" class=\"sr-only\">Footnotes</h2>\n<ol>\n";
498
+ rules.looma_footnotes_close = () => "</ol>\n</section>\n";
499
+ rules.looma_footnote_open = (tokens, index) => `<li id="fn:${escape(tokens[index].meta.label)}">\n`;
500
+ rules.looma_footnote_close = () => "</li>\n";
369
501
  rules.looma_toc = (tokens, index) => tableOfContents(tokens, tokens[index].meta.format, tokens[index].meta.depth, escape);
370
502
  rules.looma_chip = (tokens, index) => {
371
503
  const { label, attributes } = tokens[index].meta;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@threadlabs/looma",
3
- "version": "0.30.7",
3
+ "version": "0.30.8",
4
4
  "type": "module",
5
5
  "description": "Looma components as HTML, Vue, and plain DOM, with design tokens.",
6
6
  "license": "MIT",