autumnnote 1.10.0 → 1.11.1

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.
@@ -4643,7 +4643,10 @@ var PROHIBITED_TAGS = [
4643
4643
  "object",
4644
4644
  "embed",
4645
4645
  "form",
4646
- "base"
4646
+ "base",
4647
+ "template",
4648
+ "link",
4649
+ "meta"
4647
4650
  ];
4648
4651
  /** Tags whose element wrapper is stripped but content (child nodes) is preserved. */
4649
4652
  var UNWRAP_TAGS = new Set(["button"]);
@@ -4652,8 +4655,28 @@ var URL_ATTRS = [
4652
4655
  "href",
4653
4656
  "src",
4654
4657
  "action",
4655
- "formaction"
4658
+ "formaction",
4659
+ "xlink:href"
4656
4660
  ];
4661
+ /** Inline style properties the editor's own toolbar/table features persist on saved content. */
4662
+ var ALLOWED_STYLE_PROPS = new Set([
4663
+ "color",
4664
+ "background-color",
4665
+ "font-size",
4666
+ "line-height",
4667
+ "text-align",
4668
+ "vertical-align",
4669
+ "width",
4670
+ "min-width",
4671
+ "height",
4672
+ "min-height",
4673
+ "border-width",
4674
+ "border-style",
4675
+ "border-color",
4676
+ "padding"
4677
+ ]);
4678
+ /** Value patterns that are never safe regardless of property. */
4679
+ var DANGEROUS_STYLE_VALUE_RE = /url\s*\(|expression\s*\(|@import|javascript:|vbscript:|behavior\s*:|-moz-binding/i;
4657
4680
  /** Trusted hosts for iframe embeds when allowIframes is enabled. */
4658
4681
  var TRUSTED_IFRAME_HOSTS = new Set([
4659
4682
  "www.youtube.com",
@@ -4694,6 +4717,12 @@ function sanitiseHTML(html, { allowIframes = false } = {}) {
4694
4717
  el.removeAttribute(attr.name);
4695
4718
  continue;
4696
4719
  }
4720
+ if (attr.name === "style") {
4721
+ const cleaned = sanitiseStyleValue(attr.value);
4722
+ if (cleaned) el.setAttribute("style", cleaned);
4723
+ else el.removeAttribute("style");
4724
+ continue;
4725
+ }
4697
4726
  if (URL_ATTRS.includes(attr.name)) {
4698
4727
  const val = attr.value.trim();
4699
4728
  if (/^(javascript|vbscript):/i.test(val)) {
@@ -4722,6 +4751,28 @@ function sanitiseHTML(html, { allowIframes = false } = {}) {
4722
4751
  return doc.body.innerHTML;
4723
4752
  }
4724
4753
  /**
4754
+ * Filters a style attribute value down to an allowlisted set of CSS
4755
+ * properties (ALLOWED_STYLE_PROPS), dropping any declaration whose value
4756
+ * contains a dangerous construct — url(), expression(), an "import" rule,
4757
+ * javascript:/vbscript:, IE behavior/-moz-binding — regardless of property.
4758
+ * @param {string} value
4759
+ * @returns {string} The filtered declaration list, or '' if nothing survives.
4760
+ */
4761
+ function sanitiseStyleValue(value) {
4762
+ const kept = [];
4763
+ for (const decl of (value || "").split(";")) {
4764
+ const idx = decl.indexOf(":");
4765
+ if (idx === -1) continue;
4766
+ const prop = decl.slice(0, idx).trim().toLowerCase();
4767
+ const val = decl.slice(idx + 1).trim();
4768
+ if (!prop || !val) continue;
4769
+ if (!ALLOWED_STYLE_PROPS.has(prop)) continue;
4770
+ if (DANGEROUS_STYLE_VALUE_RE.test(val)) continue;
4771
+ kept.push(`${prop}: ${val}`);
4772
+ }
4773
+ return kept.join("; ");
4774
+ }
4775
+ /**
4725
4776
  * Returns true if iframe src points to an approved video host.
4726
4777
  * Relative, protocol-relative and invalid URLs are rejected.
4727
4778
  * @param {string} src
@@ -5502,6 +5553,17 @@ function htmlToMarkdown(html) {
5502
5553
  * @param {number} [depth=0] - Current nesting depth used to indent nested list items.
5503
5554
  * @returns {string} The Markdown representation of the node subtree.
5504
5555
  */
5556
+ /**
5557
+ * Direct child elements matching a tag name. Used instead of the CSS
5558
+ * `:scope > tag` combinator, which this project's jsdom version resolves
5559
+ * incorrectly (matches descendants at any depth, not just direct children).
5560
+ * @param {Element} el
5561
+ * @param {string} tagName
5562
+ * @returns {Element[]}
5563
+ */
5564
+ function _directChildren(el, tagName) {
5565
+ return Array.from(el.children).filter((c) => c.tagName === tagName.toUpperCase());
5566
+ }
5505
5567
  function _domToMd(node, depth = 0) {
5506
5568
  if (node.nodeType === 3) return node.textContent.replace(/\s+/g, " ");
5507
5569
  if (node.nodeType !== 1) return "";
@@ -5527,6 +5589,12 @@ function _domToMd(node, depth = 0) {
5527
5589
  case "strike": return `~~${inner()}~~`;
5528
5590
  case "sup": return `^${inner()}^`;
5529
5591
  case "sub": return `~${inner()}~`;
5592
+ case "u": return `<u>${inner()}</u>`;
5593
+ case "span": {
5594
+ const style = el.getAttribute("style") || "";
5595
+ if (/\b(color|background-color|font-size)\s*:/.test(style)) return `<span style="${_escAttr(style)}">${inner()}</span>`;
5596
+ return inner();
5597
+ }
5530
5598
  case "code":
5531
5599
  if (el.closest("pre")) return inner();
5532
5600
  return `\`${inner()}\``;
@@ -5535,7 +5603,10 @@ function _domToMd(node, depth = 0) {
5535
5603
  const langMatch = /language-(\S+)/.exec(codeEl?.className || "");
5536
5604
  return `\n\n\`\`\`${langMatch ? langMatch[1] : ""}\n${(codeEl || el).textContent || ""}\n\`\`\`\n\n`;
5537
5605
  }
5538
- case "blockquote": return `\n\n${inner().trim().split("\n").map((l) => `> ${l}`).join("\n")}\n\n`;
5606
+ case "blockquote": {
5607
+ const rawLines = inner().trim().split("\n");
5608
+ return `\n\n${rawLines.filter((l, idx) => l.trim() !== "" || (rawLines[idx - 1] ?? "").trim() !== "").map((l) => l.trim() === "" ? ">" : `> ${l}`).join("\n")}\n\n`;
5609
+ }
5539
5610
  case "a": {
5540
5611
  const href = el.getAttribute("href") || "";
5541
5612
  return `[${inner()}](${href})`;
@@ -5545,22 +5616,20 @@ function _domToMd(node, depth = 0) {
5545
5616
  return `![${el.getAttribute("alt") || ""}](${src})`;
5546
5617
  }
5547
5618
  case "ul": {
5548
- const items = Array.from(el.querySelectorAll(":scope > li"));
5619
+ const items = _directChildren(el, "li");
5549
5620
  if (!items.length) return inner();
5550
5621
  const indent = " ".repeat(depth);
5551
5622
  const isChecklist = el.classList.contains("an-checklist");
5552
5623
  const lines = items.map((li) => {
5624
+ const cb = _directChildren(li, "input").find((c) => c.getAttribute("type") === "checkbox");
5553
5625
  let prefix = "- ";
5554
- if (isChecklist) {
5555
- const cb = li.querySelector("input[type=\"checkbox\"]");
5556
- prefix = (cb ? cb.checked : false) ? "- [x] " : "- [ ] ";
5557
- }
5626
+ if (isChecklist || cb) prefix = (cb ? cb.checked : false) ? "- [x] " : "- [ ] ";
5558
5627
  return `${indent}${prefix}${_domToMd(li, depth + 1).trim()}`;
5559
5628
  }).join("\n");
5560
5629
  return depth === 0 ? `\n\n${lines}\n\n` : `\n${lines}`;
5561
5630
  }
5562
5631
  case "ol": {
5563
- const items = Array.from(el.querySelectorAll(":scope > li"));
5632
+ const items = _directChildren(el, "li");
5564
5633
  if (!items.length) return inner();
5565
5634
  const indent = " ".repeat(depth);
5566
5635
  const lines = items.map((li, i) => `${indent}${i + 1}. ${_domToMd(li, depth + 1).trim()}`).join("\n");
@@ -5569,19 +5638,22 @@ function _domToMd(node, depth = 0) {
5569
5638
  case "li": return inner();
5570
5639
  case "hr": return "\n\n---\n\n";
5571
5640
  case "table": {
5572
- const rows = Array.from(el.querySelectorAll("tr"));
5573
- if (!rows.length) return inner();
5574
- const cellTexts = rows.map((tr) => Array.from(tr.querySelectorAll("th, td")).map((c) => c.textContent.trim().replaceAll("|", String.raw`\|`)));
5641
+ const allRows = Array.from(el.querySelectorAll("tr"));
5642
+ if (!allRows.length) return inner();
5643
+ const firstRowIsHeader = !!_directChildren(el, "thead")[0] || allRows[0].children.length > 0 && Array.from(allRows[0].children).every((c) => c.tagName === "TH");
5644
+ const cellTexts = allRows.map((tr) => Array.from(tr.querySelectorAll("th, td")).map((c) => c.textContent.trim().replaceAll("|", String.raw`\|`)));
5575
5645
  const cols = Math.max(...cellTexts.map((r) => r.length));
5576
5646
  const padRow = (row) => {
5577
5647
  const r = [...row];
5578
5648
  while (r.length < cols) r.push("");
5579
5649
  return r;
5580
5650
  };
5651
+ const bodyStart = firstRowIsHeader ? 1 : 0;
5652
+ const headerCells = firstRowIsHeader ? padRow(cellTexts[0]) : new Array(cols).fill("");
5581
5653
  let md = "\n\n";
5582
- md += `| ${padRow(cellTexts[0]).join(" | ")} |\n`;
5654
+ md += `| ${headerCells.join(" | ")} |\n`;
5583
5655
  md += `| ${new Array(cols).fill("---").join(" | ")} |\n`;
5584
- for (let r = 1; r < cellTexts.length; r++) md += `| ${padRow(cellTexts[r]).join(" | ")} |\n`;
5656
+ for (let r = bodyStart; r < cellTexts.length; r++) md += `| ${padRow(cellTexts[r]).join(" | ")} |\n`;
5585
5657
  return md + "\n";
5586
5658
  }
5587
5659
  default: return inner();
@@ -5596,8 +5668,11 @@ function _domToMd(node, depth = 0) {
5596
5668
  * @returns {boolean} `true` if any Markdown-like pattern is present, `false` otherwise.
5597
5669
  */
5598
5670
  function isMarkdown(text) {
5599
- return /^#{1,6} [^\s]|^[ \t]*[-*+] [^\s]|^[ \t]*\d+\. [^\s]|^> [^\s]|^```|^\*{2}[^*\n]+\*{2}/m.test(text) || /^.+\n=+\s*$/m.test(text) || /^.+\n-{2,}\s*$/m.test(text) || /^---\s*\n(?:[\s\S]*?\n)?(?:---|\.\.\.)\s*(?:\n|$)/.test(text) || /^\|.+\|[ \t]*\n\|[ \t:|-]+\|/m.test(text);
5671
+ return /^#{1,6} [^\s]|^[ \t]*[-*+] [^\s]|^[ \t]*\d+\. [^\s]|^> ?[^\s]|^```|^\*{2}[^*\n]+\*{2}/m.test(text) || /^.+\n=+\s*$/m.test(text) || /^.+\n-{2,}\s*$/m.test(text) || /^---\s*\n(?:[\s\S]*?\n)?(?:---|\.\.\.)\s*(?:\n|$)/.test(text) || /^\|.+\|[ \t]*\n\|[ \t:|-]+\|/m.test(text);
5600
5672
  }
5673
+ var BQ_RE = /^ {0,3}>( ?)(.*)$/;
5674
+ var HR_RE = /^ {0,3}([-*_])( *\1){2,}\s*$/;
5675
+ var HARD_BREAK = String.fromCharCode(1);
5601
5676
  /**
5602
5677
  * Converts a Markdown string to an HTML string.
5603
5678
  * @param {string} text
@@ -5610,6 +5685,16 @@ function markdownToHTML(text) {
5610
5685
  lines = refs.clean;
5611
5686
  _linkDefs = refs.linkDefs;
5612
5687
  _footnoteIds = refs.footnoteIds;
5688
+ return _parseBlocks(lines);
5689
+ }
5690
+ /**
5691
+ * Parses a line array into block-level HTML. Called recursively for content
5692
+ * nested inside a blockquote so nested quotes and block content (lists,
5693
+ * headings, etc.) inside `>` are parsed the same as top-level content.
5694
+ * @param {string[]} lines
5695
+ * @returns {string}
5696
+ */
5697
+ function _parseBlocks(lines) {
5613
5698
  const out = [];
5614
5699
  let i = 0;
5615
5700
  while (i < lines.length) {
@@ -5628,7 +5713,7 @@ function markdownToHTML(text) {
5628
5713
  i++;
5629
5714
  continue;
5630
5715
  }
5631
- if (line.trim() && !/^(-{3,}|\*{3,}|_{3,})\s*$/.test(line) && !/^#{1,6} /.test(line) && i + 1 < lines.length) {
5716
+ if (line.trim() && !HR_RE.test(line) && !/^#{1,6} /.test(line) && i + 1 < lines.length) {
5632
5717
  if (/^=+\s*$/.test(lines[i + 1])) {
5633
5718
  out.push(`<h1>${_inline(line.trim())}</h1>`);
5634
5719
  i += 2;
@@ -5640,7 +5725,7 @@ function markdownToHTML(text) {
5640
5725
  continue;
5641
5726
  }
5642
5727
  }
5643
- if (/^(-{3,}|\*{3,}|_{3,})\s*$/.test(line)) {
5728
+ if (HR_RE.test(line)) {
5644
5729
  out.push("<hr>");
5645
5730
  i++;
5646
5731
  continue;
@@ -5648,17 +5733,18 @@ function markdownToHTML(text) {
5648
5733
  const hMatch = /^(#{1,6})\s+(.+)$/.exec(line);
5649
5734
  if (hMatch) {
5650
5735
  const level = hMatch[1].length;
5651
- out.push(`<h${level}>${_inline(hMatch[2])}</h${level}>`);
5736
+ const content = hMatch[2].replace(/(?:^|\s)#+\s*$/, "");
5737
+ out.push(`<h${level}>${_inline(content)}</h${level}>`);
5652
5738
  i++;
5653
5739
  continue;
5654
5740
  }
5655
- if (line.startsWith("> ")) {
5741
+ if (BQ_RE.test(line)) {
5656
5742
  const bqLines = [];
5657
- while (i < lines.length && lines[i].startsWith("> ")) {
5658
- bqLines.push(lines[i].slice(2));
5743
+ while (i < lines.length && BQ_RE.test(lines[i])) {
5744
+ bqLines.push(BQ_RE.exec(lines[i])[2]);
5659
5745
  i++;
5660
5746
  }
5661
- out.push(`<blockquote>${bqLines.map(_inline).join("<br>")}</blockquote>`);
5747
+ out.push(`<blockquote>${_parseBlocks(bqLines)}</blockquote>`);
5662
5748
  continue;
5663
5749
  }
5664
5750
  if (/^[-*+] /.test(line)) {
@@ -5701,11 +5787,11 @@ function markdownToHTML(text) {
5701
5787
  continue;
5702
5788
  }
5703
5789
  const paraLines = [];
5704
- while (i < lines.length && lines[i].trim() !== "" && !/^(#{1,6} |> |[-*+] |\d+\. |```|---\s*$|\*{3}\s*$|_{3}\s*$)/.test(lines[i]) && !/^\|.+\|/.test(lines[i]) && !(i + 1 < lines.length && /^=+\s*$/.test(lines[i + 1])) && !(i + 1 < lines.length && /^-{2,}\s*$/.test(lines[i + 1]))) {
5790
+ while (i < lines.length && lines[i].trim() !== "" && !/^(#{1,6} |[-*+] |\d+\. |```)/.test(lines[i]) && !BQ_RE.test(lines[i]) && !HR_RE.test(lines[i]) && !/^\|.+\|/.test(lines[i]) && !(i + 1 < lines.length && /^=+\s*$/.test(lines[i + 1])) && !(i + 1 < lines.length && /^-{2,}\s*$/.test(lines[i + 1]))) {
5705
5791
  paraLines.push(lines[i]);
5706
5792
  i++;
5707
5793
  }
5708
- if (paraLines.length) out.push(`<p>${_inline(paraLines.join(" "))}</p>`);
5794
+ if (paraLines.length) out.push(`<p>${_inline(_joinParagraphLines(paraLines)).replaceAll(HARD_BREAK, "<br>")}</p>`);
5709
5795
  }
5710
5796
  return out.join("");
5711
5797
  }
@@ -5780,23 +5866,77 @@ function _extractReferenceDefinitions(lines) {
5780
5866
  };
5781
5867
  }
5782
5868
  /**
5783
- * Splits a GFM table row string into trimmed cell strings.
5784
- * '| a | b | c |' ['a', 'b', 'c']
5869
+ * Joins a paragraph's source lines into one string, converting CommonMark
5870
+ * hard-break markers (a trailing backslash, or 2+ trailing spaces) on all
5871
+ * but the last line into a HARD_BREAK placeholder instead of a plain space.
5872
+ * @param {string[]} paraLines
5873
+ * @returns {string}
5874
+ */
5875
+ function _joinParagraphLines(paraLines) {
5876
+ let joined = "";
5877
+ for (let idx = 0; idx < paraLines.length; idx++) {
5878
+ const isLast = idx === paraLines.length - 1;
5879
+ const ln = paraLines[idx];
5880
+ if (!isLast && /\\$/.test(ln)) {
5881
+ joined += ln.replace(/\\$/, "") + HARD_BREAK;
5882
+ continue;
5883
+ }
5884
+ if (!isLast && / {2,}$/.test(ln)) {
5885
+ joined += ln.replace(/ {2,}$/, "") + HARD_BREAK;
5886
+ continue;
5887
+ }
5888
+ joined += ln + (isLast ? "" : " ");
5889
+ }
5890
+ return joined;
5891
+ }
5892
+ /**
5893
+ * Splits a GFM table row string into trimmed cell strings, treating an
5894
+ * escaped pipe (`\|`) as a literal character rather than a cell separator.
5895
+ * '| a | b | c |' → ['a', 'b', 'c']; '| a\|b | c |' → ['a|b', 'c']
5785
5896
  * @param {string} row
5786
5897
  * @returns {string[]}
5787
5898
  */
5788
5899
  function _parseTableRow(row) {
5789
- return row.replace(/^\|/, "").replace(/\|$/, "").split("|").map((c) => c.trim());
5900
+ const trimmed = row.replace(/^\|/, "").replace(/\|$/, "");
5901
+ const cells = [];
5902
+ let cur = "";
5903
+ for (let i = 0; i < trimmed.length; i++) {
5904
+ if (trimmed[i] === "\\" && trimmed[i + 1] === "|") {
5905
+ cur += "|";
5906
+ i++;
5907
+ continue;
5908
+ }
5909
+ if (trimmed[i] === "|") {
5910
+ cells.push(cur);
5911
+ cur = "";
5912
+ continue;
5913
+ }
5914
+ cur += trimmed[i];
5915
+ }
5916
+ cells.push(cur);
5917
+ return cells.map((c) => c.trim());
5790
5918
  }
5791
5919
  function _parseListBlock(lines, startIdx) {
5792
5920
  const baseIndent = lines[startIdx].match(/^(\s*)/)[1].length;
5793
5921
  const isOL = /^\s*\d+\. /.test(lines[startIdx]);
5794
5922
  const items = [];
5795
5923
  let firstIsCB = null;
5924
+ let loose = false;
5925
+ let pendingBlank = false;
5796
5926
  let i = startIdx;
5797
5927
  while (i < lines.length) {
5798
5928
  const line = lines[i];
5799
- if (line.trim() === "") break;
5929
+ if (line.trim() === "") {
5930
+ const next = lines[i + 1];
5931
+ const nextIndent = next !== void 0 ? next.match(/^(\s*)/)[1].length : -1;
5932
+ const nextIsSameItem = next !== void 0 && /^\s*(?:[-*+]|\d+\.) /.test(next) && /^\s*\d+\. /.test(next) === isOL && nextIndent === baseIndent;
5933
+ const nextIsContinuation = next !== void 0 && next.trim() !== "" && nextIndent > baseIndent;
5934
+ if (!items.length || !nextIsSameItem && !nextIsContinuation) break;
5935
+ loose = true;
5936
+ pendingBlank = true;
5937
+ i++;
5938
+ continue;
5939
+ }
5800
5940
  const indent = line.match(/^(\s*)/)[1].length;
5801
5941
  if (indent < baseIndent) break;
5802
5942
  if (indent === baseIndent) {
@@ -5809,11 +5949,12 @@ function _parseListBlock(lines, startIdx) {
5809
5949
  const checked = isCB && raw[1].toLowerCase() === "x";
5810
5950
  const text = isCB ? raw.replace(/^\[[ xX]\]\s+/, "") : raw;
5811
5951
  items.push({
5812
- text,
5952
+ paras: [text],
5813
5953
  isCB,
5814
5954
  checked,
5815
5955
  sub: ""
5816
5956
  });
5957
+ pendingBlank = false;
5817
5958
  i++;
5818
5959
  } else {
5819
5960
  if (!items.length) {
@@ -5824,53 +5965,143 @@ function _parseListBlock(lines, startIdx) {
5824
5965
  const nested = _parseListBlock(lines, i);
5825
5966
  items[items.length - 1].sub += nested.html;
5826
5967
  i = nested.endIdx;
5968
+ pendingBlank = false;
5969
+ } else if (pendingBlank) {
5970
+ items[items.length - 1].paras.push(line.trim());
5971
+ pendingBlank = false;
5972
+ i++;
5827
5973
  } else {
5828
- items[items.length - 1].text += " " + line.trim();
5974
+ const paras = items[items.length - 1].paras;
5975
+ paras[paras.length - 1] += " " + line.trim();
5829
5976
  i++;
5830
5977
  }
5831
5978
  }
5832
5979
  }
5833
- const open = isOL ? "<ol>" : !isOL && firstIsCB === true ? "<ul class=\"an-checklist\">" : "<ul>";
5980
+ const hasCB = !isOL && firstIsCB === true;
5981
+ const startMatch = isOL ? /^\s*(\d+)\. /.exec(lines[startIdx]) : null;
5982
+ const startNum = startMatch ? Number.parseInt(startMatch[1], 10) : 1;
5983
+ const open = isOL ? startNum !== 1 ? `<ol start="${startNum}">` : "<ol>" : hasCB ? "<ul class=\"an-checklist\">" : "<ul>";
5834
5984
  const close = isOL ? "</ol>" : "</ul>";
5835
5985
  return {
5836
- html: `${open}${items.map(({ text, isCB, checked, sub }) => {
5837
- return `<li>${isCB ? `<input type="checkbox" contenteditable="false"${checked ? " checked" : ""}>` : ""}${_inline(text)}${sub}</li>`;
5986
+ html: `${open}${items.map(({ paras, isCB, checked, sub }) => {
5987
+ const cbHTML = isCB ? `<input type="checkbox" contenteditable="false"${checked ? " checked" : ""}>` : "";
5988
+ return `<li>${loose ? paras.map((p, idx) => `<p>${idx === 0 ? cbHTML : ""}${_inline(p)}</p>`).join("") : `${cbHTML}${_inline(paras[0])}`}${sub}</li>`;
5838
5989
  }).join("")}${close}`,
5839
5990
  endIdx: i
5840
5991
  };
5841
5992
  }
5842
- function _inline(text) {
5843
- text = text.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, (_, alt, src) => `<img src="${_escAttr(src)}" alt="${_escAttr(alt)}" class="an-image">`);
5844
- text = text.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_, label, href) => `<a href="${_escAttr(href)}">${_esc(label)}</a>`);
5993
+ var ESCAPABLE_RE = /\\([*_`#[\]()>\\~|])/g;
5994
+ var MARK = String.fromCharCode(0);
5995
+ /**
5996
+ * Step 0 of _inline(): replaces backslash-escaped punctuation with inert
5997
+ * placeholders so later syntax regexes can't match them.
5998
+ * @param {string} text
5999
+ * @returns {{ text: string, literals: string[] }}
6000
+ */
6001
+ function _extractBackslashEscapes(text) {
6002
+ const literals = [];
6003
+ return {
6004
+ text: text.replace(ESCAPABLE_RE, (_, ch) => {
6005
+ literals.push(ch);
6006
+ return `${MARK}${literals.length - 1}${MARK}`;
6007
+ }),
6008
+ literals
6009
+ };
6010
+ }
6011
+ /**
6012
+ * Restores placeholders from _extractBackslashEscapes(), HTML-escaping each
6013
+ * literal since it's inserted directly into the output.
6014
+ * @param {string} text
6015
+ * @param {string[]} literals
6016
+ * @returns {string}
6017
+ */
6018
+ function _restoreBackslashEscapes(text, literals) {
6019
+ return text.replace(new RegExp(`${MARK}(\\d+)${MARK}`, "g"), (_, idx) => _esc(literals[Number(idx)]));
6020
+ }
6021
+ /**
6022
+ * Resolves images, inline links, GFM reference-style links (explicit,
6023
+ * shortcut, and bare/implicit forms), and footnote markers. Must run on text
6024
+ * already passed through _esc() — see _inline()'s Step 1 comment.
6025
+ * @param {string} text
6026
+ * @returns {string}
6027
+ */
6028
+ function _resolveLinksAndFootnotes(text) {
6029
+ text = text.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, (_, alt, src) => `<img src="${_escAttrQuotes(src)}" alt="${_escAttrQuotes(alt)}" class="an-image">`);
6030
+ text = text.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_, label, href) => `<a href="${_escAttrQuotes(href)}">${label}</a>`);
5845
6031
  text = text.replace(/\[([^\]]+)\]\[([^\]]*)\]/g, (m, label, ref) => {
5846
- const def = _linkDefs.get((ref || label).trim().toLowerCase());
6032
+ const def = _linkDefs.get(_unescAmpLtGt(ref || label).trim().toLowerCase());
5847
6033
  if (!def) return m;
5848
6034
  const titleAttr = def.title ? ` title="${_escAttr(def.title)}"` : "";
5849
- return `<a href="${_escAttr(def.href)}"${titleAttr}>${_esc(label)}</a>`;
6035
+ return `<a href="${_escAttr(def.href)}"${titleAttr}>${label}</a>`;
5850
6036
  });
5851
6037
  text = text.replace(/\[([^\]]+)\]/g, (m, label) => {
5852
- const def = _linkDefs.get(label.trim().toLowerCase());
6038
+ const def = _linkDefs.get(_unescAmpLtGt(label).trim().toLowerCase());
5853
6039
  if (!def) return m;
5854
6040
  const titleAttr = def.title ? ` title="${_escAttr(def.title)}"` : "";
5855
- return `<a href="${_escAttr(def.href)}"${titleAttr}>${_esc(label)}</a>`;
6041
+ return `<a href="${_escAttr(def.href)}"${titleAttr}>${label}</a>`;
5856
6042
  });
5857
- text = text.replace(/\[\^([^\]]+)\]/g, (m, id) => _footnoteIds.has(id) ? `<sup>[${_esc(id)}]</sup>` : m);
5858
- text = text.replace(/\*{3}([^*\n]+?)\*{3}/g, (_, c) => `<strong><em>${_esc(c)}</em></strong>`);
5859
- text = text.replace(/_{3}([^_\n]+?)_{3}/g, (_, c) => `<strong><em>${_esc(c)}</em></strong>`);
5860
- text = text.replace(/\*{2}([^*\n]+?)\*{2}/g, (_, c) => `<strong>${_esc(c)}</strong>`);
5861
- text = text.replace(/_{2}([^_\n]+?)_{2}/g, (_, c) => `<strong>${_esc(c)}</strong>`);
5862
- text = text.replace(/\*([^*\n]+?)\*/g, (_, c) => `<em>${_esc(c)}</em>`);
5863
- text = text.replace(/_([^_\n]+?)_/g, (_, c) => `<em>${_esc(c)}</em>`);
5864
- text = text.replace(/~~([^~\n]+?)~~/g, (_, c) => `<del>${_esc(c)}</del>`);
5865
- text = text.replace(/`([^`]+)`/g, (_, c) => `<code>${_esc(c)}</code>`);
6043
+ text = text.replace(/\[\^([^\]]+)\]/g, (m, id) => _footnoteIds.has(_unescAmpLtGt(id)) ? `<sup>[${id}]</sup>` : m);
5866
6044
  return text;
5867
6045
  }
6046
+ /**
6047
+ * Converts angle-bracket (`<https://...>`) and bare (`https://...`)
6048
+ * autolinks. Runs after _resolveLinksAndFootnotes() so an already-linked URL
6049
+ * isn't reprocessed, and on already-_esc()'d text (see _inline()).
6050
+ * @param {string} text
6051
+ * @returns {string}
6052
+ */
6053
+ function _applyAutolinks(text) {
6054
+ text = text.replace(/&lt;(https?:\/\/[^\s&]+?)&gt;/g, (_, url) => `<a href="${_escAttrQuotes(url)}">${url}</a>`);
6055
+ text = text.replace(/(^|[\s(])(https?:\/\/[^\s()]+)/g, (m, pre, rawUrl) => {
6056
+ const trail = /[.,;:!?)]+$/.exec(rawUrl);
6057
+ const url = trail ? rawUrl.slice(0, -trail[0].length) : rawUrl;
6058
+ if (!url) return m;
6059
+ const suffix = trail ? trail[0] : "";
6060
+ return `${pre}<a href="${_escAttrQuotes(url)}">${url}</a>${suffix}`;
6061
+ });
6062
+ return text;
6063
+ }
6064
+ /**
6065
+ * Applies bold/italic/bold-italic (asterisk and underscore forms — underscore
6066
+ * requires a non-word-character boundary per CommonMark), strikethrough, and
6067
+ * inline code.
6068
+ * @param {string} text
6069
+ * @returns {string}
6070
+ */
6071
+ function _applyEmphasisAndCode(text) {
6072
+ text = text.replace(/\*{3}([^*\n]+?)\*{3}/g, (_, c) => `<strong><em>${c}</em></strong>`);
6073
+ text = text.replace(/(?<!\w)_{3}([^_\n]+?)_{3}(?!\w)/g, (_, c) => `<strong><em>${c}</em></strong>`);
6074
+ text = text.replace(/\*{2}([^*\n]+?)\*{2}/g, (_, c) => `<strong>${c}</strong>`);
6075
+ text = text.replace(/(?<!\w)_{2}([^_\n]+?)_{2}(?!\w)/g, (_, c) => `<strong>${c}</strong>`);
6076
+ text = text.replace(/\*([^*\n]+?)\*/g, (_, c) => `<em>${c}</em>`);
6077
+ text = text.replace(/(?<!\w)_([^_\n]+?)_(?!\w)/g, (_, c) => `<em>${c}</em>`);
6078
+ text = text.replace(/~~([^~\n]+?)~~/g, (_, c) => `<del>${c}</del>`);
6079
+ text = text.replace(/``([\s\S]*?)``/g, (_, c) => `<code>${c}</code>`);
6080
+ text = text.replace(/`([^`]+)`/g, (_, c) => `<code>${c}</code>`);
6081
+ return text;
6082
+ }
6083
+ function _inline(text) {
6084
+ const { text: withoutEscapes, literals } = _extractBackslashEscapes(text);
6085
+ let result = _esc(withoutEscapes);
6086
+ result = _resolveLinksAndFootnotes(result);
6087
+ result = _applyAutolinks(result);
6088
+ result = _applyEmphasisAndCode(result);
6089
+ return _restoreBackslashEscapes(result, literals);
6090
+ }
5868
6091
  function _esc(v) {
5869
6092
  return String(v).replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
5870
6093
  }
5871
6094
  function _escAttr(v) {
5872
6095
  return String(v).replaceAll("&", "&amp;").replaceAll("\"", "&quot;").replaceAll("'", "&#39;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
5873
6096
  }
6097
+ /** Escapes only quote characters — for attribute values already run through _esc(). */
6098
+ function _escAttrQuotes(v) {
6099
+ return String(v).replaceAll("\"", "&quot;").replaceAll("'", "&#39;");
6100
+ }
6101
+ /** Reverses _esc()'s &amp;/&lt;/&gt; substitutions, for matching against un-escaped _linkDefs/_footnoteIds keys. */
6102
+ function _unescAmpLtGt(v) {
6103
+ return String(v).replaceAll("&lt;", "<").replaceAll("&gt;", ">").replaceAll("&amp;", "&");
6104
+ }
5874
6105
  //#endregion
5875
6106
  //#region src/js/core/detectLang.js
5876
6107
  /**
@@ -7426,6 +7657,17 @@ var Clipboard = class {
7426
7657
  return doc.body.innerHTML;
7427
7658
  }
7428
7659
  /**
7660
+ * Checks whether an HTML payload has no semantic markup beyond plain
7661
+ * wrapper elements (e.g. a bare <div>/<p>). Used to decide whether a
7662
+ * markdown-shaped plain-text paste should win over an accompanying HTML
7663
+ * payload that isn't actually carrying any real rich-text formatting.
7664
+ * @param {string} html
7665
+ * @returns {boolean}
7666
+ */
7667
+ _isTriviallyPlainHtml(html) {
7668
+ return !new DOMParser().parseFromString(`<body>${html}</body>`, "text/html").body.querySelector("a,img,table,ul,ol,li,blockquote,pre,code,h1,h2,h3,h4,h5,h6,strong,b,em,i,u,s,del,strike,hr,br");
7669
+ }
7670
+ /**
7429
7671
  * Forces the next paste operation to strip all HTML formatting.
7430
7672
  * Called by Editor when Ctrl+Shift+V is pressed.
7431
7673
  * @param {boolean} val
@@ -7442,8 +7684,16 @@ var Clipboard = class {
7442
7684
  if (maxBytes > 0) {
7443
7685
  const text = clipboardData.getData("text/plain") || "";
7444
7686
  const html = clipboardData.getData("text/html") || "";
7445
- if (Math.max(text.length, html.length) > maxBytes) {
7687
+ const size = Math.max(text.length, html.length);
7688
+ if (size > maxBytes) {
7446
7689
  event.preventDefault();
7690
+ const message = `Pasted content (${size} bytes) exceeds the ${this.options.maxPasteSize ?? 5} MB paste size limit.`;
7691
+ this.context.triggerEvent("pasteError", {
7692
+ size,
7693
+ maxBytes,
7694
+ message
7695
+ });
7696
+ console.warn(`[AutumnNote] ${message}`);
7447
7697
  return;
7448
7698
  }
7449
7699
  }
@@ -7466,9 +7716,12 @@ var Clipboard = class {
7466
7716
  this.context.invoke("editor.afterCommand");
7467
7717
  return;
7468
7718
  }
7469
- if (this.options.markdownPaste !== false && !clipboardData.types.includes("text/html")) {
7719
+ if (this.options.markdownPaste !== false) {
7720
+ const hasHtml = clipboardData.types.includes("text/html");
7721
+ const html = hasHtml ? clipboardData.getData("text/html") : "";
7722
+ const htmlTriviallyPlain = !hasHtml || this._isTriviallyPlainHtml(html);
7470
7723
  const text = clipboardData.getData("text/plain");
7471
- if (text && isMarkdown(text)) {
7724
+ if (text && htmlTriviallyPlain && isMarkdown(text)) {
7472
7725
  event.preventDefault();
7473
7726
  execCommand("insertHTML", sanitiseHTML(markdownToHTML(text)));
7474
7727
  this.context.invoke("editor.afterCommand");
@@ -7501,11 +7754,51 @@ var Clipboard = class {
7501
7754
  const dt = event.dataTransfer;
7502
7755
  if (!dt?.files?.length) return;
7503
7756
  const imageFiles = Array.from(dt.files).filter((f) => f.type.startsWith("image/"));
7504
- if (imageFiles.length === 0) return;
7505
- event.preventDefault();
7506
- event.stopPropagation();
7507
- this._placeCaretAtPoint(event.clientX, event.clientY);
7508
- this._insertImageFiles(imageFiles);
7757
+ if (imageFiles.length > 0) {
7758
+ event.preventDefault();
7759
+ event.stopPropagation();
7760
+ this._placeCaretAtPoint(event.clientX, event.clientY);
7761
+ this._insertImageFiles(imageFiles);
7762
+ return;
7763
+ }
7764
+ if (this.options.markdownPaste !== false) {
7765
+ const mdFile = Array.from(dt.files).find((f) => /\.md$/i.test(f.name) || f.type === "text/markdown");
7766
+ if (mdFile) {
7767
+ event.preventDefault();
7768
+ event.stopPropagation();
7769
+ this._placeCaretAtPoint(event.clientX, event.clientY);
7770
+ this._insertMarkdownFile(mdFile);
7771
+ }
7772
+ }
7773
+ }
7774
+ /**
7775
+ * Reads a dropped `.md` File and inserts it converted to HTML at the
7776
+ * current caret. Skips the isMarkdown() heuristic — an explicit `.md`
7777
+ * extension/MIME type is an unambiguous signal, unlike pasted plain text.
7778
+ * @param {File} file
7779
+ */
7780
+ _insertMarkdownFile(file) {
7781
+ const maxBytes = (this.options.maxPasteSize ?? 5) * 1024 * 1024;
7782
+ if (maxBytes > 0 && file.size > maxBytes) {
7783
+ const message = `Dropped file "${file.name}" (${file.size} bytes) exceeds the ${this.options.maxPasteSize ?? 5} MB paste size limit.`;
7784
+ this.context.triggerEvent("pasteError", {
7785
+ size: file.size,
7786
+ maxBytes,
7787
+ message
7788
+ });
7789
+ console.warn(`[AutumnNote] ${message}`);
7790
+ return;
7791
+ }
7792
+ const reader = new FileReader();
7793
+ reader.onload = (e) => {
7794
+ execCommand("insertHTML", sanitiseHTML(markdownToHTML(
7795
+ /** @type {string} */
7796
+ e.target.result || ""
7797
+ )));
7798
+ this.context.invoke("editor.afterCommand");
7799
+ };
7800
+ reader.onerror = () => console.warn("[AutumnNote] Failed to read dropped markdown file", file.name);
7801
+ reader.readAsText(file);
7509
7802
  }
7510
7803
  /**
7511
7804
  * Inserts one or more image Files into the editor.
@@ -17669,7 +17962,7 @@ var AutumnNote = {
17669
17962
  /** All pre-built button definitions — accessible in every module format including UMD/CJS. */
17670
17963
  buttons,
17671
17964
  /** Library version */
17672
- version: "1.10.0"
17965
+ version: "1.11.1"
17673
17966
  };
17674
17967
  /**
17675
17968
  * @param {string|Element|NodeList|Element[]} selector