@threadlabs/looma 0.28.15 → 0.28.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1840,6 +1840,34 @@ function formatEditorShortcut(keys, apple) {
1840
1840
  ].filter(Boolean).join("+");
1841
1841
  }
1842
1842
  var isApplePlatform = () => typeof navigator !== "undefined" && /mac|iphone|ipad|ipod/i.test(navigator.userAgentData?.platform || navigator.platform || navigator.userAgent);
1843
+ /**
1844
+ * The anchor a heading's text derives: NFC, lowercase, keeping letters, numbers and combining
1845
+ * marks, with every other run of characters one hyphen.
1846
+ * @failure Text with no letters or numbers, such as only emoji, derives `heading`.
1847
+ */
1848
+ function loomaHeadingSlug(text) {
1849
+ return text.normalize("NFC").toLowerCase().replace(/[^\p{L}\p{N}\p{M}]+/gu, "-").replace(/^-+|-+$/g, "") || "heading";
1850
+ }
1851
+ /**
1852
+ * Gives every heading of a document its anchor, in document order.
1853
+ * @contract A custom ID is kept unless an earlier heading already has it. Any other heading takes
1854
+ * its slug, or the slug's first free suffix (`-1`, `-2`…), never one a later custom ID reserves.
1855
+ */
1856
+ function assignLoomaHeadingIds(headings) {
1857
+ const reserved = new Set(headings.map(({ id }) => id).filter((id) => Boolean(id)));
1858
+ const used = /* @__PURE__ */ new Set();
1859
+ return headings.map(({ text, id: custom }) => {
1860
+ let id = custom || loomaHeadingSlug(text);
1861
+ if (custom ? used.has(id) : used.has(id) || reserved.has(id)) {
1862
+ const stem = id;
1863
+ let suffix = 1;
1864
+ while (used.has(`${stem}-${suffix}`) || reserved.has(`${stem}-${suffix}`)) suffix++;
1865
+ id = `${stem}-${suffix}`;
1866
+ }
1867
+ used.add(id);
1868
+ return id;
1869
+ });
1870
+ }
1843
1871
  //#endregion
1844
1872
  //#region src/editor/extensions/expand.ts
1845
1873
  /**
@@ -2049,7 +2077,17 @@ function tableOfContentsTree(entries) {
2049
2077
  }
2050
2078
  return roots;
2051
2079
  }
2052
- function anchorTransaction(state) {
2080
+ var derivedFrom = (id, text) => {
2081
+ const slug = loomaHeadingSlug(text);
2082
+ return id === slug || id.startsWith(`${slug}-`) && /^\d+$/.test(id.slice(slug.length + 1));
2083
+ };
2084
+ /**
2085
+ * Keeps heading anchors on Looma Markdown's rule. An anchor derived from its heading's text
2086
+ * (as it read before this change) follows the text; any other, including `looma-heading-N` anchors
2087
+ * saved before this rule, is a custom anchor and stays, so links to it keep working. With
2088
+ * a table of contents every heading gets an anchor; without one, only headings that have one.
2089
+ */
2090
+ function anchorTransaction(state, previous) {
2053
2091
  let hasToc = false;
2054
2092
  const headings = [];
2055
2093
  state.doc.descendants((node, position) => {
@@ -2059,24 +2097,28 @@ function anchorTransaction(state) {
2059
2097
  position
2060
2098
  });
2061
2099
  });
2062
- if (!hasToc) return null;
2063
- const reserved = new Set(headings.map(({ node }) => String(node.attrs.id ?? "")).filter(Boolean));
2064
- const used = /* @__PURE__ */ new Set();
2100
+ if (!headings.length) return null;
2101
+ const before = /* @__PURE__ */ new Map();
2102
+ previous?.descendants((node) => {
2103
+ if (node.type.name === "heading" && typeof node.attrs.id === "string") before.set(node.attrs.id, node.textContent.trim());
2104
+ });
2105
+ const custom = (node) => {
2106
+ const id = node.attrs.id;
2107
+ if (typeof id !== "string" || !id || /\s/.test(id)) return void 0;
2108
+ return derivedFrom(id, before.get(id) ?? node.textContent.trim()) ? void 0 : id;
2109
+ };
2110
+ const ids = assignLoomaHeadingIds(headings.map(({ node }) => ({
2111
+ text: node.textContent.trim(),
2112
+ id: custom(node)
2113
+ })));
2065
2114
  const transaction = state.tr;
2066
- let sequence = 1;
2067
- for (const { node, position } of headings) {
2068
- let id = typeof node.attrs.id === "string" ? node.attrs.id : "";
2069
- if (!id || /\s/.test(id) || used.has(id)) {
2070
- do
2071
- id = `looma-heading-${sequence++}`;
2072
- while (reserved.has(id) || used.has(id));
2073
- transaction.setNodeMarkup(position, void 0, {
2074
- ...node.attrs,
2075
- id
2076
- });
2077
- }
2078
- used.add(id);
2079
- }
2115
+ headings.forEach(({ node, position }, index) => {
2116
+ if (node.attrs.id === ids[index] || !hasToc && !node.attrs.id) return;
2117
+ transaction.setNodeMarkup(position, void 0, {
2118
+ ...node.attrs,
2119
+ id: ids[index]
2120
+ });
2121
+ });
2080
2122
  return transaction.docChanged ? transaction : null;
2081
2123
  }
2082
2124
  var LoomaHeadingAnchors = Extension.create({
@@ -2092,7 +2134,7 @@ var LoomaHeadingAnchors = Extension.create({
2092
2134
  if (transaction) this.editor.view.dispatch(transaction.setMeta("addToHistory", false));
2093
2135
  },
2094
2136
  addProseMirrorPlugins() {
2095
- return [new Plugin({ appendTransaction: (transactions, _oldState, state) => transactions.some((transaction) => transaction.docChanged) ? anchorTransaction(state) : null })];
2137
+ return [new Plugin({ appendTransaction: (transactions, oldState, state) => transactions.some((transaction) => transaction.docChanged) ? anchorTransaction(state, oldState.doc) : null })];
2096
2138
  }
2097
2139
  });
2098
2140
  function contentsHtml(doc, config) {