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.
@@ -5215,6 +5215,17 @@
5215
5215
  * @param {number} [depth=0] - Current nesting depth used to indent nested list items.
5216
5216
  * @returns {string} The Markdown representation of the node subtree.
5217
5217
  */
5218
+ /**
5219
+ * Direct child elements matching a tag name. Used instead of the CSS
5220
+ * `:scope > tag` combinator, which this project's jsdom version resolves
5221
+ * incorrectly (matches descendants at any depth, not just direct children).
5222
+ * @param {Element} el
5223
+ * @param {string} tagName
5224
+ * @returns {Element[]}
5225
+ */
5226
+ function _directChildren(el, tagName) {
5227
+ return Array.from(el.children).filter((c) => c.tagName === tagName.toUpperCase());
5228
+ }
5218
5229
  function _domToMd(node, depth = 0) {
5219
5230
  if (node.nodeType === 3) return node.textContent.replace(/\s+/g, " ");
5220
5231
  if (node.nodeType !== 1) return "";
@@ -5240,6 +5251,12 @@
5240
5251
  case "strike": return `~~${inner()}~~`;
5241
5252
  case "sup": return `^${inner()}^`;
5242
5253
  case "sub": return `~${inner()}~`;
5254
+ case "u": return `<u>${inner()}</u>`;
5255
+ case "span": {
5256
+ const style = el.getAttribute("style") || "";
5257
+ if (/\b(color|background-color|font-size)\s*:/.test(style)) return `<span style="${_escAttr(style)}">${inner()}</span>`;
5258
+ return inner();
5259
+ }
5243
5260
  case "code":
5244
5261
  if (el.closest("pre")) return inner();
5245
5262
  return `\`${inner()}\``;
@@ -5248,7 +5265,10 @@
5248
5265
  const langMatch = /language-(\S+)/.exec(codeEl?.className || "");
5249
5266
  return `\n\n\`\`\`${langMatch ? langMatch[1] : ""}\n${(codeEl || el).textContent || ""}\n\`\`\`\n\n`;
5250
5267
  }
5251
- case "blockquote": return `\n\n${inner().trim().split("\n").map((l) => `> ${l}`).join("\n")}\n\n`;
5268
+ case "blockquote": {
5269
+ const rawLines = inner().trim().split("\n");
5270
+ return `\n\n${rawLines.filter((l, idx) => l.trim() !== "" || (rawLines[idx - 1] ?? "").trim() !== "").map((l) => l.trim() === "" ? ">" : `> ${l}`).join("\n")}\n\n`;
5271
+ }
5252
5272
  case "a": {
5253
5273
  const href = el.getAttribute("href") || "";
5254
5274
  return `[${inner()}](${href})`;
@@ -5258,22 +5278,20 @@
5258
5278
  return `![${el.getAttribute("alt") || ""}](${src})`;
5259
5279
  }
5260
5280
  case "ul": {
5261
- const items = Array.from(el.querySelectorAll(":scope > li"));
5281
+ const items = _directChildren(el, "li");
5262
5282
  if (!items.length) return inner();
5263
5283
  const indent = " ".repeat(depth);
5264
5284
  const isChecklist = el.classList.contains("an-checklist");
5265
5285
  const lines = items.map((li) => {
5286
+ const cb = _directChildren(li, "input").find((c) => c.getAttribute("type") === "checkbox");
5266
5287
  let prefix = "- ";
5267
- if (isChecklist) {
5268
- const cb = li.querySelector("input[type=\"checkbox\"]");
5269
- prefix = (cb ? cb.checked : false) ? "- [x] " : "- [ ] ";
5270
- }
5288
+ if (isChecklist || cb) prefix = (cb ? cb.checked : false) ? "- [x] " : "- [ ] ";
5271
5289
  return `${indent}${prefix}${_domToMd(li, depth + 1).trim()}`;
5272
5290
  }).join("\n");
5273
5291
  return depth === 0 ? `\n\n${lines}\n\n` : `\n${lines}`;
5274
5292
  }
5275
5293
  case "ol": {
5276
- const items = Array.from(el.querySelectorAll(":scope > li"));
5294
+ const items = _directChildren(el, "li");
5277
5295
  if (!items.length) return inner();
5278
5296
  const indent = " ".repeat(depth);
5279
5297
  const lines = items.map((li, i) => `${indent}${i + 1}. ${_domToMd(li, depth + 1).trim()}`).join("\n");
@@ -5282,19 +5300,22 @@
5282
5300
  case "li": return inner();
5283
5301
  case "hr": return "\n\n---\n\n";
5284
5302
  case "table": {
5285
- const rows = Array.from(el.querySelectorAll("tr"));
5286
- if (!rows.length) return inner();
5287
- const cellTexts = rows.map((tr) => Array.from(tr.querySelectorAll("th, td")).map((c) => c.textContent.trim().replaceAll("|", String.raw`\|`)));
5303
+ const allRows = Array.from(el.querySelectorAll("tr"));
5304
+ if (!allRows.length) return inner();
5305
+ const firstRowIsHeader = !!_directChildren(el, "thead")[0] || allRows[0].children.length > 0 && Array.from(allRows[0].children).every((c) => c.tagName === "TH");
5306
+ const cellTexts = allRows.map((tr) => Array.from(tr.querySelectorAll("th, td")).map((c) => c.textContent.trim().replaceAll("|", String.raw`\|`)));
5288
5307
  const cols = Math.max(...cellTexts.map((r) => r.length));
5289
5308
  const padRow = (row) => {
5290
5309
  const r = [...row];
5291
5310
  while (r.length < cols) r.push("");
5292
5311
  return r;
5293
5312
  };
5313
+ const bodyStart = firstRowIsHeader ? 1 : 0;
5314
+ const headerCells = firstRowIsHeader ? padRow(cellTexts[0]) : new Array(cols).fill("");
5294
5315
  let md = "\n\n";
5295
- md += `| ${padRow(cellTexts[0]).join(" | ")} |\n`;
5316
+ md += `| ${headerCells.join(" | ")} |\n`;
5296
5317
  md += `| ${new Array(cols).fill("---").join(" | ")} |\n`;
5297
- for (let r = 1; r < cellTexts.length; r++) md += `| ${padRow(cellTexts[r]).join(" | ")} |\n`;
5318
+ for (let r = bodyStart; r < cellTexts.length; r++) md += `| ${padRow(cellTexts[r]).join(" | ")} |\n`;
5298
5319
  return md + "\n";
5299
5320
  }
5300
5321
  default: return inner();
@@ -5309,8 +5330,11 @@
5309
5330
  * @returns {boolean} `true` if any Markdown-like pattern is present, `false` otherwise.
5310
5331
  */
5311
5332
  function isMarkdown(text) {
5312
- 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);
5333
+ 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);
5313
5334
  }
5335
+ var BQ_RE = /^ {0,3}>( ?)(.*)$/;
5336
+ var HR_RE = /^ {0,3}([-*_])( *\1){2,}\s*$/;
5337
+ var HARD_BREAK = String.fromCharCode(1);
5314
5338
  /**
5315
5339
  * Converts a Markdown string to an HTML string.
5316
5340
  * @param {string} text
@@ -5323,6 +5347,16 @@
5323
5347
  lines = refs.clean;
5324
5348
  _linkDefs = refs.linkDefs;
5325
5349
  _footnoteIds = refs.footnoteIds;
5350
+ return _parseBlocks(lines);
5351
+ }
5352
+ /**
5353
+ * Parses a line array into block-level HTML. Called recursively for content
5354
+ * nested inside a blockquote so nested quotes and block content (lists,
5355
+ * headings, etc.) inside `>` are parsed the same as top-level content.
5356
+ * @param {string[]} lines
5357
+ * @returns {string}
5358
+ */
5359
+ function _parseBlocks(lines) {
5326
5360
  const out = [];
5327
5361
  let i = 0;
5328
5362
  while (i < lines.length) {
@@ -5341,7 +5375,7 @@
5341
5375
  i++;
5342
5376
  continue;
5343
5377
  }
5344
- if (line.trim() && !/^(-{3,}|\*{3,}|_{3,})\s*$/.test(line) && !/^#{1,6} /.test(line) && i + 1 < lines.length) {
5378
+ if (line.trim() && !HR_RE.test(line) && !/^#{1,6} /.test(line) && i + 1 < lines.length) {
5345
5379
  if (/^=+\s*$/.test(lines[i + 1])) {
5346
5380
  out.push(`<h1>${_inline(line.trim())}</h1>`);
5347
5381
  i += 2;
@@ -5353,7 +5387,7 @@
5353
5387
  continue;
5354
5388
  }
5355
5389
  }
5356
- if (/^(-{3,}|\*{3,}|_{3,})\s*$/.test(line)) {
5390
+ if (HR_RE.test(line)) {
5357
5391
  out.push("<hr>");
5358
5392
  i++;
5359
5393
  continue;
@@ -5361,17 +5395,18 @@
5361
5395
  const hMatch = /^(#{1,6})\s+(.+)$/.exec(line);
5362
5396
  if (hMatch) {
5363
5397
  const level = hMatch[1].length;
5364
- out.push(`<h${level}>${_inline(hMatch[2])}</h${level}>`);
5398
+ const content = hMatch[2].replace(/(?:^|\s)#+\s*$/, "");
5399
+ out.push(`<h${level}>${_inline(content)}</h${level}>`);
5365
5400
  i++;
5366
5401
  continue;
5367
5402
  }
5368
- if (line.startsWith("> ")) {
5403
+ if (BQ_RE.test(line)) {
5369
5404
  const bqLines = [];
5370
- while (i < lines.length && lines[i].startsWith("> ")) {
5371
- bqLines.push(lines[i].slice(2));
5405
+ while (i < lines.length && BQ_RE.test(lines[i])) {
5406
+ bqLines.push(BQ_RE.exec(lines[i])[2]);
5372
5407
  i++;
5373
5408
  }
5374
- out.push(`<blockquote>${bqLines.map(_inline).join("<br>")}</blockquote>`);
5409
+ out.push(`<blockquote>${_parseBlocks(bqLines)}</blockquote>`);
5375
5410
  continue;
5376
5411
  }
5377
5412
  if (/^[-*+] /.test(line)) {
@@ -5414,11 +5449,11 @@
5414
5449
  continue;
5415
5450
  }
5416
5451
  const paraLines = [];
5417
- 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]))) {
5452
+ 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]))) {
5418
5453
  paraLines.push(lines[i]);
5419
5454
  i++;
5420
5455
  }
5421
- if (paraLines.length) out.push(`<p>${_inline(paraLines.join(" "))}</p>`);
5456
+ if (paraLines.length) out.push(`<p>${_inline(_joinParagraphLines(paraLines)).replaceAll(HARD_BREAK, "<br>")}</p>`);
5422
5457
  }
5423
5458
  return out.join("");
5424
5459
  }
@@ -5493,23 +5528,77 @@
5493
5528
  };
5494
5529
  }
5495
5530
  /**
5496
- * Splits a GFM table row string into trimmed cell strings.
5497
- * '| a | b | c |' → ['a', 'b', 'c']
5531
+ * Joins a paragraph's source lines into one string, converting CommonMark
5532
+ * hard-break markers (a trailing backslash, or 2+ trailing spaces) on all
5533
+ * but the last line into a HARD_BREAK placeholder instead of a plain space.
5534
+ * @param {string[]} paraLines
5535
+ * @returns {string}
5536
+ */
5537
+ function _joinParagraphLines(paraLines) {
5538
+ let joined = "";
5539
+ for (let idx = 0; idx < paraLines.length; idx++) {
5540
+ const isLast = idx === paraLines.length - 1;
5541
+ const ln = paraLines[idx];
5542
+ if (!isLast && /\\$/.test(ln)) {
5543
+ joined += ln.replace(/\\$/, "") + HARD_BREAK;
5544
+ continue;
5545
+ }
5546
+ if (!isLast && / {2,}$/.test(ln)) {
5547
+ joined += ln.replace(/ {2,}$/, "") + HARD_BREAK;
5548
+ continue;
5549
+ }
5550
+ joined += ln + (isLast ? "" : " ");
5551
+ }
5552
+ return joined;
5553
+ }
5554
+ /**
5555
+ * Splits a GFM table row string into trimmed cell strings, treating an
5556
+ * escaped pipe (`\|`) as a literal character rather than a cell separator.
5557
+ * '| a | b | c |' → ['a', 'b', 'c']; '| a\|b | c |' → ['a|b', 'c']
5498
5558
  * @param {string} row
5499
5559
  * @returns {string[]}
5500
5560
  */
5501
5561
  function _parseTableRow(row) {
5502
- return row.replace(/^\|/, "").replace(/\|$/, "").split("|").map((c) => c.trim());
5562
+ const trimmed = row.replace(/^\|/, "").replace(/\|$/, "");
5563
+ const cells = [];
5564
+ let cur = "";
5565
+ for (let i = 0; i < trimmed.length; i++) {
5566
+ if (trimmed[i] === "\\" && trimmed[i + 1] === "|") {
5567
+ cur += "|";
5568
+ i++;
5569
+ continue;
5570
+ }
5571
+ if (trimmed[i] === "|") {
5572
+ cells.push(cur);
5573
+ cur = "";
5574
+ continue;
5575
+ }
5576
+ cur += trimmed[i];
5577
+ }
5578
+ cells.push(cur);
5579
+ return cells.map((c) => c.trim());
5503
5580
  }
5504
5581
  function _parseListBlock(lines, startIdx) {
5505
5582
  const baseIndent = lines[startIdx].match(/^(\s*)/)[1].length;
5506
5583
  const isOL = /^\s*\d+\. /.test(lines[startIdx]);
5507
5584
  const items = [];
5508
5585
  let firstIsCB = null;
5586
+ let loose = false;
5587
+ let pendingBlank = false;
5509
5588
  let i = startIdx;
5510
5589
  while (i < lines.length) {
5511
5590
  const line = lines[i];
5512
- if (line.trim() === "") break;
5591
+ if (line.trim() === "") {
5592
+ const next = lines[i + 1];
5593
+ const nextIndent = next !== void 0 ? next.match(/^(\s*)/)[1].length : -1;
5594
+ const nextIsSameItem = next !== void 0 && /^\s*(?:[-*+]|\d+\.) /.test(next) && /^\s*\d+\. /.test(next) === isOL && nextIndent === baseIndent;
5595
+ const nextIsContinuation = next !== void 0 && next.trim() !== "" && nextIndent > baseIndent;
5596
+ if (!items.length || !nextIsSameItem && !nextIsContinuation) break;
5597
+ loose = true;
5598
+ pendingBlank = true;
5599
+ i++;
5600
+ continue;
5601
+ }
5513
5602
  const indent = line.match(/^(\s*)/)[1].length;
5514
5603
  if (indent < baseIndent) break;
5515
5604
  if (indent === baseIndent) {
@@ -5522,11 +5611,12 @@
5522
5611
  const checked = isCB && raw[1].toLowerCase() === "x";
5523
5612
  const text = isCB ? raw.replace(/^\[[ xX]\]\s+/, "") : raw;
5524
5613
  items.push({
5525
- text,
5614
+ paras: [text],
5526
5615
  isCB,
5527
5616
  checked,
5528
5617
  sub: ""
5529
5618
  });
5619
+ pendingBlank = false;
5530
5620
  i++;
5531
5621
  } else {
5532
5622
  if (!items.length) {
@@ -5537,53 +5627,143 @@
5537
5627
  const nested = _parseListBlock(lines, i);
5538
5628
  items[items.length - 1].sub += nested.html;
5539
5629
  i = nested.endIdx;
5630
+ pendingBlank = false;
5631
+ } else if (pendingBlank) {
5632
+ items[items.length - 1].paras.push(line.trim());
5633
+ pendingBlank = false;
5634
+ i++;
5540
5635
  } else {
5541
- items[items.length - 1].text += " " + line.trim();
5636
+ const paras = items[items.length - 1].paras;
5637
+ paras[paras.length - 1] += " " + line.trim();
5542
5638
  i++;
5543
5639
  }
5544
5640
  }
5545
5641
  }
5546
- const open = isOL ? "<ol>" : !isOL && firstIsCB === true ? "<ul class=\"an-checklist\">" : "<ul>";
5642
+ const hasCB = !isOL && firstIsCB === true;
5643
+ const startMatch = isOL ? /^\s*(\d+)\. /.exec(lines[startIdx]) : null;
5644
+ const startNum = startMatch ? Number.parseInt(startMatch[1], 10) : 1;
5645
+ const open = isOL ? startNum !== 1 ? `<ol start="${startNum}">` : "<ol>" : hasCB ? "<ul class=\"an-checklist\">" : "<ul>";
5547
5646
  const close = isOL ? "</ol>" : "</ul>";
5548
5647
  return {
5549
- html: `${open}${items.map(({ text, isCB, checked, sub }) => {
5550
- return `<li>${isCB ? `<input type="checkbox" contenteditable="false"${checked ? " checked" : ""}>` : ""}${_inline(text)}${sub}</li>`;
5648
+ html: `${open}${items.map(({ paras, isCB, checked, sub }) => {
5649
+ const cbHTML = isCB ? `<input type="checkbox" contenteditable="false"${checked ? " checked" : ""}>` : "";
5650
+ return `<li>${loose ? paras.map((p, idx) => `<p>${idx === 0 ? cbHTML : ""}${_inline(p)}</p>`).join("") : `${cbHTML}${_inline(paras[0])}`}${sub}</li>`;
5551
5651
  }).join("")}${close}`,
5552
5652
  endIdx: i
5553
5653
  };
5554
5654
  }
5555
- function _inline(text) {
5556
- text = text.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, (_, alt, src) => `<img src="${_escAttr(src)}" alt="${_escAttr(alt)}" class="an-image">`);
5557
- text = text.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_, label, href) => `<a href="${_escAttr(href)}">${_esc(label)}</a>`);
5655
+ var ESCAPABLE_RE = /\\([*_`#[\]()>\\~|])/g;
5656
+ var MARK = String.fromCharCode(0);
5657
+ /**
5658
+ * Step 0 of _inline(): replaces backslash-escaped punctuation with inert
5659
+ * placeholders so later syntax regexes can't match them.
5660
+ * @param {string} text
5661
+ * @returns {{ text: string, literals: string[] }}
5662
+ */
5663
+ function _extractBackslashEscapes(text) {
5664
+ const literals = [];
5665
+ return {
5666
+ text: text.replace(ESCAPABLE_RE, (_, ch) => {
5667
+ literals.push(ch);
5668
+ return `${MARK}${literals.length - 1}${MARK}`;
5669
+ }),
5670
+ literals
5671
+ };
5672
+ }
5673
+ /**
5674
+ * Restores placeholders from _extractBackslashEscapes(), HTML-escaping each
5675
+ * literal since it's inserted directly into the output.
5676
+ * @param {string} text
5677
+ * @param {string[]} literals
5678
+ * @returns {string}
5679
+ */
5680
+ function _restoreBackslashEscapes(text, literals) {
5681
+ return text.replace(new RegExp(`${MARK}(\\d+)${MARK}`, "g"), (_, idx) => _esc(literals[Number(idx)]));
5682
+ }
5683
+ /**
5684
+ * Resolves images, inline links, GFM reference-style links (explicit,
5685
+ * shortcut, and bare/implicit forms), and footnote markers. Must run on text
5686
+ * already passed through _esc() — see _inline()'s Step 1 comment.
5687
+ * @param {string} text
5688
+ * @returns {string}
5689
+ */
5690
+ function _resolveLinksAndFootnotes(text) {
5691
+ text = text.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, (_, alt, src) => `<img src="${_escAttrQuotes(src)}" alt="${_escAttrQuotes(alt)}" class="an-image">`);
5692
+ text = text.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_, label, href) => `<a href="${_escAttrQuotes(href)}">${label}</a>`);
5558
5693
  text = text.replace(/\[([^\]]+)\]\[([^\]]*)\]/g, (m, label, ref) => {
5559
- const def = _linkDefs.get((ref || label).trim().toLowerCase());
5694
+ const def = _linkDefs.get(_unescAmpLtGt(ref || label).trim().toLowerCase());
5560
5695
  if (!def) return m;
5561
5696
  const titleAttr = def.title ? ` title="${_escAttr(def.title)}"` : "";
5562
- return `<a href="${_escAttr(def.href)}"${titleAttr}>${_esc(label)}</a>`;
5697
+ return `<a href="${_escAttr(def.href)}"${titleAttr}>${label}</a>`;
5563
5698
  });
5564
5699
  text = text.replace(/\[([^\]]+)\]/g, (m, label) => {
5565
- const def = _linkDefs.get(label.trim().toLowerCase());
5700
+ const def = _linkDefs.get(_unescAmpLtGt(label).trim().toLowerCase());
5566
5701
  if (!def) return m;
5567
5702
  const titleAttr = def.title ? ` title="${_escAttr(def.title)}"` : "";
5568
- return `<a href="${_escAttr(def.href)}"${titleAttr}>${_esc(label)}</a>`;
5703
+ return `<a href="${_escAttr(def.href)}"${titleAttr}>${label}</a>`;
5704
+ });
5705
+ text = text.replace(/\[\^([^\]]+)\]/g, (m, id) => _footnoteIds.has(_unescAmpLtGt(id)) ? `<sup>[${id}]</sup>` : m);
5706
+ return text;
5707
+ }
5708
+ /**
5709
+ * Converts angle-bracket (`<https://...>`) and bare (`https://...`)
5710
+ * autolinks. Runs after _resolveLinksAndFootnotes() so an already-linked URL
5711
+ * isn't reprocessed, and on already-_esc()'d text (see _inline()).
5712
+ * @param {string} text
5713
+ * @returns {string}
5714
+ */
5715
+ function _applyAutolinks(text) {
5716
+ text = text.replace(/&lt;(https?:\/\/[^\s&]+?)&gt;/g, (_, url) => `<a href="${_escAttrQuotes(url)}">${url}</a>`);
5717
+ text = text.replace(/(^|[\s(])(https?:\/\/[^\s()]+)/g, (m, pre, rawUrl) => {
5718
+ const trail = /[.,;:!?)]+$/.exec(rawUrl);
5719
+ const url = trail ? rawUrl.slice(0, -trail[0].length) : rawUrl;
5720
+ if (!url) return m;
5721
+ const suffix = trail ? trail[0] : "";
5722
+ return `${pre}<a href="${_escAttrQuotes(url)}">${url}</a>${suffix}`;
5569
5723
  });
5570
- text = text.replace(/\[\^([^\]]+)\]/g, (m, id) => _footnoteIds.has(id) ? `<sup>[${_esc(id)}]</sup>` : m);
5571
- text = text.replace(/\*{3}([^*\n]+?)\*{3}/g, (_, c) => `<strong><em>${_esc(c)}</em></strong>`);
5572
- text = text.replace(/_{3}([^_\n]+?)_{3}/g, (_, c) => `<strong><em>${_esc(c)}</em></strong>`);
5573
- text = text.replace(/\*{2}([^*\n]+?)\*{2}/g, (_, c) => `<strong>${_esc(c)}</strong>`);
5574
- text = text.replace(/_{2}([^_\n]+?)_{2}/g, (_, c) => `<strong>${_esc(c)}</strong>`);
5575
- text = text.replace(/\*([^*\n]+?)\*/g, (_, c) => `<em>${_esc(c)}</em>`);
5576
- text = text.replace(/_([^_\n]+?)_/g, (_, c) => `<em>${_esc(c)}</em>`);
5577
- text = text.replace(/~~([^~\n]+?)~~/g, (_, c) => `<del>${_esc(c)}</del>`);
5578
- text = text.replace(/`([^`]+)`/g, (_, c) => `<code>${_esc(c)}</code>`);
5579
5724
  return text;
5580
5725
  }
5726
+ /**
5727
+ * Applies bold/italic/bold-italic (asterisk and underscore forms — underscore
5728
+ * requires a non-word-character boundary per CommonMark), strikethrough, and
5729
+ * inline code.
5730
+ * @param {string} text
5731
+ * @returns {string}
5732
+ */
5733
+ function _applyEmphasisAndCode(text) {
5734
+ text = text.replace(/\*{3}([^*\n]+?)\*{3}/g, (_, c) => `<strong><em>${c}</em></strong>`);
5735
+ text = text.replace(/(?<!\w)_{3}([^_\n]+?)_{3}(?!\w)/g, (_, c) => `<strong><em>${c}</em></strong>`);
5736
+ text = text.replace(/\*{2}([^*\n]+?)\*{2}/g, (_, c) => `<strong>${c}</strong>`);
5737
+ text = text.replace(/(?<!\w)_{2}([^_\n]+?)_{2}(?!\w)/g, (_, c) => `<strong>${c}</strong>`);
5738
+ text = text.replace(/\*([^*\n]+?)\*/g, (_, c) => `<em>${c}</em>`);
5739
+ text = text.replace(/(?<!\w)_([^_\n]+?)_(?!\w)/g, (_, c) => `<em>${c}</em>`);
5740
+ text = text.replace(/~~([^~\n]+?)~~/g, (_, c) => `<del>${c}</del>`);
5741
+ text = text.replace(/``([\s\S]*?)``/g, (_, c) => `<code>${c}</code>`);
5742
+ text = text.replace(/`([^`]+)`/g, (_, c) => `<code>${c}</code>`);
5743
+ return text;
5744
+ }
5745
+ function _inline(text) {
5746
+ const { text: withoutEscapes, literals } = _extractBackslashEscapes(text);
5747
+ let result = _esc(withoutEscapes);
5748
+ result = _resolveLinksAndFootnotes(result);
5749
+ result = _applyAutolinks(result);
5750
+ result = _applyEmphasisAndCode(result);
5751
+ return _restoreBackslashEscapes(result, literals);
5752
+ }
5581
5753
  function _esc(v) {
5582
5754
  return String(v).replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
5583
5755
  }
5584
5756
  function _escAttr(v) {
5585
5757
  return String(v).replaceAll("&", "&amp;").replaceAll("\"", "&quot;").replaceAll("'", "&#39;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
5586
5758
  }
5759
+ /** Escapes only quote characters — for attribute values already run through _esc(). */
5760
+ function _escAttrQuotes(v) {
5761
+ return String(v).replaceAll("\"", "&quot;").replaceAll("'", "&#39;");
5762
+ }
5763
+ /** Reverses _esc()'s &amp;/&lt;/&gt; substitutions, for matching against un-escaped _linkDefs/_footnoteIds keys. */
5764
+ function _unescAmpLtGt(v) {
5765
+ return String(v).replaceAll("&lt;", "<").replaceAll("&gt;", ">").replaceAll("&amp;", "&");
5766
+ }
5587
5767
  //#endregion
5588
5768
  //#region src/js/core/detectLang.js
5589
5769
  /**
@@ -7139,6 +7319,17 @@
7139
7319
  return doc.body.innerHTML;
7140
7320
  }
7141
7321
  /**
7322
+ * Checks whether an HTML payload has no semantic markup beyond plain
7323
+ * wrapper elements (e.g. a bare <div>/<p>). Used to decide whether a
7324
+ * markdown-shaped plain-text paste should win over an accompanying HTML
7325
+ * payload that isn't actually carrying any real rich-text formatting.
7326
+ * @param {string} html
7327
+ * @returns {boolean}
7328
+ */
7329
+ _isTriviallyPlainHtml(html) {
7330
+ 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");
7331
+ }
7332
+ /**
7142
7333
  * Forces the next paste operation to strip all HTML formatting.
7143
7334
  * Called by Editor when Ctrl+Shift+V is pressed.
7144
7335
  * @param {boolean} val
@@ -7155,8 +7346,16 @@
7155
7346
  if (maxBytes > 0) {
7156
7347
  const text = clipboardData.getData("text/plain") || "";
7157
7348
  const html = clipboardData.getData("text/html") || "";
7158
- if (Math.max(text.length, html.length) > maxBytes) {
7349
+ const size = Math.max(text.length, html.length);
7350
+ if (size > maxBytes) {
7159
7351
  event.preventDefault();
7352
+ const message = `Pasted content (${size} bytes) exceeds the ${this.options.maxPasteSize ?? 5} MB paste size limit.`;
7353
+ this.context.triggerEvent("pasteError", {
7354
+ size,
7355
+ maxBytes,
7356
+ message
7357
+ });
7358
+ console.warn(`[AutumnNote] ${message}`);
7160
7359
  return;
7161
7360
  }
7162
7361
  }
@@ -7179,9 +7378,12 @@
7179
7378
  this.context.invoke("editor.afterCommand");
7180
7379
  return;
7181
7380
  }
7182
- if (this.options.markdownPaste !== false && !clipboardData.types.includes("text/html")) {
7381
+ if (this.options.markdownPaste !== false) {
7382
+ const hasHtml = clipboardData.types.includes("text/html");
7383
+ const html = hasHtml ? clipboardData.getData("text/html") : "";
7384
+ const htmlTriviallyPlain = !hasHtml || this._isTriviallyPlainHtml(html);
7183
7385
  const text = clipboardData.getData("text/plain");
7184
- if (text && isMarkdown(text)) {
7386
+ if (text && htmlTriviallyPlain && isMarkdown(text)) {
7185
7387
  event.preventDefault();
7186
7388
  execCommand("insertHTML", sanitiseHTML(markdownToHTML(text)));
7187
7389
  this.context.invoke("editor.afterCommand");
@@ -7214,11 +7416,51 @@
7214
7416
  const dt = event.dataTransfer;
7215
7417
  if (!dt?.files?.length) return;
7216
7418
  const imageFiles = Array.from(dt.files).filter((f) => f.type.startsWith("image/"));
7217
- if (imageFiles.length === 0) return;
7218
- event.preventDefault();
7219
- event.stopPropagation();
7220
- this._placeCaretAtPoint(event.clientX, event.clientY);
7221
- this._insertImageFiles(imageFiles);
7419
+ if (imageFiles.length > 0) {
7420
+ event.preventDefault();
7421
+ event.stopPropagation();
7422
+ this._placeCaretAtPoint(event.clientX, event.clientY);
7423
+ this._insertImageFiles(imageFiles);
7424
+ return;
7425
+ }
7426
+ if (this.options.markdownPaste !== false) {
7427
+ const mdFile = Array.from(dt.files).find((f) => /\.md$/i.test(f.name) || f.type === "text/markdown");
7428
+ if (mdFile) {
7429
+ event.preventDefault();
7430
+ event.stopPropagation();
7431
+ this._placeCaretAtPoint(event.clientX, event.clientY);
7432
+ this._insertMarkdownFile(mdFile);
7433
+ }
7434
+ }
7435
+ }
7436
+ /**
7437
+ * Reads a dropped `.md` File and inserts it converted to HTML at the
7438
+ * current caret. Skips the isMarkdown() heuristic — an explicit `.md`
7439
+ * extension/MIME type is an unambiguous signal, unlike pasted plain text.
7440
+ * @param {File} file
7441
+ */
7442
+ _insertMarkdownFile(file) {
7443
+ const maxBytes = (this.options.maxPasteSize ?? 5) * 1024 * 1024;
7444
+ if (maxBytes > 0 && file.size > maxBytes) {
7445
+ const message = `Dropped file "${file.name}" (${file.size} bytes) exceeds the ${this.options.maxPasteSize ?? 5} MB paste size limit.`;
7446
+ this.context.triggerEvent("pasteError", {
7447
+ size: file.size,
7448
+ maxBytes,
7449
+ message
7450
+ });
7451
+ console.warn(`[AutumnNote] ${message}`);
7452
+ return;
7453
+ }
7454
+ const reader = new FileReader();
7455
+ reader.onload = (e) => {
7456
+ execCommand("insertHTML", sanitiseHTML(markdownToHTML(
7457
+ /** @type {string} */
7458
+ e.target.result || ""
7459
+ )));
7460
+ this.context.invoke("editor.afterCommand");
7461
+ };
7462
+ reader.onerror = () => console.warn("[AutumnNote] Failed to read dropped markdown file", file.name);
7463
+ reader.readAsText(file);
7222
7464
  }
7223
7465
  /**
7224
7466
  * Inserts one or more image Files into the editor.
@@ -17256,7 +17498,7 @@
17256
17498
  /** All pre-built button definitions — accessible in every module format including UMD/CJS. */
17257
17499
  buttons,
17258
17500
  /** Library version */
17259
- version: "1.10.0"
17501
+ version: "1.11.0"
17260
17502
  };
17261
17503
  /**
17262
17504
  * @param {string|Element|NodeList|Element[]} selector