@threadlabs/looma 0.31.0 → 0.31.2

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.
Files changed (36) hide show
  1. package/components/shared/icons.js +1 -0
  2. package/components/shared/tooltip.js +7 -0
  3. package/components/ui-editor-footnote-editor/ui-editor-footnote-editor.html +57 -0
  4. package/components/ui-editor-footnote-editor/ui-editor-footnote-editor.js +38 -0
  5. package/components/ui-icon/ui-icon.html +1 -0
  6. package/components/ui-tooltip/ui-tooltip.js +4 -1
  7. package/dist/index.js +37 -36
  8. package/editor/chunks/extensions.js +363 -20
  9. package/editor/expressmark.js +1 -1
  10. package/editor/extensions/footnote.d.ts +40 -7
  11. package/editor/extensions/index.d.ts +1 -1
  12. package/editor/extensions/index.js +2 -2
  13. package/editor/footnote-editing.d.ts +46 -0
  14. package/editor/icons.d.ts +1 -0
  15. package/editor/index.js +4 -3
  16. package/editor/table-ui.d.ts +2 -11
  17. package/editor/toolbar-tooltip.d.ts +1 -1
  18. package/package.json +1 -1
  19. package/styles/ui-editor-footnote-editor.css +1 -0
  20. package/vanilla/UiEditorFootnoteEditor.d.ts +30 -0
  21. package/vanilla/UiEditorFootnoteEditor.js +101 -0
  22. package/vanilla/UiIcon.js +1 -1
  23. package/vanilla/index.js +1 -0
  24. package/vue/UiEditorFootnoteEditor.d.ts +57 -0
  25. package/vue/UiEditorFootnoteEditor.js +2 -0
  26. package/vue/UiEditorFootnoteEditor.vue +304 -0
  27. package/vue/UiIcon.vue +4 -0
  28. package/vue/chunks/UiEditorFootnoteEditor.js +306 -0
  29. package/vue/chunks/UiEditorImageToolbar.js +1 -1
  30. package/vue/chunks/UiEditorLinkForm.js +1 -1
  31. package/vue/chunks/UiIcon.js +15 -1
  32. package/vue/components.css +590 -575
  33. package/vue/editor/index.js +643 -45
  34. package/vue/editor/primitives.d.ts +3 -1
  35. package/vue/index.d.ts +1 -0
  36. package/vue/index.js +5 -4
@@ -1,6 +1,6 @@
1
1
  import { n as TABLE_CELL_BACKGROUND_PRESETS } from "./table-backgrounds.js";
2
2
  import { _ as parseLoomaAttributes, b as splitLoomaFrontmatter, c as LOOMA_CONTAINER_NAMES, d as formatLoomaAttributes, f as loomaCodeFence, g as normalizeLoomaChipTone, h as normalizeLoomaCalloutTone, i as referenceDisplay, l as applyLoomaCodeNotation, m as loomaHeadingSlug, n as LoomaReferencePresentation, p as loomaContainerMarker, r as getReferenceDisplay, s as LOOMA_CHIP_TONES, t as LoomaReference, u as assignLoomaHeadingIds, v as parseLoomaFenceInfo, y as readLoomaAttributes } from "./reference-display.js";
3
- import { Extension, Mark, Node as Node$1, callOrReturn, canInsertNode, combineTransactionSteps, escapeForRegEx, findChildren, findChildrenInRange, findParentNodeClosestToPos, getAttributes, getChangedRanges, getExtensionField, getMarksBetween, getText, getTextSerializersFromSchema, isNodeEmpty, isNodeSelection, markInputRule, markPasteRule, mergeAttributes, nodeInputRule, textInputRule, textblockTypeInputRule, wrappingInputRule } from "@tiptap/core";
3
+ import { Extension, InputRule, Mark, Node as Node$1, PasteRule, callOrReturn, canInsertNode, combineTransactionSteps, escapeForRegEx, findChildren, findChildrenInRange, findParentNodeClosestToPos, getAttributes, getChangedRanges, getExtensionField, getMarksBetween, getText, getTextSerializersFromSchema, isNodeEmpty, isNodeSelection, markInputRule, markPasteRule, mergeAttributes, nodeInputRule, textInputRule, textblockTypeInputRule, wrappingInputRule } from "@tiptap/core";
4
4
  import { NodeSelection, Plugin, PluginKey, Selection, TextSelection } from "@tiptap/pm/state";
5
5
  import { Decoration, DecorationSet } from "@tiptap/pm/view";
6
6
  import { DOMParser, DOMSerializer, Fragment, Node as Node$2, Slice } from "@tiptap/pm/model";
@@ -477,13 +477,180 @@ var LoomaFigcaption = Node$1.create({
477
477
  //#endregion
478
478
  //#region src/editor/extensions/footnote.ts
479
479
  var labelFrom = (href, prefix) => href?.startsWith(`#${prefix}`) ? href.slice(prefix.length + 1) : null;
480
+ /** A footnote label as Looma Markdown reads it in `[^label]`: no whitespace and no `]`. */
481
+ var LOOMA_FOOTNOTE_LABEL = /^[^\s\]]+$/;
482
+ var REFERENCE = "loomaFootnoteReference";
483
+ var NOTES = "loomaFootnotes";
484
+ var NOTE = "loomaFootnote";
485
+ var isLabel = (label) => LOOMA_FOOTNOTE_LABEL.test(label);
486
+ /** Every label a reference or note in `doc` uses. */
487
+ function usedLabels(doc) {
488
+ const labels = /* @__PURE__ */ new Set();
489
+ doc.descendants((node) => {
490
+ if (node.type.name === REFERENCE || node.type.name === NOTE) labels.add(String(node.attrs.label));
491
+ });
492
+ return labels;
493
+ }
494
+ /**
495
+ * The first note for `label` in `doc` and its position.
496
+ * @failure Returns null when no note has that label.
497
+ */
498
+ function findLoomaFootnote(doc, label) {
499
+ let found = null;
500
+ doc.descendants((node, position) => {
501
+ if (found) return false;
502
+ if (node.type.name === NOTE && node.attrs.label === label) {
503
+ found = {
504
+ node,
505
+ position
506
+ };
507
+ return false;
508
+ }
509
+ });
510
+ return found;
511
+ }
512
+ function mapReferences(node, change) {
513
+ if (node.type.name === REFERENCE) return change(node);
514
+ if (node.isLeaf) return node;
515
+ const children = [];
516
+ node.content.forEach((child) => children.push(mapReferences(child, change)));
517
+ return node.copy(Fragment.from(children));
518
+ }
519
+ function noteAt(doc, position) {
520
+ const $position = doc.resolve(position);
521
+ for (let depth = $position.depth; depth > 0; depth--) {
522
+ const node = $position.node(depth);
523
+ if (node.type.name === NOTE) return {
524
+ node,
525
+ start: $position.before(depth)
526
+ };
527
+ }
528
+ return null;
529
+ }
530
+ /**
531
+ * Keeps footnotes on Looma Markdown's rule, so the saved HTML is the HTML Looma Markdown renders for
532
+ * the same document. Notes are numbered by first reference, in the text and then in the notes, and
533
+ * every reference carries its note's number and its place among that note's references. The notes
534
+ * live in one `loomaFootnotes` section at the end of the document, in number order. A note no
535
+ * reference points to leaves the document; a reference with no note gets one back, the note it last
536
+ * had in this editor (so cutting a reference and pasting it keeps its note) or an empty one.
537
+ * @invariant Changing nothing but text inside a note never rebuilds the section.
538
+ */
539
+ function footnoteTransaction(state, known) {
540
+ const { doc, schema } = state;
541
+ const sections = [];
542
+ const notes = /* @__PURE__ */ new Map();
543
+ const body = [];
544
+ doc.descendants((node, position) => {
545
+ if (node.type.name === NOTES) {
546
+ sections.push({
547
+ node,
548
+ position
549
+ });
550
+ node.forEach((note) => {
551
+ if (!notes.has(note.attrs.label)) notes.set(note.attrs.label, note);
552
+ });
553
+ return false;
554
+ }
555
+ if (node.type.name === REFERENCE) body.push({
556
+ node,
557
+ position
558
+ });
559
+ });
560
+ if (!sections.length && !body.length) return null;
561
+ const order = [];
562
+ const counts = /* @__PURE__ */ new Map();
563
+ const numbered = (reference) => {
564
+ const label = String(reference.attrs.label);
565
+ if (!counts.has(label)) order.push(label);
566
+ counts.set(label, (counts.get(label) ?? 0) + 1);
567
+ const attrs = {
568
+ ...reference.attrs,
569
+ number: order.indexOf(label) + 1,
570
+ occurrence: counts.get(label)
571
+ };
572
+ return attrs.number === reference.attrs.number && attrs.occurrence === reference.attrs.occurrence ? reference : reference.type.create(attrs, null, reference.marks);
573
+ };
574
+ const transaction = state.tr;
575
+ for (const { node, position } of body) {
576
+ const next = numbered(node);
577
+ if (next !== node) transaction.setNodeMarkup(position, void 0, next.attrs);
578
+ }
579
+ const built = [];
580
+ for (let index = 0; index < order.length; index++) {
581
+ const label = order[index];
582
+ const note = notes.get(label) ?? known.get(label) ?? schema.nodes[NOTE].create({ label }, schema.nodes.paragraph.create());
583
+ built.push(mapReferences(note, numbered));
584
+ }
585
+ const wanted = built.map((note) => note.attrs.count === counts.get(note.attrs.label) ? note : note.type.create({
586
+ ...note.attrs,
587
+ count: counts.get(note.attrs.label)
588
+ }, note.content));
589
+ for (const note of [...notes.values(), ...wanted]) known.set(note.attrs.label, note);
590
+ const section = wanted.length ? schema.nodes[NOTES].create(null, wanted) : null;
591
+ const last = doc.lastChild?.type.name === NOTES ? {
592
+ node: doc.lastChild,
593
+ position: doc.content.size - doc.lastChild.nodeSize
594
+ } : null;
595
+ if (!(section && last && sections.length === 1 && last.node.eq(section)) && (sections.length || section)) {
596
+ const { selection } = state;
597
+ const inside = noteAt(doc, selection.from);
598
+ const keep = inside && selection.to <= inside.start + inside.node.nodeSize ? {
599
+ label: inside.node.attrs.label,
600
+ start: inside.start
601
+ } : null;
602
+ for (const { node, position } of [...sections].reverse()) if (node !== last?.node) transaction.delete(position, position + node.nodeSize);
603
+ if (last) {
604
+ const position = transaction.mapping.map(last.position);
605
+ if (section) transaction.replaceWith(position, position + last.node.nodeSize, section);
606
+ else transaction.delete(position, position + last.node.nodeSize);
607
+ } else if (section) transaction.insert(transaction.doc.content.size, section);
608
+ const moved = keep && findLoomaFootnote(transaction.doc, keep.label);
609
+ if (keep && moved) {
610
+ const json = selection.toJSON();
611
+ json.anchor += moved.position - keep.start;
612
+ if (json.head !== void 0) json.head += moved.position - keep.start;
613
+ try {
614
+ transaction.setSelection(Selection.fromJSON(transaction.doc, json));
615
+ } catch {}
616
+ }
617
+ }
618
+ return transaction.docChanged ? transaction : null;
619
+ }
620
+ function appendBackLinks(item, label, number, count) {
621
+ const document = item.ownerDocument;
622
+ let paragraph = item.lastElementChild;
623
+ if (paragraph?.nodeName !== "P") item.append(paragraph = document.createElement("p"));
624
+ for (let occurrence = 1; occurrence <= count; occurrence++) {
625
+ const link = document.createElement("a");
626
+ link.setAttribute("href", `#fnref:${label}${occurrence > 1 ? `:${occurrence}` : ""}`);
627
+ link.setAttribute("data-footnote-backref", "");
628
+ link.setAttribute("aria-label", `Back to reference ${number}${occurrence > 1 ? `-${occurrence}` : ""}`);
629
+ link.append("↩");
630
+ if (occurrence > 1) link.append(Object.assign(document.createElement("sup"), { textContent: String(occurrence) }));
631
+ paragraph.append(" ", link);
632
+ }
633
+ }
634
+ var FootnoteSerializer = class extends DOMSerializer {
635
+ serializeFragment(fragment, options = {}, target) {
636
+ const dom = super.serializeFragment(fragment, options, target);
637
+ fragment.forEach((node, _offset, index) => {
638
+ if (node.type.name === NOTE) appendBackLinks(dom.childNodes[index], String(node.attrs.label), index + 1, Number(node.attrs.count) || 1);
639
+ });
640
+ return dom;
641
+ }
642
+ };
643
+ function installSerializer(schema) {
644
+ if (!(schema.cached.domSerializer instanceof FootnoteSerializer)) schema.cached.domSerializer = new FootnoteSerializer(DOMSerializer.nodesFromSchema(schema), DOMSerializer.marksFromSchema(schema));
645
+ }
480
646
  /**
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.
647
+ * A footnote reference, `[^label]`, saved as `<sup><a data-footnote-ref>`. Typing or pasting
648
+ * `[^label]` makes one; the note it needs is created with it.
649
+ * @invariant `label` names its note. `number` and `occurrence`, the note's number and this
650
+ * reference's place among the note's references, follow the document's order on every change.
484
651
  */
485
652
  var LoomaFootnoteReference = Node$1.create({
486
- name: "loomaFootnoteReference",
653
+ name: REFERENCE,
487
654
  group: "inline",
488
655
  inline: true,
489
656
  atom: true,
@@ -531,16 +698,59 @@ var LoomaFootnoteReference = Node$1.create({
531
698
  },
532
699
  renderText({ node }) {
533
700
  return `[^${node.attrs.label}]`;
701
+ },
702
+ addInputRules() {
703
+ return [new InputRule({
704
+ find: /\[\^([^\s\]]+)\]$/,
705
+ handler: ({ state, range, match }) => {
706
+ state.tr.replaceWith(range.from, range.to, this.type.create({ label: match[1] }));
707
+ }
708
+ })];
709
+ },
710
+ addPasteRules() {
711
+ return [new PasteRule({
712
+ find: /\[\^([^\s\]]+)\]/g,
713
+ handler: ({ state, range, match }) => {
714
+ state.tr.replaceWith(range.from, range.to, this.type.create({ label: match[1] }));
715
+ }
716
+ })];
717
+ },
718
+ addNodeView() {
719
+ return ({ node, editor }) => {
720
+ const dom = document.createElement("sup");
721
+ dom.dataset.loomaFootnoteRef = "";
722
+ dom.contentEditable = "false";
723
+ dom.setAttribute("role", "button");
724
+ dom.setAttribute("aria-haspopup", "dialog");
725
+ dom.setAttribute("aria-disabled", String(!editor.isEditable));
726
+ dom.tabIndex = 0;
727
+ const number = dom.appendChild(document.createElement("a"));
728
+ const update = (next) => {
729
+ if (next.type !== node.type) return false;
730
+ const shown = String(next.attrs.number ?? next.attrs.label);
731
+ dom.dataset.label = String(next.attrs.label);
732
+ if (number.textContent !== shown) number.textContent = shown;
733
+ dom.setAttribute("aria-label", `Footnote ${shown}`);
734
+ return true;
735
+ };
736
+ update(node);
737
+ return {
738
+ dom,
739
+ update,
740
+ stopEvent: (event) => event.type === "click" || event.type === "keydown"
741
+ };
742
+ };
534
743
  }
535
744
  });
536
745
  /**
537
746
  * 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.
747
+ * @invariant It holds only footnotes and stays the document's last block, one section in number order.
748
+ * @ownership It owns footnote numbering and the back-links in serialized HTML for the whole editor.
539
749
  */
540
750
  var LoomaFootnotes = Node$1.create({
541
- name: "loomaFootnotes",
751
+ name: NOTES,
542
752
  group: "block",
543
- content: "loomaFootnote+",
753
+ content: `${NOTE}+`,
544
754
  isolating: true,
545
755
  parseHTML() {
546
756
  return [{
@@ -565,29 +775,121 @@ var LoomaFootnotes = Node$1.create({
565
775
  ],
566
776
  ["ol", 0]
567
777
  ];
778
+ },
779
+ onBeforeCreate() {
780
+ installSerializer(this.editor.schema);
781
+ },
782
+ onCreate() {
783
+ const transaction = footnoteTransaction(this.editor.state, this.storage.known);
784
+ if (transaction) this.editor.view.dispatch(transaction.setMeta("addToHistory", false));
785
+ },
786
+ addStorage: () => ({ known: /* @__PURE__ */ new Map() }),
787
+ addProseMirrorPlugins() {
788
+ return [new Plugin({
789
+ appendTransaction: (transactions, _oldState, state) => transactions.some((transaction) => transaction.docChanged) ? footnoteTransaction(state, this.storage.known) : null,
790
+ props: { transformPasted: (slice) => {
791
+ const kept = [];
792
+ slice.content.forEach((node) => {
793
+ if (node.type.name === NOTES) node.forEach((note) => {
794
+ this.storage.known.set(note.attrs.label, note);
795
+ });
796
+ else kept.push(node);
797
+ });
798
+ if (!kept.length || kept.length === slice.content.childCount) return slice;
799
+ const content = Fragment.from(kept);
800
+ const open = Slice.maxOpen(content);
801
+ return new Slice(content, kept[0] === slice.content.firstChild ? slice.openStart : open.openStart, kept.at(-1) === slice.content.lastChild ? slice.openEnd : open.openEnd);
802
+ } }
803
+ })];
804
+ },
805
+ addCommands() {
806
+ return {
807
+ insertLoomaFootnote: (label) => ({ state, tr, dispatch }) => {
808
+ const name = label ?? (() => {
809
+ const used = usedLabels(state.doc);
810
+ let next = 1;
811
+ while (used.has(String(next))) next++;
812
+ return String(next);
813
+ })();
814
+ const { $from, $to } = state.selection;
815
+ if (!isLabel(name) || !$from.parent.canReplaceWith($from.index(), $to.index(), state.schema.nodes[REFERENCE])) return false;
816
+ if (!dispatch) return true;
817
+ tr.replaceSelectionWith(state.schema.nodes[REFERENCE].create({ label: name }));
818
+ if (!findLoomaFootnote(tr.doc, name)) {
819
+ const note = state.schema.nodes[NOTE].create({ label: name }, state.schema.nodes.paragraph.create());
820
+ if (tr.doc.lastChild?.type.name === NOTES) tr.insert(tr.doc.content.size - 1, note);
821
+ else tr.insert(tr.doc.content.size, state.schema.nodes[NOTES].create(null, note));
822
+ tr.setSelection(TextSelection.create(tr.doc, tr.doc.content.size - 3));
823
+ }
824
+ tr.scrollIntoView();
825
+ return true;
826
+ },
827
+ renameLoomaFootnote: (from, to) => ({ state, tr, dispatch }) => {
828
+ if (!isLabel(to) || usedLabels(state.doc).has(to) || !findLoomaFootnote(state.doc, from)) return false;
829
+ if (dispatch) state.doc.descendants((node, position) => {
830
+ if ((node.type.name === REFERENCE || node.type.name === NOTE) && node.attrs.label === from) tr.setNodeMarkup(position, void 0, {
831
+ ...node.attrs,
832
+ label: to
833
+ });
834
+ });
835
+ return true;
836
+ },
837
+ goToLoomaFootnote: () => ({ state, tr, dispatch }) => {
838
+ const { selection } = state;
839
+ const reference = selection instanceof NodeSelection ? selection.node : selection.empty ? selection.$from.nodeBefore : null;
840
+ if (reference?.type.name === REFERENCE) {
841
+ const note = findLoomaFootnote(state.doc, reference.attrs.label);
842
+ if (!note) return false;
843
+ if (dispatch) tr.setSelection(Selection.near(state.doc.resolve(note.position + note.node.nodeSize - 1), -1)).scrollIntoView();
844
+ return true;
845
+ }
846
+ const note = noteAt(state.doc, selection.from);
847
+ let first = -1;
848
+ if (note) state.doc.descendants((node, position) => {
849
+ if (first >= 0) return false;
850
+ if (node.type.name === REFERENCE && node.attrs.label === note.node.attrs.label) first = position;
851
+ });
852
+ if (first < 0) return false;
853
+ if (dispatch) tr.setSelection(TextSelection.create(state.doc, first + 1)).scrollIntoView();
854
+ return true;
855
+ }
856
+ };
857
+ },
858
+ addKeyboardShortcuts() {
859
+ return {
860
+ "Mod-Alt-f": () => this.editor.commands.insertLoomaFootnote(),
861
+ "Mod-Alt-n": () => this.editor.commands.goToLoomaFootnote()
862
+ };
568
863
  }
569
864
  });
570
865
  /**
571
866
  * 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.
867
+ * @invariant It exists only inside `LoomaFootnotes`. `count` is how many references point to it. Its
868
+ * back-links are presentation: the editor reads past them and writes them only into saved HTML.
574
869
  */
575
870
  var LoomaFootnote = Node$1.create({
576
- name: "loomaFootnote",
871
+ name: NOTE,
577
872
  content: "block+",
578
873
  defining: true,
579
874
  addAttributes() {
580
- return { label: {
581
- default: "",
582
- parseHTML: (element) => labelFrom(`#${element.id}`, "fn:") ?? "",
583
- renderHTML: () => ({})
584
- } };
875
+ return {
876
+ label: {
877
+ default: "",
878
+ parseHTML: (element) => labelFrom(`#${element.id}`, "fn:") ?? "",
879
+ renderHTML: () => ({})
880
+ },
881
+ count: {
882
+ default: 1,
883
+ parseHTML: (element) => element.querySelectorAll("a[data-footnote-backref]").length || 1,
884
+ renderHTML: () => ({})
885
+ }
886
+ };
585
887
  },
586
888
  parseHTML() {
587
889
  return [{
588
890
  tag: "li",
589
891
  priority: 1100,
590
- context: "loomaFootnotes/"
892
+ context: `${NOTES}/`
591
893
  }, {
592
894
  tag: "a[data-footnote-backref]",
593
895
  priority: 1100,
@@ -10392,7 +10694,8 @@ function looksLikeMarkdown(value) {
10392
10694
  "image",
10393
10695
  "strong_open",
10394
10696
  "looma_chip",
10395
- "looma_mention"
10697
+ "looma_mention",
10698
+ "looma_footnote_ref"
10396
10699
  ].includes(child.type)));
10397
10700
  });
10398
10701
  }
@@ -10476,8 +10779,11 @@ var LoomaSmartPaste = Extension.create({
10476
10779
  const plainText = event.clipboardData.getData("text/plain");
10477
10780
  if (!plainText.trim()) return false;
10478
10781
  if (hasRichClipboardStructure(event.clipboardData.getData("text/html"))) return false;
10479
- const slice = documentSlice(view.state.schema, plainText);
10782
+ let slice = documentSlice(view.state.schema, plainText);
10480
10783
  if (!slice) return false;
10784
+ view.someProp("transformPasted", (transform) => {
10785
+ slice = transform(slice, view, true);
10786
+ });
10481
10787
  if (slice.size === 0) return true;
10482
10788
  const transaction = view.state.tr.replaceSelection(slice).scrollIntoView().setMeta("paste", true).setMeta("uiEvent", "paste");
10483
10789
  view.dispatch(transaction);
@@ -10849,6 +11155,26 @@ var TableHeader = Node$1.create({
10849
11155
  });
10850
11156
  //#endregion
10851
11157
  //#region src/editor/extensions/table-formatting.ts
11158
+ /** Table chrome lives outside the horizontal scroller, within the table's document block. */
11159
+ var LoomaTableView = class extends TableView {
11160
+ toolbarHost;
11161
+ constructor(node, cellMinWidth) {
11162
+ super(node, cellMinWidth);
11163
+ const scroller = this.dom;
11164
+ this.dom = document.createElement("div");
11165
+ this.dom.dataset.loomaTableBlock = "";
11166
+ this.toolbarHost = document.createElement("div");
11167
+ this.toolbarHost.dataset.loomaTableToolbarHost = "";
11168
+ this.toolbarHost.contentEditable = "false";
11169
+ this.dom.append(scroller, this.toolbarHost);
11170
+ }
11171
+ ignoreMutation(mutation) {
11172
+ return mutation.type !== "selection" && this.toolbarHost.contains(mutation.target) || super.ignoreMutation(mutation);
11173
+ }
11174
+ stopEvent(event) {
11175
+ return event.target instanceof globalThis.Node && this.toolbarHost.contains(event.target);
11176
+ }
11177
+ };
10852
11178
  /**
10853
11179
  * Looma's table behavior policy: resizable columns, stable minimum cell width,
10854
11180
  * and no independently resizable trailing column. The narrow handle is also a
@@ -10859,6 +11185,7 @@ var TableHeader = Node$1.create({
10859
11185
  */
10860
11186
  var LoomaTable = Table.configure({
10861
11187
  resizable: true,
11188
+ View: LoomaTableView,
10862
11189
  handleWidth: 3,
10863
11190
  cellMinWidth: 112,
10864
11191
  lastColumnResizable: false
@@ -11785,6 +12112,20 @@ function getDefaultSlashCommands(onOpenImagePicker, onOpenChipEditor, onOpenLink
11785
12112
  editor.chain().focus().deleteRange(range).insertLoomaTableOfContents().run();
11786
12113
  }
11787
12114
  },
12115
+ {
12116
+ title: "Footnote",
12117
+ description: "Numbered note at the end of the document",
12118
+ icon: "superscript",
12119
+ keywords: [
12120
+ "footnote",
12121
+ "note",
12122
+ "reference",
12123
+ "citation"
12124
+ ],
12125
+ command: ({ editor, range }) => {
12126
+ editor.chain().focus().deleteRange(range).insertLoomaFootnote().run();
12127
+ }
12128
+ },
11788
12129
  {
11789
12130
  title: "Expand",
11790
12131
  description: "Collapsible section with a summary",
@@ -11852,6 +12193,7 @@ function getDefaultSlashCommands(onOpenImagePicker, onOpenChipEditor, onOpenLink
11852
12193
  "Image",
11853
12194
  "Table",
11854
12195
  "Table of contents",
12196
+ "Footnote",
11855
12197
  "Expand"
11856
12198
  ].includes(command.title) ? "Insertions" : "Basic blocks",
11857
12199
  isAvailable: (context) => canRunDefaultCommand(command.title, context)
@@ -11887,6 +12229,7 @@ function canRunDefaultCommand(title, { editor, range }) {
11887
12229
  withHeaderRow: true
11888
12230
  }).run();
11889
12231
  case "Table of contents": return chain.insertLoomaTableOfContents().run();
12232
+ case "Footnote": return chain.insertLoomaFootnote().run();
11890
12233
  case "Expand": return chain.insertLoomaExpand().run();
11891
12234
  case "Divider": return chain.setHorizontalRule().run();
11892
12235
  default: return chain.run();
@@ -12093,4 +12436,4 @@ function createLoomaSlashCommandExtension(options = {}) {
12093
12436
  });
12094
12437
  }
12095
12438
  //#endregion
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 };
12439
+ export { LOOMA_ACTIVE_BLOCK_BLUR_GRACE_MS as $, filterLoomaMentionItems as A, LOOMA_FOOTNOTE_LABEL as B, loomaMarkdownToHtml as C, createLoomaMentionExtension as D, LoomaMentionSuggestionPluginKey as E, LoomaTableOfContents as F, LoomaFigcaption as G, LoomaFootnoteReference as H, getTableOfContentsEntries as I, revealLoomaExpandAt as J, LoomaFigure as K, normalizeTableOfContentsConfig as L, LoomaImage as M, normalizeImageDimension as N, DEFAULT_MENTION_RESULT_LIMIT as O, normalizeImagePlacement as P, LoomaCallout as Q, LoomaTab as R, loomaMarkdownFromDoc as S, siteRelativeHref as T, LoomaFootnotes as U, LoomaFootnote as V, findLoomaFootnote as W, LoomaChip as X, LOOMA_CHIP_COLORS as Y, normalizeLoomaChipColor as Z, getActiveTableCellAlignment as _, getActiveTableUiState as a, setActiveTableCellBackground as b, insertTableAtRange as c, LoomaTableKit as d, LOOMA_ACTIVE_BLOCK_CLASS 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, LoomaActiveBlock as nt, handleTableAction as o, getLoomaTableExtensions as p, LoomaExpand as q, filterLoomaSlashCommands as r, LoomaActiveBlockPluginKey as rt, handleTableOverlayAction as s, LoomaSlashCommand as t, LOOMA_ACTIVE_BLOCK_TYPING_IDLE_MS as tt, shouldShowTextFormattingToolbar as u, getActiveTableCellBackground as v, createSiteRelativeLinks as w, LoomaSmartPaste as x, setActiveTableCellAlignment as y, LoomaTabs as z };
@@ -408,7 +408,7 @@ function toBareNode(node, path, context) {
408
408
  value: string(attrs.label)
409
409
  };
410
410
  case "loomaFootnotes": return block("container", [], { subType: "footnotes" });
411
- case "loomaFootnote": return block("container", ["label"], {
411
+ case "loomaFootnote": return block("container", ["label", "count"], {
412
412
  subType: "footnote",
413
413
  title: title(string(attrs.label))
414
414
  });
@@ -1,18 +1,51 @@
1
1
  import { Node } from "@tiptap/core";
2
+ import { type Node as ProseMirrorNode } from "@tiptap/pm/model";
3
+ /** A footnote label as Looma Markdown reads it in `[^label]`: no whitespace and no `]`. */
4
+ export declare const LOOMA_FOOTNOTE_LABEL: RegExp;
2
5
  /**
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
+ * The first note for `label` in `doc` and its position.
7
+ * @failure Returns null when no note has that label.
8
+ */
9
+ export declare function findLoomaFootnote(doc: ProseMirrorNode, label: string): {
10
+ node: ProseMirrorNode;
11
+ position: number;
12
+ } | null;
13
+ /**
14
+ * A footnote reference, `[^label]`, saved as `<sup><a data-footnote-ref>`. Typing or pasting
15
+ * `[^label]` makes one; the note it needs is created with it.
16
+ * @invariant `label` names its note. `number` and `occurrence`, the note's number and this
17
+ * reference's place among the note's references, follow the document's order on every change.
6
18
  */
7
19
  export declare const LoomaFootnoteReference: Node<any, any>;
20
+ declare module "@tiptap/core" {
21
+ interface Commands<ReturnType> {
22
+ loomaFootnotes: {
23
+ /**
24
+ * Inserts a reference at the selection. A new label (the next free number when none is given)
25
+ * also gets an empty note, and the caret moves into it.
26
+ */
27
+ insertLoomaFootnote: (label?: string) => ReturnType;
28
+ /** Gives a footnote's note and every reference to it a new, unused label. */
29
+ renameLoomaFootnote: (from: string, to: string) => ReturnType;
30
+ /**
31
+ * From a reference (selected, or just before the caret) to the end of its note; from inside a
32
+ * note to just after its first reference.
33
+ */
34
+ goToLoomaFootnote: () => ReturnType;
35
+ };
36
+ }
37
+ }
8
38
  /**
9
39
  * 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.
40
+ * @invariant It holds only footnotes and stays the document's last block, one section in number order.
41
+ * @ownership It owns footnote numbering and the back-links in serialized HTML for the whole editor.
11
42
  */
12
- export declare const LoomaFootnotes: Node<any, any>;
43
+ export declare const LoomaFootnotes: Node<any, {
44
+ known: Map<string, ProseMirrorNode>;
45
+ }>;
13
46
  /**
14
47
  * 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.
48
+ * @invariant It exists only inside `LoomaFootnotes`. `count` is how many references point to it. Its
49
+ * back-links are presentation: the editor reads past them and writes them only into saved HTML.
17
50
  */
18
51
  export declare const LoomaFootnote: Node<any, any>;
@@ -11,7 +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
+ export { findLoomaFootnote, LOOMA_FOOTNOTE_LABEL, LoomaFootnote, LoomaFootnoteReference, LoomaFootnotes } from "./footnote.js";
15
15
  export { LoomaTab, LoomaTabs } from "./tabs.js";
16
16
  export { LoomaTableOfContents, getTableOfContentsEntries, normalizeTableOfContentsConfig, type LoomaTableOfContentsConfig, type LoomaTableOfContentsEntry, type LoomaTableOfContentsFormat, } from "./table-of-contents.js";
17
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 { $ 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";
2
+ import { $ as LOOMA_ACTIVE_BLOCK_BLUR_GRACE_MS, A as filterLoomaMentionItems, B as LOOMA_FOOTNOTE_LABEL, C as loomaMarkdownToHtml, D as createLoomaMentionExtension, E as LoomaMentionSuggestionPluginKey, F as LoomaTableOfContents, G as LoomaFigcaption, H as LoomaFootnoteReference, I as getTableOfContentsEntries, J as revealLoomaExpandAt, K as LoomaFigure, L as normalizeTableOfContentsConfig, M as LoomaImage, N as normalizeImageDimension, O as DEFAULT_MENTION_RESULT_LIMIT, P as normalizeImagePlacement, Q as LoomaCallout, R as LoomaTab, S as loomaMarkdownFromDoc, T as siteRelativeHref, U as LoomaFootnotes, V as LoomaFootnote, W as findLoomaFootnote, X as LoomaChip, Y as LOOMA_CHIP_COLORS, Z as normalizeLoomaChipColor, _ as getActiveTableCellAlignment, a as getActiveTableUiState, b as setActiveTableCellBackground, c as insertTableAtRange, d as LoomaTableKit, et as LOOMA_ACTIVE_BLOCK_CLASS, 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, nt as LoomaActiveBlock, o as handleTableAction, p as getLoomaTableExtensions, q as LoomaExpand, r as filterLoomaSlashCommands, rt as LoomaActiveBlockPluginKey, s as handleTableOverlayAction, t as LoomaSlashCommand, tt as LOOMA_ACTIVE_BLOCK_TYPING_IDLE_MS, 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, 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 };
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, LOOMA_FOOTNOTE_LABEL, 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, findLoomaFootnote, 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 };
@@ -0,0 +1,46 @@
1
+ import type { Editor } from "@tiptap/core";
2
+ /** The footnote editor's state: the edited reference's label and its note's text. */
3
+ export interface LoomaFootnoteEditingState {
4
+ readonly open: boolean;
5
+ readonly label: string;
6
+ /** The note's text, its blocks on separate lines. */
7
+ readonly note: string;
8
+ /** Whether `note` can be edited as text: the note is one paragraph of unformatted text. */
9
+ readonly editable: boolean;
10
+ /** Why the last label offered was not used, or empty. */
11
+ readonly error: string;
12
+ }
13
+ export declare const LOOMA_FOOTNOTE_EDITING_IDLE: LoomaFootnoteEditingState;
14
+ /**
15
+ * The footnote editor as a state machine: it opens on one reference, writes the note's text as the
16
+ * author types it, and renames the footnote everywhere when a new label is committed.
17
+ *
18
+ * @ownership The host owns presentation: the popover, its anchor, and focus. This owns which footnote
19
+ * is edited and the document changes it makes.
20
+ * @failure A change after the footnote was deleted closes the editor and writes nothing. A label with
21
+ * a space or `]`, or one another footnote uses, is not applied; `error` says why.
22
+ */
23
+ export interface LoomaFootnoteEditing {
24
+ readonly state: LoomaFootnoteEditingState;
25
+ /** The edited reference's element, for anchoring the editor, or null while it is closed. */
26
+ readonly element: HTMLElement | null;
27
+ /** Calls `listener` after every state change; returns the unsubscribe function. */
28
+ subscribe(listener: () => void): () => void;
29
+ /**
30
+ * Opens on the reference at `position`.
31
+ * @failure Returns false, and stays as it was, when the editor is read-only or no reference is there.
32
+ */
33
+ open(position: number): boolean;
34
+ /** Replaces the note's text; ignored unless `state.editable`. */
35
+ setNote(note: string): void;
36
+ rename(label: string): void;
37
+ /** Closes the editor and moves the caret to the end of the note. */
38
+ goToNote(): void;
39
+ close(): void;
40
+ }
41
+ export interface LoomaFootnoteEditingOptions {
42
+ editor: Editor;
43
+ /** Read on each opening, so a host can change it while the editor runs. */
44
+ isEditable: () => boolean;
45
+ }
46
+ export declare function createFootnoteEditing(options: LoomaFootnoteEditingOptions): LoomaFootnoteEditing;
package/editor/icons.d.ts CHANGED
@@ -65,6 +65,7 @@ export declare const LOOMA_ICONS: {
65
65
  settings: IconNode;
66
66
  split: IconNode;
67
67
  strikethrough: IconNode;
68
+ superscript: IconNode;
68
69
  tag: IconNode;
69
70
  table: IconNode;
70
71
  trash: IconNode;