autumnnote 1.10.0 → 1.11.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.
@@ -377,6 +377,24 @@
377
377
  margin: 0.5em 0;
378
378
  line-height: 1.3;
379
379
  }
380
+ .an-editable h1 {
381
+ font-size: 2em;
382
+ }
383
+ .an-editable h2 {
384
+ font-size: 1.5em;
385
+ }
386
+ .an-editable h3 {
387
+ font-size: 1.25em;
388
+ }
389
+ .an-editable h4 {
390
+ font-size: 1.1em;
391
+ }
392
+ .an-editable h5 {
393
+ font-size: 1em;
394
+ }
395
+ .an-editable h6 {
396
+ font-size: 0.875em;
397
+ }
380
398
  .an-editable ul, .an-editable ol {
381
399
  margin: 0 0 0.75em 0;
382
400
  padding-left: 1.5em;
@@ -385,6 +403,18 @@
385
403
  .an-editable li {
386
404
  margin-bottom: 0.25em;
387
405
  }
406
+ .an-editable li > p {
407
+ margin: 0 0 0.5em;
408
+ }
409
+ .an-editable li > p:first-child {
410
+ display: inline;
411
+ }
412
+ .an-editable li > p:last-child {
413
+ margin-bottom: 0;
414
+ }
415
+ .an-editable li:has(> p) {
416
+ margin-bottom: 0.6em;
417
+ }
388
418
  .an-editable blockquote {
389
419
  margin: 0.5em 0 0.5em 1em;
390
420
  padding: 0.5em 1em;
@@ -404,6 +434,10 @@
404
434
  .an-editable code {
405
435
  padding: 0.1em 0.3em;
406
436
  }
437
+ .an-editable pre > code {
438
+ background: transparent;
439
+ padding: 0;
440
+ }
407
441
  .an-editable pre[class*=language-] {
408
442
  background: #2d2d2d;
409
443
  color: #ccc;
@@ -1807,6 +1841,9 @@
1807
1841
  background: #313244;
1808
1842
  color: #cdd6f4;
1809
1843
  }
1844
+ .an-theme-dark .an-editable pre:not([class*=language-]) > code {
1845
+ background: transparent;
1846
+ }
1810
1847
  .an-theme-dark .an-editable .an-code-line-numbers::before {
1811
1848
  background: rgba(255, 255, 255, 0.05);
1812
1849
  border-right-color: rgba(255, 255, 255, 0.1);
@@ -2229,6 +2266,9 @@
2229
2266
  background: #313244;
2230
2267
  color: #cdd6f4;
2231
2268
  }
2269
+ .an-theme-auto .an-editable pre:not([class*=language-]) > code {
2270
+ background: transparent;
2271
+ }
2232
2272
  .an-theme-auto .an-editable hr {
2233
2273
  border-color: #3f3f5f;
2234
2274
  }
@@ -5502,6 +5502,17 @@ function htmlToMarkdown(html) {
5502
5502
  * @param {number} [depth=0] - Current nesting depth used to indent nested list items.
5503
5503
  * @returns {string} The Markdown representation of the node subtree.
5504
5504
  */
5505
+ /**
5506
+ * Direct child elements matching a tag name. Used instead of the CSS
5507
+ * `:scope > tag` combinator, which this project's jsdom version resolves
5508
+ * incorrectly (matches descendants at any depth, not just direct children).
5509
+ * @param {Element} el
5510
+ * @param {string} tagName
5511
+ * @returns {Element[]}
5512
+ */
5513
+ function _directChildren(el, tagName) {
5514
+ return Array.from(el.children).filter((c) => c.tagName === tagName.toUpperCase());
5515
+ }
5505
5516
  function _domToMd(node, depth = 0) {
5506
5517
  if (node.nodeType === 3) return node.textContent.replace(/\s+/g, " ");
5507
5518
  if (node.nodeType !== 1) return "";
@@ -5527,6 +5538,12 @@ function _domToMd(node, depth = 0) {
5527
5538
  case "strike": return `~~${inner()}~~`;
5528
5539
  case "sup": return `^${inner()}^`;
5529
5540
  case "sub": return `~${inner()}~`;
5541
+ case "u": return `<u>${inner()}</u>`;
5542
+ case "span": {
5543
+ const style = el.getAttribute("style") || "";
5544
+ if (/\b(color|background-color|font-size)\s*:/.test(style)) return `<span style="${_escAttr(style)}">${inner()}</span>`;
5545
+ return inner();
5546
+ }
5530
5547
  case "code":
5531
5548
  if (el.closest("pre")) return inner();
5532
5549
  return `\`${inner()}\``;
@@ -5535,7 +5552,10 @@ function _domToMd(node, depth = 0) {
5535
5552
  const langMatch = /language-(\S+)/.exec(codeEl?.className || "");
5536
5553
  return `\n\n\`\`\`${langMatch ? langMatch[1] : ""}\n${(codeEl || el).textContent || ""}\n\`\`\`\n\n`;
5537
5554
  }
5538
- case "blockquote": return `\n\n${inner().trim().split("\n").map((l) => `> ${l}`).join("\n")}\n\n`;
5555
+ case "blockquote": {
5556
+ const rawLines = inner().trim().split("\n");
5557
+ return `\n\n${rawLines.filter((l, idx) => l.trim() !== "" || (rawLines[idx - 1] ?? "").trim() !== "").map((l) => l.trim() === "" ? ">" : `> ${l}`).join("\n")}\n\n`;
5558
+ }
5539
5559
  case "a": {
5540
5560
  const href = el.getAttribute("href") || "";
5541
5561
  return `[${inner()}](${href})`;
@@ -5545,22 +5565,20 @@ function _domToMd(node, depth = 0) {
5545
5565
  return `![${el.getAttribute("alt") || ""}](${src})`;
5546
5566
  }
5547
5567
  case "ul": {
5548
- const items = Array.from(el.querySelectorAll(":scope > li"));
5568
+ const items = _directChildren(el, "li");
5549
5569
  if (!items.length) return inner();
5550
5570
  const indent = " ".repeat(depth);
5551
5571
  const isChecklist = el.classList.contains("an-checklist");
5552
5572
  const lines = items.map((li) => {
5573
+ const cb = _directChildren(li, "input").find((c) => c.getAttribute("type") === "checkbox");
5553
5574
  let prefix = "- ";
5554
- if (isChecklist) {
5555
- const cb = li.querySelector("input[type=\"checkbox\"]");
5556
- prefix = (cb ? cb.checked : false) ? "- [x] " : "- [ ] ";
5557
- }
5575
+ if (isChecklist || cb) prefix = (cb ? cb.checked : false) ? "- [x] " : "- [ ] ";
5558
5576
  return `${indent}${prefix}${_domToMd(li, depth + 1).trim()}`;
5559
5577
  }).join("\n");
5560
5578
  return depth === 0 ? `\n\n${lines}\n\n` : `\n${lines}`;
5561
5579
  }
5562
5580
  case "ol": {
5563
- const items = Array.from(el.querySelectorAll(":scope > li"));
5581
+ const items = _directChildren(el, "li");
5564
5582
  if (!items.length) return inner();
5565
5583
  const indent = " ".repeat(depth);
5566
5584
  const lines = items.map((li, i) => `${indent}${i + 1}. ${_domToMd(li, depth + 1).trim()}`).join("\n");
@@ -5569,19 +5587,22 @@ function _domToMd(node, depth = 0) {
5569
5587
  case "li": return inner();
5570
5588
  case "hr": return "\n\n---\n\n";
5571
5589
  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`\|`)));
5590
+ const allRows = Array.from(el.querySelectorAll("tr"));
5591
+ if (!allRows.length) return inner();
5592
+ const firstRowIsHeader = !!_directChildren(el, "thead")[0] || allRows[0].children.length > 0 && Array.from(allRows[0].children).every((c) => c.tagName === "TH");
5593
+ const cellTexts = allRows.map((tr) => Array.from(tr.querySelectorAll("th, td")).map((c) => c.textContent.trim().replaceAll("|", String.raw`\|`)));
5575
5594
  const cols = Math.max(...cellTexts.map((r) => r.length));
5576
5595
  const padRow = (row) => {
5577
5596
  const r = [...row];
5578
5597
  while (r.length < cols) r.push("");
5579
5598
  return r;
5580
5599
  };
5600
+ const bodyStart = firstRowIsHeader ? 1 : 0;
5601
+ const headerCells = firstRowIsHeader ? padRow(cellTexts[0]) : new Array(cols).fill("");
5581
5602
  let md = "\n\n";
5582
- md += `| ${padRow(cellTexts[0]).join(" | ")} |\n`;
5603
+ md += `| ${headerCells.join(" | ")} |\n`;
5583
5604
  md += `| ${new Array(cols).fill("---").join(" | ")} |\n`;
5584
- for (let r = 1; r < cellTexts.length; r++) md += `| ${padRow(cellTexts[r]).join(" | ")} |\n`;
5605
+ for (let r = bodyStart; r < cellTexts.length; r++) md += `| ${padRow(cellTexts[r]).join(" | ")} |\n`;
5585
5606
  return md + "\n";
5586
5607
  }
5587
5608
  default: return inner();
@@ -5596,8 +5617,11 @@ function _domToMd(node, depth = 0) {
5596
5617
  * @returns {boolean} `true` if any Markdown-like pattern is present, `false` otherwise.
5597
5618
  */
5598
5619
  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);
5620
+ 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
5621
  }
5622
+ var BQ_RE = /^ {0,3}>( ?)(.*)$/;
5623
+ var HR_RE = /^ {0,3}([-*_])( *\1){2,}\s*$/;
5624
+ var HARD_BREAK = String.fromCharCode(1);
5601
5625
  /**
5602
5626
  * Converts a Markdown string to an HTML string.
5603
5627
  * @param {string} text
@@ -5610,6 +5634,16 @@ function markdownToHTML(text) {
5610
5634
  lines = refs.clean;
5611
5635
  _linkDefs = refs.linkDefs;
5612
5636
  _footnoteIds = refs.footnoteIds;
5637
+ return _parseBlocks(lines);
5638
+ }
5639
+ /**
5640
+ * Parses a line array into block-level HTML. Called recursively for content
5641
+ * nested inside a blockquote so nested quotes and block content (lists,
5642
+ * headings, etc.) inside `>` are parsed the same as top-level content.
5643
+ * @param {string[]} lines
5644
+ * @returns {string}
5645
+ */
5646
+ function _parseBlocks(lines) {
5613
5647
  const out = [];
5614
5648
  let i = 0;
5615
5649
  while (i < lines.length) {
@@ -5628,7 +5662,7 @@ function markdownToHTML(text) {
5628
5662
  i++;
5629
5663
  continue;
5630
5664
  }
5631
- if (line.trim() && !/^(-{3,}|\*{3,}|_{3,})\s*$/.test(line) && !/^#{1,6} /.test(line) && i + 1 < lines.length) {
5665
+ if (line.trim() && !HR_RE.test(line) && !/^#{1,6} /.test(line) && i + 1 < lines.length) {
5632
5666
  if (/^=+\s*$/.test(lines[i + 1])) {
5633
5667
  out.push(`<h1>${_inline(line.trim())}</h1>`);
5634
5668
  i += 2;
@@ -5640,7 +5674,7 @@ function markdownToHTML(text) {
5640
5674
  continue;
5641
5675
  }
5642
5676
  }
5643
- if (/^(-{3,}|\*{3,}|_{3,})\s*$/.test(line)) {
5677
+ if (HR_RE.test(line)) {
5644
5678
  out.push("<hr>");
5645
5679
  i++;
5646
5680
  continue;
@@ -5648,17 +5682,18 @@ function markdownToHTML(text) {
5648
5682
  const hMatch = /^(#{1,6})\s+(.+)$/.exec(line);
5649
5683
  if (hMatch) {
5650
5684
  const level = hMatch[1].length;
5651
- out.push(`<h${level}>${_inline(hMatch[2])}</h${level}>`);
5685
+ const content = hMatch[2].replace(/(?:^|\s)#+\s*$/, "");
5686
+ out.push(`<h${level}>${_inline(content)}</h${level}>`);
5652
5687
  i++;
5653
5688
  continue;
5654
5689
  }
5655
- if (line.startsWith("> ")) {
5690
+ if (BQ_RE.test(line)) {
5656
5691
  const bqLines = [];
5657
- while (i < lines.length && lines[i].startsWith("> ")) {
5658
- bqLines.push(lines[i].slice(2));
5692
+ while (i < lines.length && BQ_RE.test(lines[i])) {
5693
+ bqLines.push(BQ_RE.exec(lines[i])[2]);
5659
5694
  i++;
5660
5695
  }
5661
- out.push(`<blockquote>${bqLines.map(_inline).join("<br>")}</blockquote>`);
5696
+ out.push(`<blockquote>${_parseBlocks(bqLines)}</blockquote>`);
5662
5697
  continue;
5663
5698
  }
5664
5699
  if (/^[-*+] /.test(line)) {
@@ -5701,11 +5736,11 @@ function markdownToHTML(text) {
5701
5736
  continue;
5702
5737
  }
5703
5738
  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]))) {
5739
+ 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
5740
  paraLines.push(lines[i]);
5706
5741
  i++;
5707
5742
  }
5708
- if (paraLines.length) out.push(`<p>${_inline(paraLines.join(" "))}</p>`);
5743
+ if (paraLines.length) out.push(`<p>${_inline(_joinParagraphLines(paraLines)).replaceAll(HARD_BREAK, "<br>")}</p>`);
5709
5744
  }
5710
5745
  return out.join("");
5711
5746
  }
@@ -5780,23 +5815,77 @@ function _extractReferenceDefinitions(lines) {
5780
5815
  };
5781
5816
  }
5782
5817
  /**
5783
- * Splits a GFM table row string into trimmed cell strings.
5784
- * '| a | b | c |' ['a', 'b', 'c']
5818
+ * Joins a paragraph's source lines into one string, converting CommonMark
5819
+ * hard-break markers (a trailing backslash, or 2+ trailing spaces) on all
5820
+ * but the last line into a HARD_BREAK placeholder instead of a plain space.
5821
+ * @param {string[]} paraLines
5822
+ * @returns {string}
5823
+ */
5824
+ function _joinParagraphLines(paraLines) {
5825
+ let joined = "";
5826
+ for (let idx = 0; idx < paraLines.length; idx++) {
5827
+ const isLast = idx === paraLines.length - 1;
5828
+ const ln = paraLines[idx];
5829
+ if (!isLast && /\\$/.test(ln)) {
5830
+ joined += ln.replace(/\\$/, "") + HARD_BREAK;
5831
+ continue;
5832
+ }
5833
+ if (!isLast && / {2,}$/.test(ln)) {
5834
+ joined += ln.replace(/ {2,}$/, "") + HARD_BREAK;
5835
+ continue;
5836
+ }
5837
+ joined += ln + (isLast ? "" : " ");
5838
+ }
5839
+ return joined;
5840
+ }
5841
+ /**
5842
+ * Splits a GFM table row string into trimmed cell strings, treating an
5843
+ * escaped pipe (`\|`) as a literal character rather than a cell separator.
5844
+ * '| a | b | c |' → ['a', 'b', 'c']; '| a\|b | c |' → ['a|b', 'c']
5785
5845
  * @param {string} row
5786
5846
  * @returns {string[]}
5787
5847
  */
5788
5848
  function _parseTableRow(row) {
5789
- return row.replace(/^\|/, "").replace(/\|$/, "").split("|").map((c) => c.trim());
5849
+ const trimmed = row.replace(/^\|/, "").replace(/\|$/, "");
5850
+ const cells = [];
5851
+ let cur = "";
5852
+ for (let i = 0; i < trimmed.length; i++) {
5853
+ if (trimmed[i] === "\\" && trimmed[i + 1] === "|") {
5854
+ cur += "|";
5855
+ i++;
5856
+ continue;
5857
+ }
5858
+ if (trimmed[i] === "|") {
5859
+ cells.push(cur);
5860
+ cur = "";
5861
+ continue;
5862
+ }
5863
+ cur += trimmed[i];
5864
+ }
5865
+ cells.push(cur);
5866
+ return cells.map((c) => c.trim());
5790
5867
  }
5791
5868
  function _parseListBlock(lines, startIdx) {
5792
5869
  const baseIndent = lines[startIdx].match(/^(\s*)/)[1].length;
5793
5870
  const isOL = /^\s*\d+\. /.test(lines[startIdx]);
5794
5871
  const items = [];
5795
5872
  let firstIsCB = null;
5873
+ let loose = false;
5874
+ let pendingBlank = false;
5796
5875
  let i = startIdx;
5797
5876
  while (i < lines.length) {
5798
5877
  const line = lines[i];
5799
- if (line.trim() === "") break;
5878
+ if (line.trim() === "") {
5879
+ const next = lines[i + 1];
5880
+ const nextIndent = next !== void 0 ? next.match(/^(\s*)/)[1].length : -1;
5881
+ const nextIsSameItem = next !== void 0 && /^\s*(?:[-*+]|\d+\.) /.test(next) && /^\s*\d+\. /.test(next) === isOL && nextIndent === baseIndent;
5882
+ const nextIsContinuation = next !== void 0 && next.trim() !== "" && nextIndent > baseIndent;
5883
+ if (!items.length || !nextIsSameItem && !nextIsContinuation) break;
5884
+ loose = true;
5885
+ pendingBlank = true;
5886
+ i++;
5887
+ continue;
5888
+ }
5800
5889
  const indent = line.match(/^(\s*)/)[1].length;
5801
5890
  if (indent < baseIndent) break;
5802
5891
  if (indent === baseIndent) {
@@ -5809,11 +5898,12 @@ function _parseListBlock(lines, startIdx) {
5809
5898
  const checked = isCB && raw[1].toLowerCase() === "x";
5810
5899
  const text = isCB ? raw.replace(/^\[[ xX]\]\s+/, "") : raw;
5811
5900
  items.push({
5812
- text,
5901
+ paras: [text],
5813
5902
  isCB,
5814
5903
  checked,
5815
5904
  sub: ""
5816
5905
  });
5906
+ pendingBlank = false;
5817
5907
  i++;
5818
5908
  } else {
5819
5909
  if (!items.length) {
@@ -5824,53 +5914,143 @@ function _parseListBlock(lines, startIdx) {
5824
5914
  const nested = _parseListBlock(lines, i);
5825
5915
  items[items.length - 1].sub += nested.html;
5826
5916
  i = nested.endIdx;
5917
+ pendingBlank = false;
5918
+ } else if (pendingBlank) {
5919
+ items[items.length - 1].paras.push(line.trim());
5920
+ pendingBlank = false;
5921
+ i++;
5827
5922
  } else {
5828
- items[items.length - 1].text += " " + line.trim();
5923
+ const paras = items[items.length - 1].paras;
5924
+ paras[paras.length - 1] += " " + line.trim();
5829
5925
  i++;
5830
5926
  }
5831
5927
  }
5832
5928
  }
5833
- const open = isOL ? "<ol>" : !isOL && firstIsCB === true ? "<ul class=\"an-checklist\">" : "<ul>";
5929
+ const hasCB = !isOL && firstIsCB === true;
5930
+ const startMatch = isOL ? /^\s*(\d+)\. /.exec(lines[startIdx]) : null;
5931
+ const startNum = startMatch ? Number.parseInt(startMatch[1], 10) : 1;
5932
+ const open = isOL ? startNum !== 1 ? `<ol start="${startNum}">` : "<ol>" : hasCB ? "<ul class=\"an-checklist\">" : "<ul>";
5834
5933
  const close = isOL ? "</ol>" : "</ul>";
5835
5934
  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>`;
5935
+ html: `${open}${items.map(({ paras, isCB, checked, sub }) => {
5936
+ const cbHTML = isCB ? `<input type="checkbox" contenteditable="false"${checked ? " checked" : ""}>` : "";
5937
+ return `<li>${loose ? paras.map((p, idx) => `<p>${idx === 0 ? cbHTML : ""}${_inline(p)}</p>`).join("") : `${cbHTML}${_inline(paras[0])}`}${sub}</li>`;
5838
5938
  }).join("")}${close}`,
5839
5939
  endIdx: i
5840
5940
  };
5841
5941
  }
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>`);
5942
+ var ESCAPABLE_RE = /\\([*_`#[\]()>\\~|])/g;
5943
+ var MARK = String.fromCharCode(0);
5944
+ /**
5945
+ * Step 0 of _inline(): replaces backslash-escaped punctuation with inert
5946
+ * placeholders so later syntax regexes can't match them.
5947
+ * @param {string} text
5948
+ * @returns {{ text: string, literals: string[] }}
5949
+ */
5950
+ function _extractBackslashEscapes(text) {
5951
+ const literals = [];
5952
+ return {
5953
+ text: text.replace(ESCAPABLE_RE, (_, ch) => {
5954
+ literals.push(ch);
5955
+ return `${MARK}${literals.length - 1}${MARK}`;
5956
+ }),
5957
+ literals
5958
+ };
5959
+ }
5960
+ /**
5961
+ * Restores placeholders from _extractBackslashEscapes(), HTML-escaping each
5962
+ * literal since it's inserted directly into the output.
5963
+ * @param {string} text
5964
+ * @param {string[]} literals
5965
+ * @returns {string}
5966
+ */
5967
+ function _restoreBackslashEscapes(text, literals) {
5968
+ return text.replace(new RegExp(`${MARK}(\\d+)${MARK}`, "g"), (_, idx) => _esc(literals[Number(idx)]));
5969
+ }
5970
+ /**
5971
+ * Resolves images, inline links, GFM reference-style links (explicit,
5972
+ * shortcut, and bare/implicit forms), and footnote markers. Must run on text
5973
+ * already passed through _esc() — see _inline()'s Step 1 comment.
5974
+ * @param {string} text
5975
+ * @returns {string}
5976
+ */
5977
+ function _resolveLinksAndFootnotes(text) {
5978
+ text = text.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, (_, alt, src) => `<img src="${_escAttrQuotes(src)}" alt="${_escAttrQuotes(alt)}" class="an-image">`);
5979
+ text = text.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_, label, href) => `<a href="${_escAttrQuotes(href)}">${label}</a>`);
5845
5980
  text = text.replace(/\[([^\]]+)\]\[([^\]]*)\]/g, (m, label, ref) => {
5846
- const def = _linkDefs.get((ref || label).trim().toLowerCase());
5981
+ const def = _linkDefs.get(_unescAmpLtGt(ref || label).trim().toLowerCase());
5847
5982
  if (!def) return m;
5848
5983
  const titleAttr = def.title ? ` title="${_escAttr(def.title)}"` : "";
5849
- return `<a href="${_escAttr(def.href)}"${titleAttr}>${_esc(label)}</a>`;
5984
+ return `<a href="${_escAttr(def.href)}"${titleAttr}>${label}</a>`;
5850
5985
  });
5851
5986
  text = text.replace(/\[([^\]]+)\]/g, (m, label) => {
5852
- const def = _linkDefs.get(label.trim().toLowerCase());
5987
+ const def = _linkDefs.get(_unescAmpLtGt(label).trim().toLowerCase());
5853
5988
  if (!def) return m;
5854
5989
  const titleAttr = def.title ? ` title="${_escAttr(def.title)}"` : "";
5855
- return `<a href="${_escAttr(def.href)}"${titleAttr}>${_esc(label)}</a>`;
5990
+ return `<a href="${_escAttr(def.href)}"${titleAttr}>${label}</a>`;
5991
+ });
5992
+ text = text.replace(/\[\^([^\]]+)\]/g, (m, id) => _footnoteIds.has(_unescAmpLtGt(id)) ? `<sup>[${id}]</sup>` : m);
5993
+ return text;
5994
+ }
5995
+ /**
5996
+ * Converts angle-bracket (`<https://...>`) and bare (`https://...`)
5997
+ * autolinks. Runs after _resolveLinksAndFootnotes() so an already-linked URL
5998
+ * isn't reprocessed, and on already-_esc()'d text (see _inline()).
5999
+ * @param {string} text
6000
+ * @returns {string}
6001
+ */
6002
+ function _applyAutolinks(text) {
6003
+ text = text.replace(/&lt;(https?:\/\/[^\s&]+?)&gt;/g, (_, url) => `<a href="${_escAttrQuotes(url)}">${url}</a>`);
6004
+ text = text.replace(/(^|[\s(])(https?:\/\/[^\s()]+)/g, (m, pre, rawUrl) => {
6005
+ const trail = /[.,;:!?)]+$/.exec(rawUrl);
6006
+ const url = trail ? rawUrl.slice(0, -trail[0].length) : rawUrl;
6007
+ if (!url) return m;
6008
+ const suffix = trail ? trail[0] : "";
6009
+ return `${pre}<a href="${_escAttrQuotes(url)}">${url}</a>${suffix}`;
5856
6010
  });
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>`);
5866
6011
  return text;
5867
6012
  }
6013
+ /**
6014
+ * Applies bold/italic/bold-italic (asterisk and underscore forms — underscore
6015
+ * requires a non-word-character boundary per CommonMark), strikethrough, and
6016
+ * inline code.
6017
+ * @param {string} text
6018
+ * @returns {string}
6019
+ */
6020
+ function _applyEmphasisAndCode(text) {
6021
+ text = text.replace(/\*{3}([^*\n]+?)\*{3}/g, (_, c) => `<strong><em>${c}</em></strong>`);
6022
+ text = text.replace(/(?<!\w)_{3}([^_\n]+?)_{3}(?!\w)/g, (_, c) => `<strong><em>${c}</em></strong>`);
6023
+ text = text.replace(/\*{2}([^*\n]+?)\*{2}/g, (_, c) => `<strong>${c}</strong>`);
6024
+ text = text.replace(/(?<!\w)_{2}([^_\n]+?)_{2}(?!\w)/g, (_, c) => `<strong>${c}</strong>`);
6025
+ text = text.replace(/\*([^*\n]+?)\*/g, (_, c) => `<em>${c}</em>`);
6026
+ text = text.replace(/(?<!\w)_([^_\n]+?)_(?!\w)/g, (_, c) => `<em>${c}</em>`);
6027
+ text = text.replace(/~~([^~\n]+?)~~/g, (_, c) => `<del>${c}</del>`);
6028
+ text = text.replace(/``([\s\S]*?)``/g, (_, c) => `<code>${c}</code>`);
6029
+ text = text.replace(/`([^`]+)`/g, (_, c) => `<code>${c}</code>`);
6030
+ return text;
6031
+ }
6032
+ function _inline(text) {
6033
+ const { text: withoutEscapes, literals } = _extractBackslashEscapes(text);
6034
+ let result = _esc(withoutEscapes);
6035
+ result = _resolveLinksAndFootnotes(result);
6036
+ result = _applyAutolinks(result);
6037
+ result = _applyEmphasisAndCode(result);
6038
+ return _restoreBackslashEscapes(result, literals);
6039
+ }
5868
6040
  function _esc(v) {
5869
6041
  return String(v).replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
5870
6042
  }
5871
6043
  function _escAttr(v) {
5872
6044
  return String(v).replaceAll("&", "&amp;").replaceAll("\"", "&quot;").replaceAll("'", "&#39;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
5873
6045
  }
6046
+ /** Escapes only quote characters — for attribute values already run through _esc(). */
6047
+ function _escAttrQuotes(v) {
6048
+ return String(v).replaceAll("\"", "&quot;").replaceAll("'", "&#39;");
6049
+ }
6050
+ /** Reverses _esc()'s &amp;/&lt;/&gt; substitutions, for matching against un-escaped _linkDefs/_footnoteIds keys. */
6051
+ function _unescAmpLtGt(v) {
6052
+ return String(v).replaceAll("&lt;", "<").replaceAll("&gt;", ">").replaceAll("&amp;", "&");
6053
+ }
5874
6054
  //#endregion
5875
6055
  //#region src/js/core/detectLang.js
5876
6056
  /**
@@ -7426,6 +7606,17 @@ var Clipboard = class {
7426
7606
  return doc.body.innerHTML;
7427
7607
  }
7428
7608
  /**
7609
+ * Checks whether an HTML payload has no semantic markup beyond plain
7610
+ * wrapper elements (e.g. a bare <div>/<p>). Used to decide whether a
7611
+ * markdown-shaped plain-text paste should win over an accompanying HTML
7612
+ * payload that isn't actually carrying any real rich-text formatting.
7613
+ * @param {string} html
7614
+ * @returns {boolean}
7615
+ */
7616
+ _isTriviallyPlainHtml(html) {
7617
+ 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");
7618
+ }
7619
+ /**
7429
7620
  * Forces the next paste operation to strip all HTML formatting.
7430
7621
  * Called by Editor when Ctrl+Shift+V is pressed.
7431
7622
  * @param {boolean} val
@@ -7442,8 +7633,16 @@ var Clipboard = class {
7442
7633
  if (maxBytes > 0) {
7443
7634
  const text = clipboardData.getData("text/plain") || "";
7444
7635
  const html = clipboardData.getData("text/html") || "";
7445
- if (Math.max(text.length, html.length) > maxBytes) {
7636
+ const size = Math.max(text.length, html.length);
7637
+ if (size > maxBytes) {
7446
7638
  event.preventDefault();
7639
+ const message = `Pasted content (${size} bytes) exceeds the ${this.options.maxPasteSize ?? 5} MB paste size limit.`;
7640
+ this.context.triggerEvent("pasteError", {
7641
+ size,
7642
+ maxBytes,
7643
+ message
7644
+ });
7645
+ console.warn(`[AutumnNote] ${message}`);
7447
7646
  return;
7448
7647
  }
7449
7648
  }
@@ -7466,9 +7665,12 @@ var Clipboard = class {
7466
7665
  this.context.invoke("editor.afterCommand");
7467
7666
  return;
7468
7667
  }
7469
- if (this.options.markdownPaste !== false && !clipboardData.types.includes("text/html")) {
7668
+ if (this.options.markdownPaste !== false) {
7669
+ const hasHtml = clipboardData.types.includes("text/html");
7670
+ const html = hasHtml ? clipboardData.getData("text/html") : "";
7671
+ const htmlTriviallyPlain = !hasHtml || this._isTriviallyPlainHtml(html);
7470
7672
  const text = clipboardData.getData("text/plain");
7471
- if (text && isMarkdown(text)) {
7673
+ if (text && htmlTriviallyPlain && isMarkdown(text)) {
7472
7674
  event.preventDefault();
7473
7675
  execCommand("insertHTML", sanitiseHTML(markdownToHTML(text)));
7474
7676
  this.context.invoke("editor.afterCommand");
@@ -7501,11 +7703,51 @@ var Clipboard = class {
7501
7703
  const dt = event.dataTransfer;
7502
7704
  if (!dt?.files?.length) return;
7503
7705
  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);
7706
+ if (imageFiles.length > 0) {
7707
+ event.preventDefault();
7708
+ event.stopPropagation();
7709
+ this._placeCaretAtPoint(event.clientX, event.clientY);
7710
+ this._insertImageFiles(imageFiles);
7711
+ return;
7712
+ }
7713
+ if (this.options.markdownPaste !== false) {
7714
+ const mdFile = Array.from(dt.files).find((f) => /\.md$/i.test(f.name) || f.type === "text/markdown");
7715
+ if (mdFile) {
7716
+ event.preventDefault();
7717
+ event.stopPropagation();
7718
+ this._placeCaretAtPoint(event.clientX, event.clientY);
7719
+ this._insertMarkdownFile(mdFile);
7720
+ }
7721
+ }
7722
+ }
7723
+ /**
7724
+ * Reads a dropped `.md` File and inserts it converted to HTML at the
7725
+ * current caret. Skips the isMarkdown() heuristic — an explicit `.md`
7726
+ * extension/MIME type is an unambiguous signal, unlike pasted plain text.
7727
+ * @param {File} file
7728
+ */
7729
+ _insertMarkdownFile(file) {
7730
+ const maxBytes = (this.options.maxPasteSize ?? 5) * 1024 * 1024;
7731
+ if (maxBytes > 0 && file.size > maxBytes) {
7732
+ const message = `Dropped file "${file.name}" (${file.size} bytes) exceeds the ${this.options.maxPasteSize ?? 5} MB paste size limit.`;
7733
+ this.context.triggerEvent("pasteError", {
7734
+ size: file.size,
7735
+ maxBytes,
7736
+ message
7737
+ });
7738
+ console.warn(`[AutumnNote] ${message}`);
7739
+ return;
7740
+ }
7741
+ const reader = new FileReader();
7742
+ reader.onload = (e) => {
7743
+ execCommand("insertHTML", sanitiseHTML(markdownToHTML(
7744
+ /** @type {string} */
7745
+ e.target.result || ""
7746
+ )));
7747
+ this.context.invoke("editor.afterCommand");
7748
+ };
7749
+ reader.onerror = () => console.warn("[AutumnNote] Failed to read dropped markdown file", file.name);
7750
+ reader.readAsText(file);
7509
7751
  }
7510
7752
  /**
7511
7753
  * Inserts one or more image Files into the editor.
@@ -17669,7 +17911,7 @@ var AutumnNote = {
17669
17911
  /** All pre-built button definitions — accessible in every module format including UMD/CJS. */
17670
17912
  buttons,
17671
17913
  /** Library version */
17672
- version: "1.10.0"
17914
+ version: "1.11.0"
17673
17915
  };
17674
17916
  /**
17675
17917
  * @param {string|Element|NodeList|Element[]} selector