autumnnote 1.2.1 → 1.4.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.
@@ -428,9 +428,15 @@
428
428
  execCommand("outdent");
429
429
  }
430
430
  /**
431
- * G.5 helper: splits a checklist at checkLi, converts it to a <p>,
432
- * and keeps items before/after as separate checklists.
433
- * @param {HTMLElement} checkLi
431
+ * Convert a checklist <li> into a paragraph and move any following items into a new checklist.
432
+ *
433
+ * Preserves inline markup from the converted item, strips zero-width space anchors,
434
+ * and replaces empty content with a non‑breaking space. If there are list items
435
+ * after the converted item they are moved into a new <ul class="an-checklist">
436
+ * inserted immediately after the original list. The original <li> is removed and
437
+ * the original list is removed if it becomes empty. Attempts to place the caret
438
+ * at the start of the newly created <p>.
439
+ * @param {HTMLElement} checkLi - The checklist `<li>` element to convert to a `<p>`.
434
440
  */
435
441
  function _checklistItemToP(checkLi) {
436
442
  const checkUl = checkLi.closest(".an-checklist");
@@ -439,7 +445,15 @@
439
445
  const liIndex = allLis.indexOf(checkLi);
440
446
  const afterLis = allLis.slice(liIndex + 1);
441
447
  const p = document.createElement("p");
442
- p.textContent = Array.from(checkLi.childNodes).filter((n) => !(n.nodeType === 1 && n.tagName === "INPUT")).map((n) => n.textContent).join("").replace(/\u200B/g, "").trim() || "\xA0";
448
+ for (const child of checkLi.childNodes) {
449
+ if (child.nodeType === 1 && child.tagName === "INPUT") continue;
450
+ p.appendChild(child.cloneNode(true));
451
+ }
452
+ p.innerHTML = p.innerHTML.replace(/\u200B/g, "");
453
+ if (!p.hasChildNodes() || !p.textContent.trim()) {
454
+ p.innerHTML = "";
455
+ p.appendChild(document.createTextNode("\xA0"));
456
+ }
443
457
  if (afterLis.length > 0) {
444
458
  const newUl = document.createElement("ul");
445
459
  newUl.className = "an-checklist";
@@ -470,9 +484,12 @@
470
484
  */
471
485
  var insertOrderedList = () => execCommand("insertOrderedList");
472
486
  /**
473
- * Applies a line-height value to every block-level element that intersects
474
- * the current selection.
475
- * @param {string} value - unitless multiplier, e.g. '1.5'
487
+ * Set the line-height on every block-level element that intersects the current selection.
488
+ *
489
+ * If the selection is collapsed, the nearest enclosing block element receives the style.
490
+ * For a non-collapsed selection, all unique block ancestors of text nodes that intersect the range are updated;
491
+ * if none are found, the nearest block ancestor of the range's common ancestor is updated.
492
+ * @param {string} value - Line-height value to apply; typically a unitless multiplier (for example, "1.5").
476
493
  */
477
494
  function lineHeight(value) {
478
495
  const sel = window.getSelection();
@@ -507,9 +524,9 @@
507
524
  return;
508
525
  }
509
526
  const blocks = /* @__PURE__ */ new Set();
510
- const iter = document.createNodeIterator(range.commonAncestorContainer, NodeFilter.SHOW_TEXT, null);
527
+ const iter = document.createTreeWalker(range.commonAncestorContainer, NodeFilter.SHOW_TEXT, { acceptNode: (node) => range.intersectsNode(node) ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_SKIP });
511
528
  let textNode;
512
- while (textNode = iter.nextNode()) if (range.intersectsNode(textNode)) {
529
+ while (textNode = iter.nextNode()) {
513
530
  const block = nearestBlock(textNode);
514
531
  if (block) blocks.add(block);
515
532
  }
@@ -591,9 +608,18 @@
591
608
  return !!(code && !code.closest("pre"));
592
609
  }
593
610
  /**
594
- * Toggles a task-list at the cursor.
595
- * If inside a checklist <li>, converts it (and any other selected items) back to <p> elements.
596
- * Otherwise inserts a new <ul class="an-checklist"> with one item per selected line.
611
+ * Toggle a checklist at the current selection or caret.
612
+ *
613
+ * When the selection is inside an existing checklist `<ul class="an-checklist">`,
614
+ * converts the selected `<li>` items back into `<p>` paragraphs and places the caret
615
+ * at the start of the first converted paragraph. Otherwise creates a checklist:
616
+ * - If the selection is collapsed, converts the nearest block-level ancestor (or inserts
617
+ * a single checklist item at the editable root) into a checklist with one item containing
618
+ * that block's text and places the caret inside the new item.
619
+ * - If the selection is a range, converts each intersecting block element into one checklist
620
+ * item (preserving textual content) and places the caret at the end of the last item.
621
+ *
622
+ * Empty or whitespace-only selections do not create a checklist.
597
623
  */
598
624
  function toggleChecklist() {
599
625
  const sel = window.getSelection();
@@ -652,8 +678,9 @@
652
678
  ul.appendChild(li);
653
679
  if (block && BLOCK_TAGS.has(block.tagName)) block.parentNode.replaceChild(ul, block);
654
680
  else {
655
- document.execCommand("insertHTML", false, ul.outerHTML);
656
- return;
681
+ const nativeRange = sel.getRangeAt(0);
682
+ nativeRange.deleteContents();
683
+ nativeRange.insertNode(ul);
657
684
  }
658
685
  const textNode = li.lastChild;
659
686
  const nr = document.createRange();
@@ -776,6 +803,34 @@
776
803
  isDisabled
777
804
  };
778
805
  }
806
+ /**
807
+ * Global registry for custom buttons registered via AutumnNote.registerButton()
808
+ * or via a plugin's `buttons` array. Toolbar resolves string names from here.
809
+ * @type {Map<string, object>}
810
+ */
811
+ var _buttonRegistry = /* @__PURE__ */ new Map();
812
+ /**
813
+ * Registers a button definition in the global registry so it can be referenced
814
+ * by string name in toolbar configuration: `toolbar: [['myBtn', boldBtn]]`.
815
+ * @param {object} btnDef - Any ToolbarItemDef-compatible object with a `name` string.
816
+ */
817
+ function registerButton(btnDef) {
818
+ if (!btnDef || typeof btnDef.name !== "string") {
819
+ console.warn("[AutumnNote] registerButton: btnDef must have a string `name` property.");
820
+ return;
821
+ }
822
+ if (_buttonRegistry.has(btnDef.name)) console.warn(`[AutumnNote] registerButton: overwriting existing button "${btnDef.name}".`);
823
+ _buttonRegistry.set(btnDef.name, btnDef);
824
+ }
825
+ /**
826
+ * Looks up a button definition by name from the global registry.
827
+ * Returns undefined when not found.
828
+ * @param {string} name
829
+ * @returns {object|undefined}
830
+ */
831
+ function getButton(name) {
832
+ return _buttonRegistry.get(name);
833
+ }
779
834
  var boldBtn = btn("bold", "bold", "Bold (Ctrl+B)", () => bold(), () => document.queryCommandState("bold"));
780
835
  var italicBtn = btn("italic", "italic", "Italic (Ctrl+I)", () => italic(), () => document.queryCommandState("italic"));
781
836
  var underlineBtn = btn("underline", "underline", "Underline (Ctrl+U)", () => underline(), () => {
@@ -1332,6 +1387,7 @@
1332
1387
  replaceAriaLabel: "Replace with",
1333
1388
  replaceBtn: "Replace",
1334
1389
  replaceAllBtn: "Replace All",
1390
+ noResults: "No results",
1335
1391
  close: "×"
1336
1392
  },
1337
1393
  shortcutsDialog: {
@@ -1684,6 +1740,7 @@
1684
1740
  replaceAriaLabel: "Thay thế bằng",
1685
1741
  replaceBtn: "Thay thế",
1686
1742
  replaceAllBtn: "Thay thế tất cả",
1743
+ noResults: "Không có kết quả",
1687
1744
  close: "×"
1688
1745
  },
1689
1746
  shortcutsDialog: {
@@ -2013,6 +2070,7 @@
2013
2070
  replacePlaceholder: "置換後…",
2014
2071
  replaceAriaLabel: "置換後のテキスト",
2015
2072
  replaceBtn: "置換",
2073
+ noResults: "結果なし",
2016
2074
  replaceAllBtn: "すべて置換",
2017
2075
  close: "×"
2018
2076
  },
@@ -2343,6 +2401,7 @@
2343
2401
  replacePlaceholder: "替换为…",
2344
2402
  replaceAriaLabel: "替换为",
2345
2403
  replaceBtn: "替换",
2404
+ noResults: "没有结果",
2346
2405
  replaceAllBtn: "全部替换",
2347
2406
  close: "×"
2348
2407
  },
@@ -2673,6 +2732,7 @@
2673
2732
  replacePlaceholder: "Remplacer par…",
2674
2733
  replaceAriaLabel: "Remplacer par",
2675
2734
  replaceBtn: "Remplacer",
2735
+ noResults: "Aucun résultat",
2676
2736
  replaceAllBtn: "Tout remplacer",
2677
2737
  close: "×"
2678
2738
  },
@@ -3003,6 +3063,7 @@
3003
3063
  replacePlaceholder: "Ersetzen durch…",
3004
3064
  replaceAriaLabel: "Ersetzen durch",
3005
3065
  replaceBtn: "Ersetzen",
3066
+ noResults: "Keine Ergebnisse",
3006
3067
  replaceAllBtn: "Alle ersetzen",
3007
3068
  close: "×"
3008
3069
  },
@@ -3333,6 +3394,7 @@
3333
3394
  replacePlaceholder: "Reemplazar con…",
3334
3395
  replaceAriaLabel: "Reemplazar con",
3335
3396
  replaceBtn: "Reemplazar",
3397
+ noResults: "Sin resultados",
3336
3398
  replaceAllBtn: "Reemplazar todo",
3337
3399
  close: "×"
3338
3400
  },
@@ -3663,6 +3725,7 @@
3663
3725
  replacePlaceholder: "바꿀 내용…",
3664
3726
  replaceAriaLabel: "바꿀 내용",
3665
3727
  replaceBtn: "바꾸기",
3728
+ noResults: "결과 없음",
3666
3729
  replaceAllBtn: "모두 바꾸기",
3667
3730
  close: "×"
3668
3731
  },
@@ -3883,8 +3946,10 @@
3883
3946
  "object",
3884
3947
  "embed",
3885
3948
  "form",
3886
- "button"
3949
+ "base"
3887
3950
  ];
3951
+ /** Tags whose element wrapper is stripped but content (child nodes) is preserved. */
3952
+ var UNWRAP_TAGS = new Set(["button"]);
3888
3953
  /** Attributes whose values must be sanitised as URLs. */
3889
3954
  var URL_ATTRS = [
3890
3955
  "href",
@@ -3902,60 +3967,61 @@
3902
3967
  "player.vimeo.com"
3903
3968
  ]);
3904
3969
  /**
3905
- * Sanitises an HTML string by removing dangerous elements and attributes.
3906
- * Uses DOMParser so the sanitisation follows normal browser parsing rules —
3907
- * no regex shortcuts that can be bypassed by encoding tricks.
3970
+ * Produce a sanitized HTML string with dangerous elements and attributes removed.
3908
3971
  *
3909
- * - Strips PROHIBITED_TAGS (script, style, iframe, object, embed, form, button)
3910
- * - Allows input[type="checkbox"] only inside ul.an-checklist li; removes all other <input>
3911
- * - Removes all on* event-handler attributes
3912
- * - Rejects javascript: and vbscript: URLs in URL attributes
3913
- * - Rejects data: URIs everywhere except img[src] (base64 uploads)
3972
+ * Removes disallowed tags and wrappers, strips event-handler attributes, rejects
3973
+ * `javascript:`/`vbscript:` URLs and most `data:` URIs, restricts iframe `src`
3974
+ * to trusted hosts when enabled, and permits only checklist checkboxes as inputs.
3914
3975
  *
3915
- * @param {string} html
3916
- * @returns {string}
3976
+ * @param {string} html - HTML fragment to sanitize.
3977
+ * @param {Object} [options]
3978
+ * @param {boolean} [options.allowIframes=false] - If true, `iframe` elements are not removed but their `src` is restricted to trusted hosts and `srcdoc` is removed.
3979
+ * @returns {string} The sanitized HTML fragment.
3917
3980
  */
3918
3981
  function sanitiseHTML(html, { allowIframes = false } = {}) {
3919
3982
  const doc = new DOMParser().parseFromString(`<body>${html || ""}</body>`, "text/html");
3920
- (allowIframes ? PROHIBITED_TAGS.filter((t) => t !== "iframe") : PROHIBITED_TAGS).forEach((tag) => {
3921
- doc.querySelectorAll(tag).forEach((el) => el.remove());
3922
- });
3923
- doc.querySelectorAll("*").forEach((el) => {
3924
- Array.from(el.attributes).forEach((attr) => {
3983
+ const allElements = Array.from(doc.querySelectorAll("*"));
3984
+ const prohibited = new Set(allowIframes ? PROHIBITED_TAGS.filter((t) => t !== "iframe") : PROHIBITED_TAGS);
3985
+ for (const el of allElements) {
3986
+ const tag = el.tagName.toLowerCase();
3987
+ if (UNWRAP_TAGS.has(tag)) {
3988
+ el.replaceWith(...el.childNodes);
3989
+ continue;
3990
+ }
3991
+ if (prohibited.has(tag)) {
3992
+ el.remove();
3993
+ continue;
3994
+ }
3995
+ for (const attr of Array.from(el.attributes)) {
3925
3996
  if (attr.name.startsWith("on")) {
3926
3997
  el.removeAttribute(attr.name);
3927
- return;
3998
+ continue;
3928
3999
  }
3929
4000
  if (URL_ATTRS.includes(attr.name)) {
3930
4001
  const val = attr.value.trim();
3931
4002
  if (/^(javascript|vbscript):/i.test(val)) {
3932
4003
  el.removeAttribute(attr.name);
3933
- return;
4004
+ continue;
3934
4005
  }
3935
4006
  if (/^data:/i.test(val) && !(attr.name === "src" && el.tagName === "IMG")) el.removeAttribute(attr.name);
3936
4007
  }
3937
4008
  if (el.tagName === "IFRAME") {
3938
4009
  if (attr.name === "srcdoc") {
3939
4010
  el.removeAttribute(attr.name);
3940
- return;
3941
- }
3942
- if (attr.name === "src") {
3943
- if (!isTrustedIframeSrc(attr.value)) el.removeAttribute(attr.name);
3944
- return;
4011
+ continue;
3945
4012
  }
4013
+ if (attr.name === "src" && !isTrustedIframeSrc(attr.value)) el.removeAttribute(attr.name);
3946
4014
  }
3947
- });
3948
- });
3949
- doc.querySelectorAll("input").forEach((el) => {
3950
- if (!(el.closest("ul.an-checklist") !== null && el.closest("li") !== null) || el.getAttribute("type") !== "checkbox") el.remove();
3951
- else Array.from(el.attributes).forEach((attr) => {
3952
- if (![
4015
+ }
4016
+ if (tag === "input") {
4017
+ if (!(el.closest("ul.an-checklist") !== null && el.closest("li") !== null) || el.getAttribute("type") !== "checkbox") el.remove();
4018
+ else for (const attr of Array.from(el.attributes)) if (![
3953
4019
  "type",
3954
4020
  "checked",
3955
4021
  "contenteditable"
3956
4022
  ].includes(attr.name)) el.removeAttribute(attr.name);
3957
- });
3958
- });
4023
+ }
4024
+ }
3959
4025
  return doc.body.innerHTML;
3960
4026
  }
3961
4027
  /**
@@ -4156,7 +4222,18 @@
4156
4222
  const sel = window.getSelection();
4157
4223
  sel.removeAllRanges();
4158
4224
  sel.addRange(range);
4159
- } catch (_) {}
4225
+ } catch (_) {
4226
+ try {
4227
+ const fb = document.createRange();
4228
+ fb.setStart(this.editable, 0);
4229
+ fb.collapse(true);
4230
+ const s = window.getSelection();
4231
+ if (s) {
4232
+ s.removeAllRanges();
4233
+ s.addRange(fb);
4234
+ }
4235
+ } catch (_2) {}
4236
+ }
4160
4237
  }
4161
4238
  _savePoint() {
4162
4239
  if (this.stackOffset < this.stack.length - 1) this.stack = this.stack.slice(0, this.stackOffset + 1);
@@ -4183,6 +4260,10 @@
4183
4260
  * @returns {{ html: string, images: Object<string,string> }}
4184
4261
  */
4185
4262
  _tokenizeImages(html) {
4263
+ if (!html.includes("data:")) return {
4264
+ html,
4265
+ images: {}
4266
+ };
4186
4267
  const images = {};
4187
4268
  let index = 0;
4188
4269
  return {
@@ -4254,11 +4335,12 @@
4254
4335
  * Inspired by Summernote's table handling
4255
4336
  */
4256
4337
  /**
4257
- * Creates a table element with the specified dimensions.
4258
- * @param {number} cols
4259
- * @param {number} rows
4260
- * @param {{ headerRow?: boolean }} [opts]
4261
- * @returns {HTMLTableElement}
4338
+ * Build an HTML table with the given number of columns and rows, optionally including a header row.
4339
+ * @param {number} cols - Number of columns in each row.
4340
+ * @param {number} rows - Total number of rows to create (including header when `headerRow` is true).
4341
+ * @param {{ headerRow?: boolean }} [opts] - Options object.
4342
+ * @param {boolean} [opts.headerRow=false] - When true and `rows > 0`, creates a header row (`<thead>`) plus body rows for the remainder.
4343
+ * @returns {HTMLTableElement} The constructed `<table>` element with a `<tbody>` and optional `<thead>`; each cell contains a `<br>` placeholder.
4262
4344
  */
4263
4345
  function createTable(cols, rows, opts = {}) {
4264
4346
  const { headerRow = false } = opts;
@@ -4267,7 +4349,7 @@
4267
4349
  const thead = createElement("thead");
4268
4350
  const tr = createElement("tr");
4269
4351
  for (let c = 0; c < cols; c++) {
4270
- const th = createElement("th", {}, ["\xA0"]);
4352
+ const th = createElement("th", {}, [document.createElement("br")]);
4271
4353
  tr.appendChild(th);
4272
4354
  }
4273
4355
  thead.appendChild(tr);
@@ -4279,7 +4361,7 @@
4279
4361
  for (let r = 0; r < bodyRows; r++) {
4280
4362
  const tr = createElement("tr");
4281
4363
  for (let c = 0; c < cols; c++) {
4282
- const td = createElement("td", {}, ["\xA0"]);
4364
+ const td = createElement("td", {}, [document.createElement("br")]);
4283
4365
  tr.appendChild(td);
4284
4366
  }
4285
4367
  tbody.appendChild(tr);
@@ -4287,13 +4369,52 @@
4287
4369
  return table;
4288
4370
  }
4289
4371
  /**
4290
- * Inserts a table at the current cursor position.
4291
- * @param {number} cols
4292
- * @param {number} rows
4293
- * @param {{ headerRow?: boolean }} [opts]
4372
+ * Insert a table at the current selection and place the caret into its first cell.
4373
+ * @param {number} cols - Number of columns for the new table.
4374
+ * @param {number} rows - Number of rows for the new table.
4375
+ * @param {{ headerRow?: boolean }} [opts] - Options for table creation.
4376
+ * @param {boolean} [opts.headerRow=false] - If true, include a header row as the first row.
4294
4377
  */
4295
4378
  function insertTable(cols, rows, opts = {}) {
4296
- execCommand("insertHTML", createTable(cols, rows, opts).outerHTML);
4379
+ if (cols <= 0 || rows <= 0) return;
4380
+ const table = createTable(cols, rows, opts);
4381
+ const sel = window.getSelection();
4382
+ if (!sel || sel.rangeCount === 0) return;
4383
+ const range = sel.getRangeAt(0);
4384
+ range.deleteContents();
4385
+ const BLOCK = new Set([
4386
+ "P",
4387
+ "DIV",
4388
+ "H1",
4389
+ "H2",
4390
+ "H3",
4391
+ "H4",
4392
+ "H5",
4393
+ "H6",
4394
+ "BLOCKQUOTE",
4395
+ "LI",
4396
+ "PRE"
4397
+ ]);
4398
+ let anchor = range.startContainer;
4399
+ if (anchor.nodeType === 3) anchor = anchor.parentElement;
4400
+ while (anchor && !BLOCK.has(anchor.tagName?.toUpperCase()) && anchor.parentElement) anchor = anchor.parentElement;
4401
+ if (anchor && BLOCK.has(anchor.tagName?.toUpperCase()) && anchor.parentNode) {
4402
+ anchor.after(table);
4403
+ if (!table.nextElementSibling) {
4404
+ const p = document.createElement("p");
4405
+ p.appendChild(document.createElement("br"));
4406
+ table.after(p);
4407
+ }
4408
+ if (!anchor.textContent.trim() && !anchor.querySelector("img, video, table")) anchor.remove();
4409
+ } else range.insertNode(table);
4410
+ const firstCell = table.querySelector("td, th");
4411
+ if (firstCell) {
4412
+ const nr = document.createRange();
4413
+ nr.setStart(firstCell, 0);
4414
+ nr.collapse(true);
4415
+ sel.removeAllRanges();
4416
+ sel.addRange(nr);
4417
+ }
4297
4418
  }
4298
4419
  //#endregion
4299
4420
  //#region src/js/core/key.js
@@ -4508,7 +4629,7 @@
4508
4629
  if (para && para.nodeName.toUpperCase() === "PRE") {
4509
4630
  if (event.shiftKey) return false;
4510
4631
  event.preventDefault();
4511
- execCommand("insertText", " ");
4632
+ execCommand("insertText", " ".repeat(options.tabSize || 4));
4512
4633
  return true;
4513
4634
  }
4514
4635
  if (options.tabSize) {
@@ -4601,6 +4722,11 @@
4601
4722
  return true;
4602
4723
  }
4603
4724
  const para = closestPara(range.sc, editable);
4725
+ if (para && para.nodeName.toUpperCase() === "PRE") {
4726
+ event.preventDefault();
4727
+ execCommand("insertText", "\n");
4728
+ return true;
4729
+ }
4604
4730
  if (para && para.nodeName.toUpperCase() === "BLOCKQUOTE") {
4605
4731
  const native = range.toNativeRange();
4606
4732
  native.setEnd(para, para.childNodes.length);
@@ -4634,11 +4760,22 @@
4634
4760
  function htmlToMarkdown(html) {
4635
4761
  return _domToMd(new DOMParser().parseFromString(`<body>${html || ""}</body>`, "text/html").body).replace(/\n{3,}/g, "\n\n").trim();
4636
4762
  }
4637
- function _domToMd(node) {
4763
+ /**
4764
+ * Convert a DOM node subtree into Markdown.
4765
+ *
4766
+ * Recursively produces a Markdown string representing the given DOM node and its descendants,
4767
+ * handling common HTML constructs such as paragraphs, headings, lists (with nested indentation),
4768
+ * blockquotes, fenced and inline code, links, images, tables, horizontal rules, and basic inline emphasis.
4769
+ *
4770
+ * @param {Node} node - The DOM node to convert.
4771
+ * @param {number} [depth=0] - Current nesting depth used to indent nested list items.
4772
+ * @returns {string} The Markdown representation of the node subtree.
4773
+ */
4774
+ function _domToMd(node, depth = 0) {
4638
4775
  if (node.nodeType === 3) return node.textContent.replace(/\s+/g, " ");
4639
4776
  if (node.nodeType !== 1) return "";
4640
4777
  const tag = node.nodeName.toLowerCase();
4641
- const inner = () => Array.from(node.childNodes).map(_domToMd).join("");
4778
+ const inner = () => Array.from(node.childNodes).map((n) => _domToMd(n, depth)).join("");
4642
4779
  switch (tag) {
4643
4780
  case "p":
4644
4781
  case "div": return `\n\n${inner()}\n\n`;
@@ -4678,12 +4815,16 @@
4678
4815
  case "ul": {
4679
4816
  const items = Array.from(node.querySelectorAll(":scope > li"));
4680
4817
  if (!items.length) return inner();
4681
- return `\n\n${items.map((li) => `- ${_domToMd(li).trim()}`).join("\n")}\n\n`;
4818
+ const indent = " ".repeat(depth);
4819
+ const lines = items.map((li) => `${indent}- ${_domToMd(li, depth + 1).trim()}`).join("\n");
4820
+ return depth === 0 ? `\n\n${lines}\n\n` : `\n${lines}`;
4682
4821
  }
4683
4822
  case "ol": {
4684
4823
  const items = Array.from(node.querySelectorAll(":scope > li"));
4685
4824
  if (!items.length) return inner();
4686
- return `\n\n${items.map((li, i) => `${i + 1}. ${_domToMd(li).trim()}`).join("\n")}\n\n`;
4825
+ const indent = " ".repeat(depth);
4826
+ const lines = items.map((li, i) => `${indent}${i + 1}. ${_domToMd(li, depth + 1).trim()}`).join("\n");
4827
+ return depth === 0 ? `\n\n${lines}\n\n` : `\n${lines}`;
4687
4828
  }
4688
4829
  case "li": return inner();
4689
4830
  case "hr": return "\n\n---\n\n";
@@ -4707,12 +4848,15 @@
4707
4848
  }
4708
4849
  }
4709
4850
  /**
4710
- * Returns true if the text contains recognisable Markdown patterns.
4711
- * @param {string} text
4712
- * @returns {boolean}
4851
+ * Detects whether a string likely contains Markdown syntax.
4852
+ *
4853
+ * Checks for common Markdown constructs such as ATX headings, unordered or
4854
+ * ordered list items, blockquotes, fenced code blocks, and bold emphasis.
4855
+ * @param {string} text - Input text to inspect for Markdown patterns.
4856
+ * @returns {boolean} `true` if any Markdown-like pattern is present, `false` otherwise.
4713
4857
  */
4714
4858
  function isMarkdown(text) {
4715
- return /^#{1,6} \S|^\s*[-*+] \S|^\s*\d+\. \S|^> \S|\*{2}.+?\*{2}|^```/m.test(text);
4859
+ return /^#{1,6} \S|^\s*[-*+] \S|^\s*\d+\. \S|^> \S|^```|^\*{2}.+?\*{2}/m.test(text);
4716
4860
  }
4717
4861
  /**
4718
4862
  * Converts a Markdown string to an HTML string.
@@ -5386,6 +5530,8 @@
5386
5530
  * Toolbar.js - Builds and manages the editor toolbar UI
5387
5531
  * Inspired by Summernote's Toolbar module — rewritten without jQuery
5388
5532
  */
5533
+ /** Resolve a toolbar item: string → registry lookup, object → pass-through. */
5534
+ var _resolveBtn = (item) => typeof item === "string" ? getButton(item) : item;
5389
5535
  var _faPageLevelReady = null;
5390
5536
  var _S$1 = "stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"";
5391
5537
  var _svgWrap = (paths) => `<svg xmlns="http://www.w3.org/2000/svg" width="15" height="15" viewBox="0 0 24 24" fill="none" ${_S$1} style="display:block">${paths}</svg>`;
@@ -5475,15 +5621,19 @@
5475
5621
  this._disposers = [];
5476
5622
  /** @type {Array<() => void>} closers for all open color picker popups */
5477
5623
  this._colorPickerClosers = [];
5624
+ /** @type {number|null} rAF handle for debounced refresh */
5625
+ this._refreshRaf = null;
5478
5626
  }
5479
5627
  initialize() {
5480
5628
  this.el = createElement("div", { class: "an-toolbar" });
5481
5629
  this._faReady = this._detectFontAwesome();
5482
5630
  this._buildButtons();
5483
- this._btnMap = new Map((this.options.toolbar || []).flat().map((b) => [b.name, b]));
5631
+ this._btnMap = new Map((this.options.toolbar || []).flat().map(_resolveBtn).filter(Boolean).map((b) => [b.name, b]));
5484
5632
  return this;
5485
5633
  }
5486
5634
  destroy() {
5635
+ if (this._refreshRaf) cancelAnimationFrame(this._refreshRaf);
5636
+ this._refreshRaf = null;
5487
5637
  this._disposers.forEach((d) => d());
5488
5638
  this._disposers = [];
5489
5639
  if (this.el && this.el.parentNode) this.el.parentNode.removeChild(this.el);
@@ -5494,7 +5644,12 @@
5494
5644
  const fragment = document.createDocumentFragment();
5495
5645
  toolbar.forEach((group) => {
5496
5646
  const groupEl = createElement("div", { class: "an-btn-group" });
5497
- group.forEach((btnDef) => {
5647
+ group.forEach((item) => {
5648
+ const btnDef = _resolveBtn(item);
5649
+ if (!btnDef) {
5650
+ console.warn(`[AutumnNote] Toolbar: button "${item}" not found in registry. Skipped.`);
5651
+ return;
5652
+ }
5498
5653
  let el;
5499
5654
  if (btnDef.type === "select") el = this._createSelect(btnDef);
5500
5655
  else if (btnDef.type === "grid") el = this._createGridPicker(btnDef);
@@ -5879,6 +6034,13 @@
5879
6034
  return _faPageLevelReady;
5880
6035
  }
5881
6036
  refresh() {
6037
+ if (this._refreshRaf) cancelAnimationFrame(this._refreshRaf);
6038
+ this._refreshRaf = requestAnimationFrame(() => {
6039
+ this._refreshRaf = null;
6040
+ this._doRefresh();
6041
+ });
6042
+ }
6043
+ _doRefresh() {
5882
6044
  if (!this.el) return;
5883
6045
  const btnMap = this._btnMap || /* @__PURE__ */ new Map();
5884
6046
  this.el.querySelectorAll("button[data-btn]").forEach((btn) => {
@@ -5907,6 +6069,24 @@
5907
6069
  hide() {
5908
6070
  if (this.el) this.el.style.display = "none";
5909
6071
  }
6072
+ /**
6073
+ * Tears down and re-renders the toolbar in-place.
6074
+ * Call after registering new buttons post-create via context.use(plugin)
6075
+ * or AutumnNote.registerButton() to make them appear in the toolbar.
6076
+ */
6077
+ rebuild() {
6078
+ if (this._refreshRaf) {
6079
+ cancelAnimationFrame(this._refreshRaf);
6080
+ this._refreshRaf = null;
6081
+ }
6082
+ this._disposers.forEach((d) => d());
6083
+ this._disposers = [];
6084
+ if (this.el) this.el.innerHTML = "";
6085
+ this._faReady = this._detectFontAwesome();
6086
+ this._buildButtons();
6087
+ this._btnMap = new Map((this.options.toolbar || []).flat().map(_resolveBtn).filter(Boolean).map((b) => [b.name, b]));
6088
+ this.refresh();
6089
+ }
5910
6090
  };
5911
6091
  //#endregion
5912
6092
  //#region src/js/module/Statusbar.js
@@ -6056,7 +6236,7 @@
6056
6236
  }
6057
6237
  update() {
6058
6238
  if (!this._wordCountEl || !this._charCountEl) return;
6059
- const text = this.context.layoutInfo.editable.innerText || "";
6239
+ const text = this.context.layoutInfo.editable.textContent || "";
6060
6240
  const words = _countWords(text);
6061
6241
  const chars = text.replace(/\n/g, "").length;
6062
6242
  const maxWords = this.options.maxWords || 0;
@@ -6251,7 +6431,7 @@
6251
6431
  event.preventDefault();
6252
6432
  const raw = clipboardData.getData("text/html");
6253
6433
  const isWordContent = /<[a-z]+:[a-z]/i.test(raw) || /class="Mso/i.test(raw) || /\bmso-/i.test(raw);
6254
- const isSocialContent = /\bdata-testid\b/.test(raw) || /class="[^"]*\b(?:x[a-z0-9]{6,}|r-[a-z0-9]{3,})\b/.test(raw);
6434
+ const isSocialContent = /class="[^"]*\b(?:x[a-z0-9]{6,}|r-[a-z0-9]{3,})\b/.test(raw);
6255
6435
  let html = raw;
6256
6436
  if (isWordContent) html = this._cleanWordHtml(html);
6257
6437
  else if (isSocialContent) html = this._cleanSocialHtml(html);
@@ -6353,7 +6533,7 @@
6353
6533
  */
6354
6534
  _dataUrlToBlob(dataUrl) {
6355
6535
  const [header, b64] = dataUrl.split(",");
6356
- const mime = header.match(/:(.*?);/)[1];
6536
+ const mime = header.match(/:(.*?);/)?.[1] ?? "image/png";
6357
6537
  const binary = atob(b64);
6358
6538
  const arr = new Uint8Array(binary.length);
6359
6539
  for (let i = 0; i < binary.length; i++) arr[i] = binary.charCodeAt(i);
@@ -7359,6 +7539,7 @@
7359
7539
  _select(img) {
7360
7540
  if (this._activeImg && this._activeImg !== img) this._activeImg.classList.remove("an-image-selected");
7361
7541
  this._activeImg = img;
7542
+ this._lastOverlayPos = null;
7362
7543
  img.classList.add("an-image-selected");
7363
7544
  this._updateOverlayPosition();
7364
7545
  this._overlay.style.display = "block";
@@ -7382,6 +7563,14 @@
7382
7563
  const offsetParent = this._overlay.offsetParent || this._container;
7383
7564
  const containerRect = offsetParent.getBoundingClientRect();
7384
7565
  const rect = this._activeImg.getBoundingClientRect();
7566
+ const p = this._lastOverlayPos;
7567
+ if (p && p.l === rect.left && p.t === rect.top && p.w === rect.width && p.h === rect.height) return;
7568
+ this._lastOverlayPos = {
7569
+ l: rect.left,
7570
+ t: rect.top,
7571
+ w: rect.width,
7572
+ h: rect.height
7573
+ };
7385
7574
  const left = rect.left - containerRect.left + offsetParent.scrollLeft;
7386
7575
  const top = rect.top - containerRect.top + offsetParent.scrollTop;
7387
7576
  this._overlay.style.left = `${left}px`;
@@ -13080,7 +13269,7 @@
13080
13269
  cells.forEach((cell) => {
13081
13270
  cell.classList.toggle("active", +cell.dataset.row <= rows && +cell.dataset.col <= cols);
13082
13271
  });
13083
- labelEl.textContent = rows && cols ? `${cols} × ${rows}` : this.context.locale.contextMenu.table || "Insert Table";
13272
+ labelEl.textContent = rows && cols ? `${rows} × ${cols}` : this.context.locale.contextMenu.table || "Insert Table";
13084
13273
  };
13085
13274
  panel.appendChild(gridEl);
13086
13275
  panel.appendChild(labelEl);
@@ -13720,7 +13909,6 @@
13720
13909
  if (!editable) return;
13721
13910
  const rawMatches = this._findRawMatches(query, editable);
13722
13911
  if (rawMatches.length === 0) return;
13723
- this._currentIndex = 0;
13724
13912
  for (let i = rawMatches.length - 1; i >= 0; i--) {
13725
13913
  const { node, start, end } = rawMatches[i];
13726
13914
  try {
@@ -13737,6 +13925,7 @@
13737
13925
  }
13738
13926
  this._matches.reverse();
13739
13927
  this._matches = this._matches.filter((m) => m.mark);
13928
+ this._currentIndex = 0;
13740
13929
  if (this._matches.length === 0) return;
13741
13930
  if (this._matches[0] && this._matches[0].mark) {
13742
13931
  this._matches[0].mark.className = "an-highlight an-highlight-current";
@@ -13762,12 +13951,13 @@
13762
13951
  this._lastCaseSensitive = this._caseSensitive;
13763
13952
  }
13764
13953
  const re = this._queryRegex;
13954
+ const MAX_RESULTS = 500;
13765
13955
  const walker = document.createTreeWalker(root, 4);
13766
13956
  let node;
13767
- while (node = walker.nextNode()) {
13957
+ while ((node = walker.nextNode()) && results.length < MAX_RESULTS) {
13768
13958
  re.lastIndex = 0;
13769
13959
  let m;
13770
- while ((m = re.exec(node.textContent)) !== null) results.push({
13960
+ while ((m = re.exec(node.textContent)) !== null && results.length < MAX_RESULTS) results.push({
13771
13961
  node,
13772
13962
  start: m.index,
13773
13963
  end: m.index + m[0].length
@@ -13854,7 +14044,7 @@
13854
14044
  const total = this._matches.length;
13855
14045
  if (total === 0) {
13856
14046
  const query = this._findInput ? this._findInput.value : "";
13857
- this._counterEl.textContent = query ? "No results" : "";
14047
+ this._counterEl.textContent = query ? this.context.locale.findReplace.noResults : "";
13858
14048
  } else this._counterEl.textContent = `${this._currentIndex + 1} / ${total}`;
13859
14049
  }
13860
14050
  };
@@ -14053,17 +14243,33 @@
14053
14243
  e.preventDefault();
14054
14244
  e.stopPropagation();
14055
14245
  this._startHandleDrag(e, id);
14056
- }));
14246
+ }), on(h, "touchstart", (e) => {
14247
+ e.preventDefault();
14248
+ e.stopPropagation();
14249
+ this._startHandleDrag({
14250
+ clientX: e.touches[0].clientX,
14251
+ clientY: e.touches[0].clientY
14252
+ }, id);
14253
+ }, { passive: false }));
14057
14254
  this._handles[id] = h;
14058
14255
  cropBox.appendChild(h);
14059
14256
  });
14060
14257
  this._disposers.push(on(cropBox, "mousedown", (e) => {
14061
- if (e.target !== cropBox && e.target !== grid && !(e.target.tagName === "DIV" && !e.target.className.includes("handle"))) return;
14062
- if (e.target.className && e.target.className.includes("an-crop-handle")) return;
14258
+ if (e.target.classList.contains("an-crop-handle")) return;
14259
+ if (e.target !== cropBox && e.target !== grid) return;
14063
14260
  e.preventDefault();
14064
14261
  e.stopPropagation();
14065
14262
  this._startBoxMove(e);
14066
- }));
14263
+ }), on(cropBox, "touchstart", (e) => {
14264
+ if (e.target.classList.contains("an-crop-handle")) return;
14265
+ if (e.target !== cropBox && e.target !== grid) return;
14266
+ e.preventDefault();
14267
+ e.stopPropagation();
14268
+ this._startBoxMove({
14269
+ clientX: e.touches[0].clientX,
14270
+ clientY: e.touches[0].clientY
14271
+ });
14272
+ }, { passive: false }));
14067
14273
  const infoEl = document.createElement("div");
14068
14274
  infoEl.className = "an-crop-info";
14069
14275
  infoEl.style.cssText = `
@@ -14221,15 +14427,26 @@
14221
14427
  * @param {(e: MouseEvent) => void} onMove
14222
14428
  */
14223
14429
  _attachDocDrag(onMove) {
14430
+ const onTouchMove = (e) => {
14431
+ e.preventDefault();
14432
+ onMove({
14433
+ clientX: e.touches[0].clientX,
14434
+ clientY: e.touches[0].clientY
14435
+ });
14436
+ };
14224
14437
  const cleanup = () => {
14225
14438
  document.removeEventListener("mousemove", onMove);
14226
14439
  document.removeEventListener("mouseup", cleanup);
14440
+ document.removeEventListener("touchmove", onTouchMove);
14441
+ document.removeEventListener("touchend", cleanup);
14227
14442
  document.body.style.userSelect = "";
14228
14443
  document.body.style.cursor = "";
14229
14444
  };
14230
14445
  document.body.style.userSelect = "none";
14231
14446
  document.addEventListener("mousemove", onMove);
14232
14447
  document.addEventListener("mouseup", cleanup, { once: true });
14448
+ document.addEventListener("touchmove", onTouchMove, { passive: false });
14449
+ document.addEventListener("touchend", cleanup, { once: true });
14233
14450
  }
14234
14451
  _bindEsc() {
14235
14452
  const handler = (e) => {
@@ -14270,7 +14487,7 @@
14270
14487
  height: natH
14271
14488
  }, w, h);
14272
14489
  if (!canvas) {
14273
- window.alert("Cannot crop this image: the image server does not allow cross-origin access.\nUpload the image directly to use the crop tool.");
14490
+ this._showCropError("Cannot crop this image: the image server does not allow cross-origin access. Upload the image directly to use the crop tool.");
14274
14491
  this._close(false);
14275
14492
  return;
14276
14493
  }
@@ -14287,6 +14504,37 @@
14287
14504
  this.context.invoke("imageResizer.updateOverlay");
14288
14505
  }
14289
14506
  /**
14507
+ * Show a non-blocking inline error banner appended to document.body.
14508
+ * Auto-dismisses after 4 seconds.
14509
+ * @param {string} msg
14510
+ */
14511
+ _showCropError(msg) {
14512
+ const banner = document.createElement("div");
14513
+ banner.setAttribute("role", "alert");
14514
+ banner.style.cssText = [
14515
+ "position:fixed",
14516
+ "bottom:24px",
14517
+ "left:50%",
14518
+ "transform:translateX(-50%)",
14519
+ "z-index:10200",
14520
+ "max-width:420px",
14521
+ "width:max-content",
14522
+ "background:#7f1d1d",
14523
+ "color:#fecaca",
14524
+ "border:1px solid #b91c1c",
14525
+ "border-radius:8px",
14526
+ "padding:12px 18px",
14527
+ "font:13px/1.5 system-ui,sans-serif",
14528
+ "box-shadow:0 4px 16px rgba(0,0,0,.4)",
14529
+ "pointer-events:auto"
14530
+ ].join(";");
14531
+ banner.textContent = msg;
14532
+ document.body.appendChild(banner);
14533
+ setTimeout(() => {
14534
+ if (banner.parentNode) banner.parentNode.removeChild(banner);
14535
+ }, 4e3);
14536
+ }
14537
+ /**
14290
14538
  * Remove all overlay DOM elements and reset state.
14291
14539
  * @param {boolean} _committed - reserved for future use
14292
14540
  */
@@ -14701,6 +14949,16 @@
14701
14949
  if (!editable) return;
14702
14950
  editable.focus();
14703
14951
  document.execCommand("removeFormat");
14952
+ const sel = window.getSelection();
14953
+ if (sel && sel.rangeCount > 0 && !sel.getRangeAt(0).collapsed) {
14954
+ const range = sel.getRangeAt(0);
14955
+ const ancestor = range.commonAncestorContainer;
14956
+ const root = ancestor.nodeType === 1 ? ancestor : ancestor.parentElement;
14957
+ if (root) {
14958
+ const candidates = [root, ...root.querySelectorAll("[style]")];
14959
+ for (const el of candidates) if (el.hasAttribute("style") && range.intersectsNode(el)) el.removeAttribute("style");
14960
+ }
14961
+ }
14704
14962
  ctx.invoke("editor.afterCommand");
14705
14963
  },
14706
14964
  inlineCode: (ctx) => ctx.invoke("editor.inlineCode")
@@ -14814,6 +15072,7 @@
14814
15072
  }
14815
15073
  document.body.appendChild(el);
14816
15074
  this._el = el;
15075
+ this._btnCache = Array.from(el.querySelectorAll(".an-bubble-btn"));
14817
15076
  }
14818
15077
  _buildColorPicker() {
14819
15078
  const picker = document.createElement("div");
@@ -14899,8 +15158,13 @@
14899
15158
  editable.focus();
14900
15159
  const sel = window.getSelection();
14901
15160
  sel.removeAllRanges();
14902
- sel.addRange(this._savedRange.cloneRange());
14903
- document.execCommand(type, false, color);
15161
+ try {
15162
+ sel.addRange(this._savedRange.cloneRange());
15163
+ } catch (_) {
15164
+ return;
15165
+ }
15166
+ const cmd = type === "hiliteColor" ? "hiliteColor" : type;
15167
+ if (!document.execCommand(cmd, false, color) && cmd === "hiliteColor") document.execCommand("backColor", false, color);
14904
15168
  this.context.invoke("editor.afterCommand");
14905
15169
  const name = type === "hiliteColor" ? "hiliteColor" : "foreColor";
14906
15170
  const btn = this._el && this._el.querySelector(`[data-name="${name}"]`);
@@ -14935,8 +15199,8 @@
14935
15199
  this._closeColorPicker();
14936
15200
  }
14937
15201
  _syncActive() {
14938
- if (!this._el) return;
14939
- this._el.querySelectorAll(".an-bubble-btn").forEach((btn) => {
15202
+ if (!this._btnCache) return;
15203
+ this._btnCache.forEach((btn) => {
14940
15204
  const activeFn = _ACTIVE[btn.dataset.name];
14941
15205
  btn.classList.toggle("an-active", !!(activeFn && activeFn()));
14942
15206
  });
@@ -15083,14 +15347,23 @@
15083
15347
  const el = document.createElement("div");
15084
15348
  el.className = "an-mention-dropdown";
15085
15349
  el.setAttribute("role", "listbox");
15350
+ el.addEventListener("mousedown", (e) => e.preventDefault());
15351
+ el.addEventListener("click", (e) => {
15352
+ const item = e.target.closest(".an-mention-item");
15353
+ if (item) this._select(+item.dataset.index);
15354
+ });
15355
+ el.addEventListener("mousemove", (e) => {
15356
+ const item = e.target.closest(".an-mention-item");
15357
+ if (item) this._highlightItem(+item.dataset.index);
15358
+ });
15086
15359
  document.body.appendChild(el);
15087
15360
  this._dropdown = el;
15088
15361
  }
15089
15362
  _renderItems(items) {
15090
15363
  const dd = this._dropdown;
15091
- dd.innerHTML = "";
15092
15364
  this._items = items.slice(0, this._cfg.maxResults);
15093
15365
  this._activeIndex = this._items.length > 0 ? 0 : -1;
15366
+ const frag = document.createDocumentFragment();
15094
15367
  this._items.forEach((item, i) => {
15095
15368
  const li = document.createElement("div");
15096
15369
  li.className = "an-mention-item";
@@ -15106,10 +15379,10 @@
15106
15379
  const label = document.createElement("span");
15107
15380
  label.textContent = item.label;
15108
15381
  li.appendChild(label);
15109
- li.addEventListener("mousedown", (e) => e.preventDefault());
15110
- li.addEventListener("click", () => this._select(i));
15111
- dd.appendChild(li);
15382
+ frag.appendChild(li);
15112
15383
  });
15384
+ dd.innerHTML = "";
15385
+ dd.appendChild(frag);
15113
15386
  this._highlightItem(this._activeIndex);
15114
15387
  }
15115
15388
  _highlightItem(index) {
@@ -15261,6 +15534,8 @@
15261
15534
  */
15262
15535
  /** Module registry shared across all Context instances (populated via AutumnNote.registerModule). */
15263
15536
  var _customModules = /* @__PURE__ */ new Map();
15537
+ /** Global plugin registry (populated via AutumnNote.use()). Applied to every new Context. */
15538
+ var _globalPlugins = /* @__PURE__ */ new Map();
15264
15539
  var Context = class {
15265
15540
  /**
15266
15541
  * @param {HTMLElement} targetEl - The element to replace with the editor
@@ -15277,6 +15552,8 @@
15277
15552
  this._listeners = /* @__PURE__ */ new Map();
15278
15553
  /** @type {Map<string, object>} */
15279
15554
  this._modules = /* @__PURE__ */ new Map();
15555
+ /** @type {Map<string, { plugin: object, publicApi: * }>} */
15556
+ this._plugins = /* @__PURE__ */ new Map();
15280
15557
  this._disposers = [];
15281
15558
  this._alive = false;
15282
15559
  }
@@ -15299,6 +15576,7 @@
15299
15576
  if (this.options.focus) editable.focus();
15300
15577
  this._alive = true;
15301
15578
  this.invoke("toolbar.refresh");
15579
+ this._applyGlobalPlugins();
15302
15580
  if (typeof this.options.onInit === "function") this.options.onInit(this);
15303
15581
  return this;
15304
15582
  }
@@ -15350,6 +15628,46 @@
15350
15628
  this._modules.set(name, instance);
15351
15629
  return this;
15352
15630
  }
15631
+ /**
15632
+ * Installs a plugin on this editor instance.
15633
+ * If called after create(), buttons are registered immediately but the toolbar
15634
+ * must be rebuilt via ctx.invoke('toolbar.rebuild') to render new buttons.
15635
+ * @param {object} plugin - { name, version?, buttons?, install?, uninstall? }
15636
+ * @param {object} [options] - Forwarded to plugin.install(context, options)
15637
+ * @returns {this}
15638
+ */
15639
+ use(plugin, options = {}) {
15640
+ if (Array.isArray(plugin.buttons)) plugin.buttons.forEach((b) => registerButton(b));
15641
+ this._installPlugin(plugin, options);
15642
+ return this;
15643
+ }
15644
+ /**
15645
+ * Returns the public API returned by plugin.install(), or null.
15646
+ * @param {string} name
15647
+ * @returns {*}
15648
+ */
15649
+ getPlugin(name) {
15650
+ return this._plugins.get(name)?.publicApi ?? null;
15651
+ }
15652
+ _installPlugin(plugin, pluginOptions = {}) {
15653
+ const { name } = plugin;
15654
+ if (!name || typeof name !== "string") {
15655
+ console.warn("[AutumnNote] Plugin must have a string `name` property.");
15656
+ return;
15657
+ }
15658
+ if (this._plugins.has(name)) {
15659
+ console.warn(`[AutumnNote] Plugin "${name}" already installed on this instance. Skipping.`);
15660
+ return;
15661
+ }
15662
+ const publicApi = typeof plugin.install === "function" ? plugin.install(this, pluginOptions) ?? null : null;
15663
+ this._plugins.set(name, {
15664
+ plugin,
15665
+ publicApi
15666
+ });
15667
+ }
15668
+ _applyGlobalPlugins() {
15669
+ for (const { plugin, options } of _globalPlugins.values()) this._installPlugin(plugin, options);
15670
+ }
15353
15671
  _bindEditorEvents(editable) {
15354
15672
  const d0 = on(editable, "input", () => this._syncToTarget());
15355
15673
  const d1 = on(editable, "focus", () => {
@@ -15600,6 +15918,10 @@
15600
15918
  if (typeof module.destroy === "function") module.destroy();
15601
15919
  });
15602
15920
  this._modules.clear();
15921
+ for (const { plugin } of this._plugins.values()) if (typeof plugin.uninstall === "function") try {
15922
+ plugin.uninstall(this);
15923
+ } catch (_) {}
15924
+ this._plugins.clear();
15603
15925
  this._disposers.forEach((d) => d());
15604
15926
  this._disposers = [];
15605
15927
  const container = this.layoutInfo.container;
@@ -15668,7 +15990,27 @@
15668
15990
  registerModule(name, ModuleClass) {
15669
15991
  _customModules.set(name, ModuleClass);
15670
15992
  },
15671
- version: "1.1.1"
15993
+ use(plugin, options = {}) {
15994
+ if (!plugin || typeof plugin.name !== "string") throw new TypeError("[AutumnNote] AutumnNote.use: plugin must have a string `name` property.");
15995
+ if (_globalPlugins.has(plugin.name)) {
15996
+ console.warn(`[AutumnNote] Plugin "${plugin.name}" already registered globally. Skipping.`);
15997
+ return this;
15998
+ }
15999
+ if (Array.isArray(plugin.buttons)) plugin.buttons.forEach((b) => registerButton(b));
16000
+ _globalPlugins.set(plugin.name, {
16001
+ plugin,
16002
+ options
16003
+ });
16004
+ return this;
16005
+ },
16006
+ hasPlugin(name) {
16007
+ return _globalPlugins.has(name);
16008
+ },
16009
+ registerButton(btnDef) {
16010
+ registerButton(btnDef);
16011
+ return this;
16012
+ },
16013
+ version: "1.4.0"
15672
16014
  };
15673
16015
  /**
15674
16016
  * @param {string|Element|NodeList|Element[]} selector