autumnnote 1.2.0 → 1.3.0

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.
@@ -27,19 +27,29 @@ function debounce(fn, delay) {
27
27
  };
28
28
  }
29
29
  /**
30
- * Throttle a function call.
31
- * @param {Function} fn
32
- * @param {number} limit - milliseconds
33
- * @returns {Function}
30
+ * Create a wrapper that limits how often `fn` can be invoked while ensuring the last call in a burst is executed.
31
+ * @param {Function} fn - Function to be throttled.
32
+ * @param {number} limit - Time window in milliseconds during which at most one call is allowed.
33
+ * @returns {Function} A wrapper function that invokes `fn` at most once per `limit` milliseconds; calls preserve `this` and original arguments and schedule a trailing invocation for the final call in a burst.
34
34
  */
35
35
  function throttle(fn, limit) {
36
- let lastCall = 0;
36
+ let lastCall = -Infinity;
37
+ let trailingTimer = null;
37
38
  return function(...args) {
38
- const now = Date.now();
39
- if (now - lastCall >= limit) {
39
+ const now = performance.now();
40
+ const elapsed = now - lastCall;
41
+ if (elapsed >= limit) {
40
42
  lastCall = now;
43
+ clearTimeout(trailingTimer);
44
+ trailingTimer = null;
41
45
  return fn.apply(this, args);
42
46
  }
47
+ clearTimeout(trailingTimer);
48
+ trailingTimer = setTimeout(() => {
49
+ lastCall = performance.now();
50
+ trailingTimer = null;
51
+ fn.apply(this, args);
52
+ }, limit - elapsed);
43
53
  };
44
54
  }
45
55
  /**
@@ -118,12 +128,12 @@ function isPlainObject(val) {
118
128
  function rect2bnd(rect) {
119
129
  if (!rect) return null;
120
130
  return {
121
- top: Math.round(rect.top),
122
- left: Math.round(rect.left),
123
- width: Math.round(rect.width),
124
- height: Math.round(rect.height),
125
- bottom: Math.round(rect.bottom),
126
- right: Math.round(rect.right)
131
+ top: rect.top,
132
+ left: rect.left,
133
+ width: rect.width,
134
+ height: rect.height,
135
+ bottom: rect.bottom,
136
+ right: rect.right
127
137
  };
128
138
  }
129
139
  //#endregion
@@ -283,13 +293,16 @@ function nodeValue(node) {
283
293
  return isText(node) ? node.nodeValue : node.textContent || "";
284
294
  }
285
295
  /**
286
- * Returns true if a node is empty (no visible content).
287
- * @param {Node} node
288
- * @returns {boolean}
296
+ * Determine whether a DOM node contains no visible content.
297
+ *
298
+ * Text nodes are considered empty when their `nodeValue` is empty. Void elements (e.g., `img`, `br`, `input`) are considered non-empty. An element with exactly one `<br>` child is treated as empty. For other elements, emptiness means trimmed `textContent` is empty and there are no descendant `img`, `video`, `hr`, or `table` elements.
299
+ * @param {Node} node - Node to inspect for visible content.
300
+ * @returns {boolean} `true` if the node has no visible content, `false` otherwise.
289
301
  */
290
302
  function isEmpty(node) {
291
303
  if (isText(node)) return !node.nodeValue;
292
304
  if (isVoid(node)) return false;
305
+ if (node.childNodes.length === 1 && node.firstChild?.nodeName === "BR") return true;
293
306
  return !node.textContent.trim() && !node.querySelector("img, video, hr, table");
294
307
  }
295
308
  /**
@@ -701,9 +714,15 @@ function outdent() {
701
714
  execCommand("outdent");
702
715
  }
703
716
  /**
704
- * G.5 helper: splits a checklist at checkLi, converts it to a <p>,
705
- * and keeps items before/after as separate checklists.
706
- * @param {HTMLElement} checkLi
717
+ * Convert a checklist <li> into a paragraph and move any following items into a new checklist.
718
+ *
719
+ * Preserves inline markup from the converted item, strips zero-width space anchors,
720
+ * and replaces empty content with a non‑breaking space. If there are list items
721
+ * after the converted item they are moved into a new <ul class="an-checklist">
722
+ * inserted immediately after the original list. The original <li> is removed and
723
+ * the original list is removed if it becomes empty. Attempts to place the caret
724
+ * at the start of the newly created <p>.
725
+ * @param {HTMLElement} checkLi - The checklist `<li>` element to convert to a `<p>`.
707
726
  */
708
727
  function _checklistItemToP(checkLi) {
709
728
  const checkUl = checkLi.closest(".an-checklist");
@@ -712,7 +731,15 @@ function _checklistItemToP(checkLi) {
712
731
  const liIndex = allLis.indexOf(checkLi);
713
732
  const afterLis = allLis.slice(liIndex + 1);
714
733
  const p = document.createElement("p");
715
- p.textContent = Array.from(checkLi.childNodes).filter((n) => !(n.nodeType === 1 && n.tagName === "INPUT")).map((n) => n.textContent).join("").replace(/\u200B/g, "").trim() || "\xA0";
734
+ for (const child of checkLi.childNodes) {
735
+ if (child.nodeType === 1 && child.tagName === "INPUT") continue;
736
+ p.appendChild(child.cloneNode(true));
737
+ }
738
+ p.innerHTML = p.innerHTML.replace(/\u200B/g, "");
739
+ if (!p.hasChildNodes() || !p.textContent.trim()) {
740
+ p.innerHTML = "";
741
+ p.appendChild(document.createTextNode("\xA0"));
742
+ }
716
743
  if (afterLis.length > 0) {
717
744
  const newUl = document.createElement("ul");
718
745
  newUl.className = "an-checklist";
@@ -743,9 +770,12 @@ var insertUnorderedList = () => execCommand("insertUnorderedList");
743
770
  */
744
771
  var insertOrderedList = () => execCommand("insertOrderedList");
745
772
  /**
746
- * Applies a line-height value to every block-level element that intersects
747
- * the current selection.
748
- * @param {string} value - unitless multiplier, e.g. '1.5'
773
+ * Set the line-height on every block-level element that intersects the current selection.
774
+ *
775
+ * If the selection is collapsed, the nearest enclosing block element receives the style.
776
+ * For a non-collapsed selection, all unique block ancestors of text nodes that intersect the range are updated;
777
+ * if none are found, the nearest block ancestor of the range's common ancestor is updated.
778
+ * @param {string} value - Line-height value to apply; typically a unitless multiplier (for example, "1.5").
749
779
  */
750
780
  function lineHeight(value) {
751
781
  const sel = window.getSelection();
@@ -780,9 +810,9 @@ function lineHeight(value) {
780
810
  return;
781
811
  }
782
812
  const blocks = /* @__PURE__ */ new Set();
783
- const iter = document.createNodeIterator(range.commonAncestorContainer, NodeFilter.SHOW_TEXT, null);
813
+ const iter = document.createTreeWalker(range.commonAncestorContainer, NodeFilter.SHOW_TEXT, { acceptNode: (node) => range.intersectsNode(node) ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_SKIP });
784
814
  let textNode;
785
- while (textNode = iter.nextNode()) if (range.intersectsNode(textNode)) {
815
+ while (textNode = iter.nextNode()) {
786
816
  const block = nearestBlock(textNode);
787
817
  if (block) blocks.add(block);
788
818
  }
@@ -864,9 +894,18 @@ function isInlineCode() {
864
894
  return !!(code && !code.closest("pre"));
865
895
  }
866
896
  /**
867
- * Toggles a task-list at the cursor.
868
- * If inside a checklist <li>, converts it (and any other selected items) back to <p> elements.
869
- * Otherwise inserts a new <ul class="an-checklist"> with one item per selected line.
897
+ * Toggle a checklist at the current selection or caret.
898
+ *
899
+ * When the selection is inside an existing checklist `<ul class="an-checklist">`,
900
+ * converts the selected `<li>` items back into `<p>` paragraphs and places the caret
901
+ * at the start of the first converted paragraph. Otherwise creates a checklist:
902
+ * - If the selection is collapsed, converts the nearest block-level ancestor (or inserts
903
+ * a single checklist item at the editable root) into a checklist with one item containing
904
+ * that block's text and places the caret inside the new item.
905
+ * - If the selection is a range, converts each intersecting block element into one checklist
906
+ * item (preserving textual content) and places the caret at the end of the last item.
907
+ *
908
+ * Empty or whitespace-only selections do not create a checklist.
870
909
  */
871
910
  function toggleChecklist() {
872
911
  const sel = window.getSelection();
@@ -925,8 +964,9 @@ function toggleChecklist() {
925
964
  ul.appendChild(li);
926
965
  if (block && BLOCK_TAGS.has(block.tagName)) block.parentNode.replaceChild(ul, block);
927
966
  else {
928
- document.execCommand("insertHTML", false, ul.outerHTML);
929
- return;
967
+ const nativeRange = sel.getRangeAt(0);
968
+ nativeRange.deleteContents();
969
+ nativeRange.insertNode(ul);
930
970
  }
931
971
  const textNode = li.lastChild;
932
972
  const nr = document.createRange();
@@ -1614,6 +1654,7 @@ var en = {
1614
1654
  replaceAriaLabel: "Replace with",
1615
1655
  replaceBtn: "Replace",
1616
1656
  replaceAllBtn: "Replace All",
1657
+ noResults: "No results",
1617
1658
  close: "×"
1618
1659
  },
1619
1660
  shortcutsDialog: {
@@ -1966,6 +2007,7 @@ var locales = {
1966
2007
  replaceAriaLabel: "Thay thế bằng",
1967
2008
  replaceBtn: "Thay thế",
1968
2009
  replaceAllBtn: "Thay thế tất cả",
2010
+ noResults: "Không có kết quả",
1969
2011
  close: "×"
1970
2012
  },
1971
2013
  shortcutsDialog: {
@@ -2295,6 +2337,7 @@ var locales = {
2295
2337
  replacePlaceholder: "置換後…",
2296
2338
  replaceAriaLabel: "置換後のテキスト",
2297
2339
  replaceBtn: "置換",
2340
+ noResults: "結果なし",
2298
2341
  replaceAllBtn: "すべて置換",
2299
2342
  close: "×"
2300
2343
  },
@@ -2625,6 +2668,7 @@ var locales = {
2625
2668
  replacePlaceholder: "替换为…",
2626
2669
  replaceAriaLabel: "替换为",
2627
2670
  replaceBtn: "替换",
2671
+ noResults: "没有结果",
2628
2672
  replaceAllBtn: "全部替换",
2629
2673
  close: "×"
2630
2674
  },
@@ -2955,6 +2999,7 @@ var locales = {
2955
2999
  replacePlaceholder: "Remplacer par…",
2956
3000
  replaceAriaLabel: "Remplacer par",
2957
3001
  replaceBtn: "Remplacer",
3002
+ noResults: "Aucun résultat",
2958
3003
  replaceAllBtn: "Tout remplacer",
2959
3004
  close: "×"
2960
3005
  },
@@ -3285,6 +3330,7 @@ var locales = {
3285
3330
  replacePlaceholder: "Ersetzen durch…",
3286
3331
  replaceAriaLabel: "Ersetzen durch",
3287
3332
  replaceBtn: "Ersetzen",
3333
+ noResults: "Keine Ergebnisse",
3288
3334
  replaceAllBtn: "Alle ersetzen",
3289
3335
  close: "×"
3290
3336
  },
@@ -3615,6 +3661,7 @@ var locales = {
3615
3661
  replacePlaceholder: "Reemplazar con…",
3616
3662
  replaceAriaLabel: "Reemplazar con",
3617
3663
  replaceBtn: "Reemplazar",
3664
+ noResults: "Sin resultados",
3618
3665
  replaceAllBtn: "Reemplazar todo",
3619
3666
  close: "×"
3620
3667
  },
@@ -3945,6 +3992,7 @@ var locales = {
3945
3992
  replacePlaceholder: "바꿀 내용…",
3946
3993
  replaceAriaLabel: "바꿀 내용",
3947
3994
  replaceBtn: "바꾸기",
3995
+ noResults: "결과 없음",
3948
3996
  replaceAllBtn: "모두 바꾸기",
3949
3997
  close: "×"
3950
3998
  },
@@ -4165,8 +4213,10 @@ var PROHIBITED_TAGS = [
4165
4213
  "object",
4166
4214
  "embed",
4167
4215
  "form",
4168
- "button"
4216
+ "base"
4169
4217
  ];
4218
+ /** Tags whose element wrapper is stripped but content (child nodes) is preserved. */
4219
+ var UNWRAP_TAGS = new Set(["button"]);
4170
4220
  /** Attributes whose values must be sanitised as URLs. */
4171
4221
  var URL_ATTRS = [
4172
4222
  "href",
@@ -4184,60 +4234,61 @@ var TRUSTED_IFRAME_HOSTS = new Set([
4184
4234
  "player.vimeo.com"
4185
4235
  ]);
4186
4236
  /**
4187
- * Sanitises an HTML string by removing dangerous elements and attributes.
4188
- * Uses DOMParser so the sanitisation follows normal browser parsing rules —
4189
- * no regex shortcuts that can be bypassed by encoding tricks.
4237
+ * Produce a sanitized HTML string with dangerous elements and attributes removed.
4190
4238
  *
4191
- * - Strips PROHIBITED_TAGS (script, style, iframe, object, embed, form, button)
4192
- * - Allows input[type="checkbox"] only inside ul.an-checklist li; removes all other <input>
4193
- * - Removes all on* event-handler attributes
4194
- * - Rejects javascript: and vbscript: URLs in URL attributes
4195
- * - Rejects data: URIs everywhere except img[src] (base64 uploads)
4239
+ * Removes disallowed tags and wrappers, strips event-handler attributes, rejects
4240
+ * `javascript:`/`vbscript:` URLs and most `data:` URIs, restricts iframe `src`
4241
+ * to trusted hosts when enabled, and permits only checklist checkboxes as inputs.
4196
4242
  *
4197
- * @param {string} html
4198
- * @returns {string}
4243
+ * @param {string} html - HTML fragment to sanitize.
4244
+ * @param {Object} [options]
4245
+ * @param {boolean} [options.allowIframes=false] - If true, `iframe` elements are not removed but their `src` is restricted to trusted hosts and `srcdoc` is removed.
4246
+ * @returns {string} The sanitized HTML fragment.
4199
4247
  */
4200
4248
  function sanitiseHTML(html, { allowIframes = false } = {}) {
4201
4249
  const doc = new DOMParser().parseFromString(`<body>${html || ""}</body>`, "text/html");
4202
- (allowIframes ? PROHIBITED_TAGS.filter((t) => t !== "iframe") : PROHIBITED_TAGS).forEach((tag) => {
4203
- doc.querySelectorAll(tag).forEach((el) => el.remove());
4204
- });
4205
- doc.querySelectorAll("*").forEach((el) => {
4206
- Array.from(el.attributes).forEach((attr) => {
4250
+ const allElements = Array.from(doc.querySelectorAll("*"));
4251
+ const prohibited = new Set(allowIframes ? PROHIBITED_TAGS.filter((t) => t !== "iframe") : PROHIBITED_TAGS);
4252
+ for (const el of allElements) {
4253
+ const tag = el.tagName.toLowerCase();
4254
+ if (UNWRAP_TAGS.has(tag)) {
4255
+ el.replaceWith(...el.childNodes);
4256
+ continue;
4257
+ }
4258
+ if (prohibited.has(tag)) {
4259
+ el.remove();
4260
+ continue;
4261
+ }
4262
+ for (const attr of Array.from(el.attributes)) {
4207
4263
  if (attr.name.startsWith("on")) {
4208
4264
  el.removeAttribute(attr.name);
4209
- return;
4265
+ continue;
4210
4266
  }
4211
4267
  if (URL_ATTRS.includes(attr.name)) {
4212
4268
  const val = attr.value.trim();
4213
4269
  if (/^(javascript|vbscript):/i.test(val)) {
4214
4270
  el.removeAttribute(attr.name);
4215
- return;
4271
+ continue;
4216
4272
  }
4217
4273
  if (/^data:/i.test(val) && !(attr.name === "src" && el.tagName === "IMG")) el.removeAttribute(attr.name);
4218
4274
  }
4219
4275
  if (el.tagName === "IFRAME") {
4220
4276
  if (attr.name === "srcdoc") {
4221
4277
  el.removeAttribute(attr.name);
4222
- return;
4223
- }
4224
- if (attr.name === "src") {
4225
- if (!isTrustedIframeSrc(attr.value)) el.removeAttribute(attr.name);
4226
- return;
4278
+ continue;
4227
4279
  }
4280
+ if (attr.name === "src" && !isTrustedIframeSrc(attr.value)) el.removeAttribute(attr.name);
4228
4281
  }
4229
- });
4230
- });
4231
- doc.querySelectorAll("input").forEach((el) => {
4232
- if (!(el.closest("ul.an-checklist") !== null && el.closest("li") !== null) || el.getAttribute("type") !== "checkbox") el.remove();
4233
- else Array.from(el.attributes).forEach((attr) => {
4234
- if (![
4282
+ }
4283
+ if (tag === "input") {
4284
+ if (!(el.closest("ul.an-checklist") !== null && el.closest("li") !== null) || el.getAttribute("type") !== "checkbox") el.remove();
4285
+ else for (const attr of Array.from(el.attributes)) if (![
4235
4286
  "type",
4236
4287
  "checked",
4237
4288
  "contenteditable"
4238
4289
  ].includes(attr.name)) el.removeAttribute(attr.name);
4239
- });
4240
- });
4290
+ }
4291
+ }
4241
4292
  return doc.body.innerHTML;
4242
4293
  }
4243
4294
  /**
@@ -4438,7 +4489,18 @@ var History = class {
4438
4489
  const sel = window.getSelection();
4439
4490
  sel.removeAllRanges();
4440
4491
  sel.addRange(range);
4441
- } catch (_) {}
4492
+ } catch (_) {
4493
+ try {
4494
+ const fb = document.createRange();
4495
+ fb.setStart(this.editable, 0);
4496
+ fb.collapse(true);
4497
+ const s = window.getSelection();
4498
+ if (s) {
4499
+ s.removeAllRanges();
4500
+ s.addRange(fb);
4501
+ }
4502
+ } catch (_2) {}
4503
+ }
4442
4504
  }
4443
4505
  _savePoint() {
4444
4506
  if (this.stackOffset < this.stack.length - 1) this.stack = this.stack.slice(0, this.stackOffset + 1);
@@ -4465,6 +4527,10 @@ var History = class {
4465
4527
  * @returns {{ html: string, images: Object<string,string> }}
4466
4528
  */
4467
4529
  _tokenizeImages(html) {
4530
+ if (!html.includes("data:")) return {
4531
+ html,
4532
+ images: {}
4533
+ };
4468
4534
  const images = {};
4469
4535
  let index = 0;
4470
4536
  return {
@@ -4536,11 +4602,12 @@ var History = class {
4536
4602
  * Inspired by Summernote's table handling
4537
4603
  */
4538
4604
  /**
4539
- * Creates a table element with the specified dimensions.
4540
- * @param {number} cols
4541
- * @param {number} rows
4542
- * @param {{ headerRow?: boolean }} [opts]
4543
- * @returns {HTMLTableElement}
4605
+ * Build an HTML table with the given number of columns and rows, optionally including a header row.
4606
+ * @param {number} cols - Number of columns in each row.
4607
+ * @param {number} rows - Total number of rows to create (including header when `headerRow` is true).
4608
+ * @param {{ headerRow?: boolean }} [opts] - Options object.
4609
+ * @param {boolean} [opts.headerRow=false] - When true and `rows > 0`, creates a header row (`<thead>`) plus body rows for the remainder.
4610
+ * @returns {HTMLTableElement} The constructed `<table>` element with a `<tbody>` and optional `<thead>`; each cell contains a `<br>` placeholder.
4544
4611
  */
4545
4612
  function createTable(cols, rows, opts = {}) {
4546
4613
  const { headerRow = false } = opts;
@@ -4549,7 +4616,7 @@ function createTable(cols, rows, opts = {}) {
4549
4616
  const thead = createElement("thead");
4550
4617
  const tr = createElement("tr");
4551
4618
  for (let c = 0; c < cols; c++) {
4552
- const th = createElement("th", {}, ["\xA0"]);
4619
+ const th = createElement("th", {}, [document.createElement("br")]);
4553
4620
  tr.appendChild(th);
4554
4621
  }
4555
4622
  thead.appendChild(tr);
@@ -4561,7 +4628,7 @@ function createTable(cols, rows, opts = {}) {
4561
4628
  for (let r = 0; r < bodyRows; r++) {
4562
4629
  const tr = createElement("tr");
4563
4630
  for (let c = 0; c < cols; c++) {
4564
- const td = createElement("td", {}, ["\xA0"]);
4631
+ const td = createElement("td", {}, [document.createElement("br")]);
4565
4632
  tr.appendChild(td);
4566
4633
  }
4567
4634
  tbody.appendChild(tr);
@@ -4569,13 +4636,52 @@ function createTable(cols, rows, opts = {}) {
4569
4636
  return table;
4570
4637
  }
4571
4638
  /**
4572
- * Inserts a table at the current cursor position.
4573
- * @param {number} cols
4574
- * @param {number} rows
4575
- * @param {{ headerRow?: boolean }} [opts]
4639
+ * Insert a table at the current selection and place the caret into its first cell.
4640
+ * @param {number} cols - Number of columns for the new table.
4641
+ * @param {number} rows - Number of rows for the new table.
4642
+ * @param {{ headerRow?: boolean }} [opts] - Options for table creation.
4643
+ * @param {boolean} [opts.headerRow=false] - If true, include a header row as the first row.
4576
4644
  */
4577
4645
  function insertTable(cols, rows, opts = {}) {
4578
- execCommand("insertHTML", createTable(cols, rows, opts).outerHTML);
4646
+ if (cols <= 0 || rows <= 0) return;
4647
+ const table = createTable(cols, rows, opts);
4648
+ const sel = window.getSelection();
4649
+ if (!sel || sel.rangeCount === 0) return;
4650
+ const range = sel.getRangeAt(0);
4651
+ range.deleteContents();
4652
+ const BLOCK = new Set([
4653
+ "P",
4654
+ "DIV",
4655
+ "H1",
4656
+ "H2",
4657
+ "H3",
4658
+ "H4",
4659
+ "H5",
4660
+ "H6",
4661
+ "BLOCKQUOTE",
4662
+ "LI",
4663
+ "PRE"
4664
+ ]);
4665
+ let anchor = range.startContainer;
4666
+ if (anchor.nodeType === 3) anchor = anchor.parentElement;
4667
+ while (anchor && !BLOCK.has(anchor.tagName?.toUpperCase()) && anchor.parentElement) anchor = anchor.parentElement;
4668
+ if (anchor && BLOCK.has(anchor.tagName?.toUpperCase()) && anchor.parentNode) {
4669
+ anchor.after(table);
4670
+ if (!table.nextElementSibling) {
4671
+ const p = document.createElement("p");
4672
+ p.appendChild(document.createElement("br"));
4673
+ table.after(p);
4674
+ }
4675
+ if (!anchor.textContent.trim() && !anchor.querySelector("img, video, table")) anchor.remove();
4676
+ } else range.insertNode(table);
4677
+ const firstCell = table.querySelector("td, th");
4678
+ if (firstCell) {
4679
+ const nr = document.createRange();
4680
+ nr.setStart(firstCell, 0);
4681
+ nr.collapse(true);
4682
+ sel.removeAllRanges();
4683
+ sel.addRange(nr);
4684
+ }
4579
4685
  }
4580
4686
  //#endregion
4581
4687
  //#region src/js/core/key.js
@@ -4790,7 +4896,7 @@ function handleKeydown(event, editable, options = {}) {
4790
4896
  if (para && para.nodeName.toUpperCase() === "PRE") {
4791
4897
  if (event.shiftKey) return false;
4792
4898
  event.preventDefault();
4793
- execCommand("insertText", " ");
4899
+ execCommand("insertText", " ".repeat(options.tabSize || 4));
4794
4900
  return true;
4795
4901
  }
4796
4902
  if (options.tabSize) {
@@ -4883,6 +4989,11 @@ function handleKeydown(event, editable, options = {}) {
4883
4989
  return true;
4884
4990
  }
4885
4991
  const para = closestPara(range.sc, editable);
4992
+ if (para && para.nodeName.toUpperCase() === "PRE") {
4993
+ event.preventDefault();
4994
+ execCommand("insertText", "\n");
4995
+ return true;
4996
+ }
4886
4997
  if (para && para.nodeName.toUpperCase() === "BLOCKQUOTE") {
4887
4998
  const native = range.toNativeRange();
4888
4999
  native.setEnd(para, para.childNodes.length);
@@ -4916,11 +5027,22 @@ function handleKeydown(event, editable, options = {}) {
4916
5027
  function htmlToMarkdown(html) {
4917
5028
  return _domToMd(new DOMParser().parseFromString(`<body>${html || ""}</body>`, "text/html").body).replace(/\n{3,}/g, "\n\n").trim();
4918
5029
  }
4919
- function _domToMd(node) {
5030
+ /**
5031
+ * Convert a DOM node subtree into Markdown.
5032
+ *
5033
+ * Recursively produces a Markdown string representing the given DOM node and its descendants,
5034
+ * handling common HTML constructs such as paragraphs, headings, lists (with nested indentation),
5035
+ * blockquotes, fenced and inline code, links, images, tables, horizontal rules, and basic inline emphasis.
5036
+ *
5037
+ * @param {Node} node - The DOM node to convert.
5038
+ * @param {number} [depth=0] - Current nesting depth used to indent nested list items.
5039
+ * @returns {string} The Markdown representation of the node subtree.
5040
+ */
5041
+ function _domToMd(node, depth = 0) {
4920
5042
  if (node.nodeType === 3) return node.textContent.replace(/\s+/g, " ");
4921
5043
  if (node.nodeType !== 1) return "";
4922
5044
  const tag = node.nodeName.toLowerCase();
4923
- const inner = () => Array.from(node.childNodes).map(_domToMd).join("");
5045
+ const inner = () => Array.from(node.childNodes).map((n) => _domToMd(n, depth)).join("");
4924
5046
  switch (tag) {
4925
5047
  case "p":
4926
5048
  case "div": return `\n\n${inner()}\n\n`;
@@ -4960,12 +5082,16 @@ function _domToMd(node) {
4960
5082
  case "ul": {
4961
5083
  const items = Array.from(node.querySelectorAll(":scope > li"));
4962
5084
  if (!items.length) return inner();
4963
- return `\n\n${items.map((li) => `- ${_domToMd(li).trim()}`).join("\n")}\n\n`;
5085
+ const indent = " ".repeat(depth);
5086
+ const lines = items.map((li) => `${indent}- ${_domToMd(li, depth + 1).trim()}`).join("\n");
5087
+ return depth === 0 ? `\n\n${lines}\n\n` : `\n${lines}`;
4964
5088
  }
4965
5089
  case "ol": {
4966
5090
  const items = Array.from(node.querySelectorAll(":scope > li"));
4967
5091
  if (!items.length) return inner();
4968
- return `\n\n${items.map((li, i) => `${i + 1}. ${_domToMd(li).trim()}`).join("\n")}\n\n`;
5092
+ const indent = " ".repeat(depth);
5093
+ const lines = items.map((li, i) => `${indent}${i + 1}. ${_domToMd(li, depth + 1).trim()}`).join("\n");
5094
+ return depth === 0 ? `\n\n${lines}\n\n` : `\n${lines}`;
4969
5095
  }
4970
5096
  case "li": return inner();
4971
5097
  case "hr": return "\n\n---\n\n";
@@ -4989,12 +5115,15 @@ function _domToMd(node) {
4989
5115
  }
4990
5116
  }
4991
5117
  /**
4992
- * Returns true if the text contains recognisable Markdown patterns.
4993
- * @param {string} text
4994
- * @returns {boolean}
5118
+ * Detects whether a string likely contains Markdown syntax.
5119
+ *
5120
+ * Checks for common Markdown constructs such as ATX headings, unordered or
5121
+ * ordered list items, blockquotes, fenced code blocks, and bold emphasis.
5122
+ * @param {string} text - Input text to inspect for Markdown patterns.
5123
+ * @returns {boolean} `true` if any Markdown-like pattern is present, `false` otherwise.
4995
5124
  */
4996
5125
  function isMarkdown(text) {
4997
- return /^#{1,6} \S|^\s*[-*+] \S|^\s*\d+\. \S|^> \S|\*{2}.+?\*{2}|^```/m.test(text);
5126
+ return /^#{1,6} \S|^\s*[-*+] \S|^\s*\d+\. \S|^> \S|^```|^\*{2}.+?\*{2}/m.test(text);
4998
5127
  }
4999
5128
  /**
5000
5129
  * Converts a Markdown string to an HTML string.
@@ -5757,6 +5886,8 @@ var Toolbar = class {
5757
5886
  this._disposers = [];
5758
5887
  /** @type {Array<() => void>} closers for all open color picker popups */
5759
5888
  this._colorPickerClosers = [];
5889
+ /** @type {number|null} rAF handle for debounced refresh */
5890
+ this._refreshRaf = null;
5760
5891
  }
5761
5892
  initialize() {
5762
5893
  this.el = createElement("div", { class: "an-toolbar" });
@@ -5766,6 +5897,8 @@ var Toolbar = class {
5766
5897
  return this;
5767
5898
  }
5768
5899
  destroy() {
5900
+ if (this._refreshRaf) cancelAnimationFrame(this._refreshRaf);
5901
+ this._refreshRaf = null;
5769
5902
  this._disposers.forEach((d) => d());
5770
5903
  this._disposers = [];
5771
5904
  if (this.el && this.el.parentNode) this.el.parentNode.removeChild(this.el);
@@ -5840,6 +5973,7 @@ var Toolbar = class {
5840
5973
  const openPopup = () => {
5841
5974
  isOpen = true;
5842
5975
  const rect = btn.getBoundingClientRect();
5976
+ popup.style.visibility = "hidden";
5843
5977
  popup.style.display = "block";
5844
5978
  const pw = popup.offsetWidth;
5845
5979
  const ph = popup.offsetHeight;
@@ -5849,13 +5983,12 @@ var Toolbar = class {
5849
5983
  if (top + ph > window.innerHeight - 8) top = rect.top - ph - 4;
5850
5984
  popup.style.left = `${left}px`;
5851
5985
  popup.style.top = `${top}px`;
5986
+ popup.style.visibility = "";
5852
5987
  btn.setAttribute("aria-expanded", "true");
5853
5988
  };
5854
5989
  const closePopup = () => {
5855
5990
  isOpen = false;
5856
5991
  popup.style.display = "none";
5857
- popup.style.top = "";
5858
- popup.style.left = "";
5859
5992
  btn.setAttribute("aria-expanded", "false");
5860
5993
  setHighlight(0, 0);
5861
5994
  };
@@ -5882,9 +6015,11 @@ var Toolbar = class {
5882
6015
  const d5 = on(document, "click", () => {
5883
6016
  if (isOpen) closePopup();
5884
6017
  });
5885
- this._disposers.push(d1, d2, d3, d4, d5);
6018
+ this._disposers.push(d1, d2, d3, d4, d5, () => {
6019
+ if (popup.parentNode) popup.parentNode.removeChild(popup);
6020
+ });
5886
6021
  wrap.appendChild(btn);
5887
- wrap.appendChild(popup);
6022
+ document.body.appendChild(popup);
5888
6023
  return wrap;
5889
6024
  }
5890
6025
  /**
@@ -6159,6 +6294,13 @@ var Toolbar = class {
6159
6294
  return _faPageLevelReady;
6160
6295
  }
6161
6296
  refresh() {
6297
+ if (this._refreshRaf) cancelAnimationFrame(this._refreshRaf);
6298
+ this._refreshRaf = requestAnimationFrame(() => {
6299
+ this._refreshRaf = null;
6300
+ this._doRefresh();
6301
+ });
6302
+ }
6303
+ _doRefresh() {
6162
6304
  if (!this.el) return;
6163
6305
  const btnMap = this._btnMap || /* @__PURE__ */ new Map();
6164
6306
  this.el.querySelectorAll("button[data-btn]").forEach((btn) => {
@@ -6336,7 +6478,7 @@ var Statusbar = class {
6336
6478
  }
6337
6479
  update() {
6338
6480
  if (!this._wordCountEl || !this._charCountEl) return;
6339
- const text = this.context.layoutInfo.editable.innerText || "";
6481
+ const text = this.context.layoutInfo.editable.textContent || "";
6340
6482
  const words = _countWords(text);
6341
6483
  const chars = text.replace(/\n/g, "").length;
6342
6484
  const maxWords = this.options.maxWords || 0;
@@ -6531,7 +6673,7 @@ var Clipboard = class {
6531
6673
  event.preventDefault();
6532
6674
  const raw = clipboardData.getData("text/html");
6533
6675
  const isWordContent = /<[a-z]+:[a-z]/i.test(raw) || /class="Mso/i.test(raw) || /\bmso-/i.test(raw);
6534
- const isSocialContent = /\bdata-testid\b/.test(raw) || /class="[^"]*\b(?:x[a-z0-9]{6,}|r-[a-z0-9]{3,})\b/.test(raw);
6676
+ const isSocialContent = /class="[^"]*\b(?:x[a-z0-9]{6,}|r-[a-z0-9]{3,})\b/.test(raw);
6535
6677
  let html = raw;
6536
6678
  if (isWordContent) html = this._cleanWordHtml(html);
6537
6679
  else if (isSocialContent) html = this._cleanSocialHtml(html);
@@ -6633,7 +6775,7 @@ var Clipboard = class {
6633
6775
  */
6634
6776
  _dataUrlToBlob(dataUrl) {
6635
6777
  const [header, b64] = dataUrl.split(",");
6636
- const mime = header.match(/:(.*?);/)[1];
6778
+ const mime = header.match(/:(.*?);/)?.[1] ?? "image/png";
6637
6779
  const binary = atob(b64);
6638
6780
  const arr = new Uint8Array(binary.length);
6639
6781
  for (let i = 0; i < binary.length; i++) arr[i] = binary.charCodeAt(i);
@@ -7639,6 +7781,7 @@ var ImageResizer = class {
7639
7781
  _select(img) {
7640
7782
  if (this._activeImg && this._activeImg !== img) this._activeImg.classList.remove("an-image-selected");
7641
7783
  this._activeImg = img;
7784
+ this._lastOverlayPos = null;
7642
7785
  img.classList.add("an-image-selected");
7643
7786
  this._updateOverlayPosition();
7644
7787
  this._overlay.style.display = "block";
@@ -7662,6 +7805,14 @@ var ImageResizer = class {
7662
7805
  const offsetParent = this._overlay.offsetParent || this._container;
7663
7806
  const containerRect = offsetParent.getBoundingClientRect();
7664
7807
  const rect = this._activeImg.getBoundingClientRect();
7808
+ const p = this._lastOverlayPos;
7809
+ if (p && p.l === rect.left && p.t === rect.top && p.w === rect.width && p.h === rect.height) return;
7810
+ this._lastOverlayPos = {
7811
+ l: rect.left,
7812
+ t: rect.top,
7813
+ w: rect.width,
7814
+ h: rect.height
7815
+ };
7665
7816
  const left = rect.left - containerRect.left + offsetParent.scrollLeft;
7666
7817
  const top = rect.top - containerRect.top + offsetParent.scrollTop;
7667
7818
  this._overlay.style.left = `${left}px`;
@@ -13360,7 +13511,7 @@ var ContextMenu = class {
13360
13511
  cells.forEach((cell) => {
13361
13512
  cell.classList.toggle("active", +cell.dataset.row <= rows && +cell.dataset.col <= cols);
13362
13513
  });
13363
- labelEl.textContent = rows && cols ? `${cols} × ${rows}` : this.context.locale.contextMenu.table || "Insert Table";
13514
+ labelEl.textContent = rows && cols ? `${rows} × ${cols}` : this.context.locale.contextMenu.table || "Insert Table";
13364
13515
  };
13365
13516
  panel.appendChild(gridEl);
13366
13517
  panel.appendChild(labelEl);
@@ -14000,7 +14151,6 @@ var FindReplace = class {
14000
14151
  if (!editable) return;
14001
14152
  const rawMatches = this._findRawMatches(query, editable);
14002
14153
  if (rawMatches.length === 0) return;
14003
- this._currentIndex = 0;
14004
14154
  for (let i = rawMatches.length - 1; i >= 0; i--) {
14005
14155
  const { node, start, end } = rawMatches[i];
14006
14156
  try {
@@ -14017,6 +14167,7 @@ var FindReplace = class {
14017
14167
  }
14018
14168
  this._matches.reverse();
14019
14169
  this._matches = this._matches.filter((m) => m.mark);
14170
+ this._currentIndex = 0;
14020
14171
  if (this._matches.length === 0) return;
14021
14172
  if (this._matches[0] && this._matches[0].mark) {
14022
14173
  this._matches[0].mark.className = "an-highlight an-highlight-current";
@@ -14042,12 +14193,13 @@ var FindReplace = class {
14042
14193
  this._lastCaseSensitive = this._caseSensitive;
14043
14194
  }
14044
14195
  const re = this._queryRegex;
14196
+ const MAX_RESULTS = 500;
14045
14197
  const walker = document.createTreeWalker(root, 4);
14046
14198
  let node;
14047
- while (node = walker.nextNode()) {
14199
+ while ((node = walker.nextNode()) && results.length < MAX_RESULTS) {
14048
14200
  re.lastIndex = 0;
14049
14201
  let m;
14050
- while ((m = re.exec(node.textContent)) !== null) results.push({
14202
+ while ((m = re.exec(node.textContent)) !== null && results.length < MAX_RESULTS) results.push({
14051
14203
  node,
14052
14204
  start: m.index,
14053
14205
  end: m.index + m[0].length
@@ -14134,7 +14286,7 @@ var FindReplace = class {
14134
14286
  const total = this._matches.length;
14135
14287
  if (total === 0) {
14136
14288
  const query = this._findInput ? this._findInput.value : "";
14137
- this._counterEl.textContent = query ? "No results" : "";
14289
+ this._counterEl.textContent = query ? this.context.locale.findReplace.noResults : "";
14138
14290
  } else this._counterEl.textContent = `${this._currentIndex + 1} / ${total}`;
14139
14291
  }
14140
14292
  };
@@ -14333,17 +14485,33 @@ var ImageCropOverlay = class {
14333
14485
  e.preventDefault();
14334
14486
  e.stopPropagation();
14335
14487
  this._startHandleDrag(e, id);
14336
- }));
14488
+ }), on(h, "touchstart", (e) => {
14489
+ e.preventDefault();
14490
+ e.stopPropagation();
14491
+ this._startHandleDrag({
14492
+ clientX: e.touches[0].clientX,
14493
+ clientY: e.touches[0].clientY
14494
+ }, id);
14495
+ }, { passive: false }));
14337
14496
  this._handles[id] = h;
14338
14497
  cropBox.appendChild(h);
14339
14498
  });
14340
14499
  this._disposers.push(on(cropBox, "mousedown", (e) => {
14341
- if (e.target !== cropBox && e.target !== grid && !(e.target.tagName === "DIV" && !e.target.className.includes("handle"))) return;
14342
- if (e.target.className && e.target.className.includes("an-crop-handle")) return;
14500
+ if (e.target.classList.contains("an-crop-handle")) return;
14501
+ if (e.target !== cropBox && e.target !== grid) return;
14343
14502
  e.preventDefault();
14344
14503
  e.stopPropagation();
14345
14504
  this._startBoxMove(e);
14346
- }));
14505
+ }), on(cropBox, "touchstart", (e) => {
14506
+ if (e.target.classList.contains("an-crop-handle")) return;
14507
+ if (e.target !== cropBox && e.target !== grid) return;
14508
+ e.preventDefault();
14509
+ e.stopPropagation();
14510
+ this._startBoxMove({
14511
+ clientX: e.touches[0].clientX,
14512
+ clientY: e.touches[0].clientY
14513
+ });
14514
+ }, { passive: false }));
14347
14515
  const infoEl = document.createElement("div");
14348
14516
  infoEl.className = "an-crop-info";
14349
14517
  infoEl.style.cssText = `
@@ -14501,15 +14669,26 @@ var ImageCropOverlay = class {
14501
14669
  * @param {(e: MouseEvent) => void} onMove
14502
14670
  */
14503
14671
  _attachDocDrag(onMove) {
14672
+ const onTouchMove = (e) => {
14673
+ e.preventDefault();
14674
+ onMove({
14675
+ clientX: e.touches[0].clientX,
14676
+ clientY: e.touches[0].clientY
14677
+ });
14678
+ };
14504
14679
  const cleanup = () => {
14505
14680
  document.removeEventListener("mousemove", onMove);
14506
14681
  document.removeEventListener("mouseup", cleanup);
14682
+ document.removeEventListener("touchmove", onTouchMove);
14683
+ document.removeEventListener("touchend", cleanup);
14507
14684
  document.body.style.userSelect = "";
14508
14685
  document.body.style.cursor = "";
14509
14686
  };
14510
14687
  document.body.style.userSelect = "none";
14511
14688
  document.addEventListener("mousemove", onMove);
14512
14689
  document.addEventListener("mouseup", cleanup, { once: true });
14690
+ document.addEventListener("touchmove", onTouchMove, { passive: false });
14691
+ document.addEventListener("touchend", cleanup, { once: true });
14513
14692
  }
14514
14693
  _bindEsc() {
14515
14694
  const handler = (e) => {
@@ -14550,7 +14729,7 @@ var ImageCropOverlay = class {
14550
14729
  height: natH
14551
14730
  }, w, h);
14552
14731
  if (!canvas) {
14553
- window.alert("Cannot crop this image: the image server does not allow cross-origin access.\nUpload the image directly to use the crop tool.");
14732
+ this._showCropError("Cannot crop this image: the image server does not allow cross-origin access. Upload the image directly to use the crop tool.");
14554
14733
  this._close(false);
14555
14734
  return;
14556
14735
  }
@@ -14567,6 +14746,37 @@ var ImageCropOverlay = class {
14567
14746
  this.context.invoke("imageResizer.updateOverlay");
14568
14747
  }
14569
14748
  /**
14749
+ * Show a non-blocking inline error banner appended to document.body.
14750
+ * Auto-dismisses after 4 seconds.
14751
+ * @param {string} msg
14752
+ */
14753
+ _showCropError(msg) {
14754
+ const banner = document.createElement("div");
14755
+ banner.setAttribute("role", "alert");
14756
+ banner.style.cssText = [
14757
+ "position:fixed",
14758
+ "bottom:24px",
14759
+ "left:50%",
14760
+ "transform:translateX(-50%)",
14761
+ "z-index:10200",
14762
+ "max-width:420px",
14763
+ "width:max-content",
14764
+ "background:#7f1d1d",
14765
+ "color:#fecaca",
14766
+ "border:1px solid #b91c1c",
14767
+ "border-radius:8px",
14768
+ "padding:12px 18px",
14769
+ "font:13px/1.5 system-ui,sans-serif",
14770
+ "box-shadow:0 4px 16px rgba(0,0,0,.4)",
14771
+ "pointer-events:auto"
14772
+ ].join(";");
14773
+ banner.textContent = msg;
14774
+ document.body.appendChild(banner);
14775
+ setTimeout(() => {
14776
+ if (banner.parentNode) banner.parentNode.removeChild(banner);
14777
+ }, 4e3);
14778
+ }
14779
+ /**
14570
14780
  * Remove all overlay DOM elements and reset state.
14571
14781
  * @param {boolean} _committed - reserved for future use
14572
14782
  */
@@ -14981,6 +15191,16 @@ var _ACTIONS = {
14981
15191
  if (!editable) return;
14982
15192
  editable.focus();
14983
15193
  document.execCommand("removeFormat");
15194
+ const sel = window.getSelection();
15195
+ if (sel && sel.rangeCount > 0 && !sel.getRangeAt(0).collapsed) {
15196
+ const range = sel.getRangeAt(0);
15197
+ const ancestor = range.commonAncestorContainer;
15198
+ const root = ancestor.nodeType === 1 ? ancestor : ancestor.parentElement;
15199
+ if (root) {
15200
+ const candidates = [root, ...root.querySelectorAll("[style]")];
15201
+ for (const el of candidates) if (el.hasAttribute("style") && range.intersectsNode(el)) el.removeAttribute("style");
15202
+ }
15203
+ }
14984
15204
  ctx.invoke("editor.afterCommand");
14985
15205
  },
14986
15206
  inlineCode: (ctx) => ctx.invoke("editor.inlineCode")
@@ -15094,6 +15314,7 @@ var BubbleToolbar = class {
15094
15314
  }
15095
15315
  document.body.appendChild(el);
15096
15316
  this._el = el;
15317
+ this._btnCache = Array.from(el.querySelectorAll(".an-bubble-btn"));
15097
15318
  }
15098
15319
  _buildColorPicker() {
15099
15320
  const picker = document.createElement("div");
@@ -15179,8 +15400,13 @@ var BubbleToolbar = class {
15179
15400
  editable.focus();
15180
15401
  const sel = window.getSelection();
15181
15402
  sel.removeAllRanges();
15182
- sel.addRange(this._savedRange.cloneRange());
15183
- document.execCommand(type, false, color);
15403
+ try {
15404
+ sel.addRange(this._savedRange.cloneRange());
15405
+ } catch (_) {
15406
+ return;
15407
+ }
15408
+ const cmd = type === "hiliteColor" ? "hiliteColor" : type;
15409
+ if (!document.execCommand(cmd, false, color) && cmd === "hiliteColor") document.execCommand("backColor", false, color);
15184
15410
  this.context.invoke("editor.afterCommand");
15185
15411
  const name = type === "hiliteColor" ? "hiliteColor" : "foreColor";
15186
15412
  const btn = this._el && this._el.querySelector(`[data-name="${name}"]`);
@@ -15215,8 +15441,8 @@ var BubbleToolbar = class {
15215
15441
  this._closeColorPicker();
15216
15442
  }
15217
15443
  _syncActive() {
15218
- if (!this._el) return;
15219
- this._el.querySelectorAll(".an-bubble-btn").forEach((btn) => {
15444
+ if (!this._btnCache) return;
15445
+ this._btnCache.forEach((btn) => {
15220
15446
  const activeFn = _ACTIVE[btn.dataset.name];
15221
15447
  btn.classList.toggle("an-active", !!(activeFn && activeFn()));
15222
15448
  });
@@ -15363,14 +15589,23 @@ var Mention = class {
15363
15589
  const el = document.createElement("div");
15364
15590
  el.className = "an-mention-dropdown";
15365
15591
  el.setAttribute("role", "listbox");
15592
+ el.addEventListener("mousedown", (e) => e.preventDefault());
15593
+ el.addEventListener("click", (e) => {
15594
+ const item = e.target.closest(".an-mention-item");
15595
+ if (item) this._select(+item.dataset.index);
15596
+ });
15597
+ el.addEventListener("mousemove", (e) => {
15598
+ const item = e.target.closest(".an-mention-item");
15599
+ if (item) this._highlightItem(+item.dataset.index);
15600
+ });
15366
15601
  document.body.appendChild(el);
15367
15602
  this._dropdown = el;
15368
15603
  }
15369
15604
  _renderItems(items) {
15370
15605
  const dd = this._dropdown;
15371
- dd.innerHTML = "";
15372
15606
  this._items = items.slice(0, this._cfg.maxResults);
15373
15607
  this._activeIndex = this._items.length > 0 ? 0 : -1;
15608
+ const frag = document.createDocumentFragment();
15374
15609
  this._items.forEach((item, i) => {
15375
15610
  const li = document.createElement("div");
15376
15611
  li.className = "an-mention-item";
@@ -15386,10 +15621,10 @@ var Mention = class {
15386
15621
  const label = document.createElement("span");
15387
15622
  label.textContent = item.label;
15388
15623
  li.appendChild(label);
15389
- li.addEventListener("mousedown", (e) => e.preventDefault());
15390
- li.addEventListener("click", () => this._select(i));
15391
- dd.appendChild(li);
15624
+ frag.appendChild(li);
15392
15625
  });
15626
+ dd.innerHTML = "";
15627
+ dd.appendChild(frag);
15393
15628
  this._highlightItem(this._activeIndex);
15394
15629
  }
15395
15630
  _highlightItem(index) {