autumnnote 1.2.1 → 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);
@@ -6161,6 +6294,13 @@ var Toolbar = class {
6161
6294
  return _faPageLevelReady;
6162
6295
  }
6163
6296
  refresh() {
6297
+ if (this._refreshRaf) cancelAnimationFrame(this._refreshRaf);
6298
+ this._refreshRaf = requestAnimationFrame(() => {
6299
+ this._refreshRaf = null;
6300
+ this._doRefresh();
6301
+ });
6302
+ }
6303
+ _doRefresh() {
6164
6304
  if (!this.el) return;
6165
6305
  const btnMap = this._btnMap || /* @__PURE__ */ new Map();
6166
6306
  this.el.querySelectorAll("button[data-btn]").forEach((btn) => {
@@ -6338,7 +6478,7 @@ var Statusbar = class {
6338
6478
  }
6339
6479
  update() {
6340
6480
  if (!this._wordCountEl || !this._charCountEl) return;
6341
- const text = this.context.layoutInfo.editable.innerText || "";
6481
+ const text = this.context.layoutInfo.editable.textContent || "";
6342
6482
  const words = _countWords(text);
6343
6483
  const chars = text.replace(/\n/g, "").length;
6344
6484
  const maxWords = this.options.maxWords || 0;
@@ -6533,7 +6673,7 @@ var Clipboard = class {
6533
6673
  event.preventDefault();
6534
6674
  const raw = clipboardData.getData("text/html");
6535
6675
  const isWordContent = /<[a-z]+:[a-z]/i.test(raw) || /class="Mso/i.test(raw) || /\bmso-/i.test(raw);
6536
- 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);
6537
6677
  let html = raw;
6538
6678
  if (isWordContent) html = this._cleanWordHtml(html);
6539
6679
  else if (isSocialContent) html = this._cleanSocialHtml(html);
@@ -6635,7 +6775,7 @@ var Clipboard = class {
6635
6775
  */
6636
6776
  _dataUrlToBlob(dataUrl) {
6637
6777
  const [header, b64] = dataUrl.split(",");
6638
- const mime = header.match(/:(.*?);/)[1];
6778
+ const mime = header.match(/:(.*?);/)?.[1] ?? "image/png";
6639
6779
  const binary = atob(b64);
6640
6780
  const arr = new Uint8Array(binary.length);
6641
6781
  for (let i = 0; i < binary.length; i++) arr[i] = binary.charCodeAt(i);
@@ -7641,6 +7781,7 @@ var ImageResizer = class {
7641
7781
  _select(img) {
7642
7782
  if (this._activeImg && this._activeImg !== img) this._activeImg.classList.remove("an-image-selected");
7643
7783
  this._activeImg = img;
7784
+ this._lastOverlayPos = null;
7644
7785
  img.classList.add("an-image-selected");
7645
7786
  this._updateOverlayPosition();
7646
7787
  this._overlay.style.display = "block";
@@ -7664,6 +7805,14 @@ var ImageResizer = class {
7664
7805
  const offsetParent = this._overlay.offsetParent || this._container;
7665
7806
  const containerRect = offsetParent.getBoundingClientRect();
7666
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
+ };
7667
7816
  const left = rect.left - containerRect.left + offsetParent.scrollLeft;
7668
7817
  const top = rect.top - containerRect.top + offsetParent.scrollTop;
7669
7818
  this._overlay.style.left = `${left}px`;
@@ -13362,7 +13511,7 @@ var ContextMenu = class {
13362
13511
  cells.forEach((cell) => {
13363
13512
  cell.classList.toggle("active", +cell.dataset.row <= rows && +cell.dataset.col <= cols);
13364
13513
  });
13365
- 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";
13366
13515
  };
13367
13516
  panel.appendChild(gridEl);
13368
13517
  panel.appendChild(labelEl);
@@ -14002,7 +14151,6 @@ var FindReplace = class {
14002
14151
  if (!editable) return;
14003
14152
  const rawMatches = this._findRawMatches(query, editable);
14004
14153
  if (rawMatches.length === 0) return;
14005
- this._currentIndex = 0;
14006
14154
  for (let i = rawMatches.length - 1; i >= 0; i--) {
14007
14155
  const { node, start, end } = rawMatches[i];
14008
14156
  try {
@@ -14019,6 +14167,7 @@ var FindReplace = class {
14019
14167
  }
14020
14168
  this._matches.reverse();
14021
14169
  this._matches = this._matches.filter((m) => m.mark);
14170
+ this._currentIndex = 0;
14022
14171
  if (this._matches.length === 0) return;
14023
14172
  if (this._matches[0] && this._matches[0].mark) {
14024
14173
  this._matches[0].mark.className = "an-highlight an-highlight-current";
@@ -14044,12 +14193,13 @@ var FindReplace = class {
14044
14193
  this._lastCaseSensitive = this._caseSensitive;
14045
14194
  }
14046
14195
  const re = this._queryRegex;
14196
+ const MAX_RESULTS = 500;
14047
14197
  const walker = document.createTreeWalker(root, 4);
14048
14198
  let node;
14049
- while (node = walker.nextNode()) {
14199
+ while ((node = walker.nextNode()) && results.length < MAX_RESULTS) {
14050
14200
  re.lastIndex = 0;
14051
14201
  let m;
14052
- while ((m = re.exec(node.textContent)) !== null) results.push({
14202
+ while ((m = re.exec(node.textContent)) !== null && results.length < MAX_RESULTS) results.push({
14053
14203
  node,
14054
14204
  start: m.index,
14055
14205
  end: m.index + m[0].length
@@ -14136,7 +14286,7 @@ var FindReplace = class {
14136
14286
  const total = this._matches.length;
14137
14287
  if (total === 0) {
14138
14288
  const query = this._findInput ? this._findInput.value : "";
14139
- this._counterEl.textContent = query ? "No results" : "";
14289
+ this._counterEl.textContent = query ? this.context.locale.findReplace.noResults : "";
14140
14290
  } else this._counterEl.textContent = `${this._currentIndex + 1} / ${total}`;
14141
14291
  }
14142
14292
  };
@@ -14335,17 +14485,33 @@ var ImageCropOverlay = class {
14335
14485
  e.preventDefault();
14336
14486
  e.stopPropagation();
14337
14487
  this._startHandleDrag(e, id);
14338
- }));
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 }));
14339
14496
  this._handles[id] = h;
14340
14497
  cropBox.appendChild(h);
14341
14498
  });
14342
14499
  this._disposers.push(on(cropBox, "mousedown", (e) => {
14343
- if (e.target !== cropBox && e.target !== grid && !(e.target.tagName === "DIV" && !e.target.className.includes("handle"))) return;
14344
- 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;
14345
14502
  e.preventDefault();
14346
14503
  e.stopPropagation();
14347
14504
  this._startBoxMove(e);
14348
- }));
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 }));
14349
14515
  const infoEl = document.createElement("div");
14350
14516
  infoEl.className = "an-crop-info";
14351
14517
  infoEl.style.cssText = `
@@ -14503,15 +14669,26 @@ var ImageCropOverlay = class {
14503
14669
  * @param {(e: MouseEvent) => void} onMove
14504
14670
  */
14505
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
+ };
14506
14679
  const cleanup = () => {
14507
14680
  document.removeEventListener("mousemove", onMove);
14508
14681
  document.removeEventListener("mouseup", cleanup);
14682
+ document.removeEventListener("touchmove", onTouchMove);
14683
+ document.removeEventListener("touchend", cleanup);
14509
14684
  document.body.style.userSelect = "";
14510
14685
  document.body.style.cursor = "";
14511
14686
  };
14512
14687
  document.body.style.userSelect = "none";
14513
14688
  document.addEventListener("mousemove", onMove);
14514
14689
  document.addEventListener("mouseup", cleanup, { once: true });
14690
+ document.addEventListener("touchmove", onTouchMove, { passive: false });
14691
+ document.addEventListener("touchend", cleanup, { once: true });
14515
14692
  }
14516
14693
  _bindEsc() {
14517
14694
  const handler = (e) => {
@@ -14552,7 +14729,7 @@ var ImageCropOverlay = class {
14552
14729
  height: natH
14553
14730
  }, w, h);
14554
14731
  if (!canvas) {
14555
- 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.");
14556
14733
  this._close(false);
14557
14734
  return;
14558
14735
  }
@@ -14569,6 +14746,37 @@ var ImageCropOverlay = class {
14569
14746
  this.context.invoke("imageResizer.updateOverlay");
14570
14747
  }
14571
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
+ /**
14572
14780
  * Remove all overlay DOM elements and reset state.
14573
14781
  * @param {boolean} _committed - reserved for future use
14574
14782
  */
@@ -14983,6 +15191,16 @@ var _ACTIONS = {
14983
15191
  if (!editable) return;
14984
15192
  editable.focus();
14985
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
+ }
14986
15204
  ctx.invoke("editor.afterCommand");
14987
15205
  },
14988
15206
  inlineCode: (ctx) => ctx.invoke("editor.inlineCode")
@@ -15096,6 +15314,7 @@ var BubbleToolbar = class {
15096
15314
  }
15097
15315
  document.body.appendChild(el);
15098
15316
  this._el = el;
15317
+ this._btnCache = Array.from(el.querySelectorAll(".an-bubble-btn"));
15099
15318
  }
15100
15319
  _buildColorPicker() {
15101
15320
  const picker = document.createElement("div");
@@ -15181,8 +15400,13 @@ var BubbleToolbar = class {
15181
15400
  editable.focus();
15182
15401
  const sel = window.getSelection();
15183
15402
  sel.removeAllRanges();
15184
- sel.addRange(this._savedRange.cloneRange());
15185
- 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);
15186
15410
  this.context.invoke("editor.afterCommand");
15187
15411
  const name = type === "hiliteColor" ? "hiliteColor" : "foreColor";
15188
15412
  const btn = this._el && this._el.querySelector(`[data-name="${name}"]`);
@@ -15217,8 +15441,8 @@ var BubbleToolbar = class {
15217
15441
  this._closeColorPicker();
15218
15442
  }
15219
15443
  _syncActive() {
15220
- if (!this._el) return;
15221
- this._el.querySelectorAll(".an-bubble-btn").forEach((btn) => {
15444
+ if (!this._btnCache) return;
15445
+ this._btnCache.forEach((btn) => {
15222
15446
  const activeFn = _ACTIVE[btn.dataset.name];
15223
15447
  btn.classList.toggle("an-active", !!(activeFn && activeFn()));
15224
15448
  });
@@ -15365,14 +15589,23 @@ var Mention = class {
15365
15589
  const el = document.createElement("div");
15366
15590
  el.className = "an-mention-dropdown";
15367
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
+ });
15368
15601
  document.body.appendChild(el);
15369
15602
  this._dropdown = el;
15370
15603
  }
15371
15604
  _renderItems(items) {
15372
15605
  const dd = this._dropdown;
15373
- dd.innerHTML = "";
15374
15606
  this._items = items.slice(0, this._cfg.maxResults);
15375
15607
  this._activeIndex = this._items.length > 0 ? 0 : -1;
15608
+ const frag = document.createDocumentFragment();
15376
15609
  this._items.forEach((item, i) => {
15377
15610
  const li = document.createElement("div");
15378
15611
  li.className = "an-mention-item";
@@ -15388,10 +15621,10 @@ var Mention = class {
15388
15621
  const label = document.createElement("span");
15389
15622
  label.textContent = item.label;
15390
15623
  li.appendChild(label);
15391
- li.addEventListener("mousedown", (e) => e.preventDefault());
15392
- li.addEventListener("click", () => this._select(i));
15393
- dd.appendChild(li);
15624
+ frag.appendChild(li);
15394
15625
  });
15626
+ dd.innerHTML = "";
15627
+ dd.appendChild(frag);
15395
15628
  this._highlightItem(this._activeIndex);
15396
15629
  }
15397
15630
  _highlightItem(index) {