autumnnote 1.9.1 → 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,15 +5330,33 @@
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);
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
5317
5341
  * @returns {string}
5318
5342
  */
5319
5343
  function markdownToHTML(text) {
5320
- const lines = text.replaceAll("\r\n", "\n").replaceAll("\r", "\n").split("\n");
5344
+ let lines = text.replaceAll("\r\n", "\n").replaceAll("\r", "\n").split("\n");
5345
+ lines = _stripFrontmatter(lines);
5346
+ const refs = _extractReferenceDefinitions(lines);
5347
+ lines = refs.clean;
5348
+ _linkDefs = refs.linkDefs;
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) {
5321
5360
  const out = [];
5322
5361
  let i = 0;
5323
5362
  while (i < lines.length) {
@@ -5336,7 +5375,7 @@
5336
5375
  i++;
5337
5376
  continue;
5338
5377
  }
5339
- 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) {
5340
5379
  if (/^=+\s*$/.test(lines[i + 1])) {
5341
5380
  out.push(`<h1>${_inline(line.trim())}</h1>`);
5342
5381
  i += 2;
@@ -5348,7 +5387,7 @@
5348
5387
  continue;
5349
5388
  }
5350
5389
  }
5351
- if (/^(-{3,}|\*{3,}|_{3,})\s*$/.test(line)) {
5390
+ if (HR_RE.test(line)) {
5352
5391
  out.push("<hr>");
5353
5392
  i++;
5354
5393
  continue;
@@ -5356,17 +5395,18 @@
5356
5395
  const hMatch = /^(#{1,6})\s+(.+)$/.exec(line);
5357
5396
  if (hMatch) {
5358
5397
  const level = hMatch[1].length;
5359
- 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}>`);
5360
5400
  i++;
5361
5401
  continue;
5362
5402
  }
5363
- if (line.startsWith("> ")) {
5403
+ if (BQ_RE.test(line)) {
5364
5404
  const bqLines = [];
5365
- while (i < lines.length && lines[i].startsWith("> ")) {
5366
- 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]);
5367
5407
  i++;
5368
5408
  }
5369
- out.push(`<blockquote>${bqLines.map(_inline).join("<br>")}</blockquote>`);
5409
+ out.push(`<blockquote>${_parseBlocks(bqLines)}</blockquote>`);
5370
5410
  continue;
5371
5411
  }
5372
5412
  if (/^[-*+] /.test(line)) {
@@ -5409,32 +5449,156 @@
5409
5449
  continue;
5410
5450
  }
5411
5451
  const paraLines = [];
5412
- 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]))) {
5413
5453
  paraLines.push(lines[i]);
5414
5454
  i++;
5415
5455
  }
5416
- 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>`);
5417
5457
  }
5418
5458
  return out.join("");
5419
5459
  }
5460
+ /** Reference-link and footnote definitions collected per markdownToHTML() call. */
5461
+ var _linkDefs = /* @__PURE__ */ new Map();
5462
+ var _footnoteIds = /* @__PURE__ */ new Set();
5463
+ /**
5464
+ * Strips a leading YAML frontmatter block (--- ... --- or --- ... ...) from
5465
+ * the line array, only when it is the very first line and the enclosed body
5466
+ * looks like YAML (key: value / list items / indented continuations) — this
5467
+ * disambiguates real frontmatter from a horizontal rule followed by prose.
5468
+ * @param {string[]} lines
5469
+ * @returns {string[]}
5470
+ */
5471
+ function _stripFrontmatter(lines) {
5472
+ if ((lines[0] || "").trim() !== "---") return lines;
5473
+ let closeIdx = -1;
5474
+ for (let j = 1; j < lines.length; j++) {
5475
+ const t = lines[j].trim();
5476
+ if (t === "---" || t === "...") {
5477
+ closeIdx = j;
5478
+ break;
5479
+ }
5480
+ }
5481
+ if (closeIdx === -1) return lines;
5482
+ if (!lines.slice(1, closeIdx).every((l) => l.trim() === "" || /^[ \t]*[\w$.-]+\s*:(\s|$)/.test(l) || /^[ \t]*-\s+\S/.test(l) || /^[ \t]+\S/.test(l))) return lines;
5483
+ let start = closeIdx + 1;
5484
+ if (lines[start] !== void 0 && lines[start].trim() === "") start++;
5485
+ return lines.slice(start);
5486
+ }
5420
5487
  /**
5421
- * Splits a GFM table row string into trimmed cell strings.
5422
- * '| a | b | c |' → ['a', 'b', 'c']
5488
+ * Extracts GFM reference-link definitions (`[ref]: url "title"`) and footnote
5489
+ * definitions (`[^id]: text`) from the line array, skipping fenced code
5490
+ * regions. Returns the definition-free line array plus lookup maps.
5491
+ * @param {string[]} lines
5492
+ * @returns {{ clean: string[], linkDefs: Map<string, {href: string, title?: string}>, footnoteIds: Set<string> }}
5493
+ */
5494
+ function _extractReferenceDefinitions(lines) {
5495
+ const linkDefs = /* @__PURE__ */ new Map();
5496
+ const footnoteIds = /* @__PURE__ */ new Set();
5497
+ const clean = [];
5498
+ let inFence = false;
5499
+ const linkDefRe = /^\[([^\]]+)\]:\s*(\S+)(?:\s+"([^"]*)")?\s*$/;
5500
+ const footnoteDefRe = /^\[\^([^\]]+)\]:\s*(.+)$/;
5501
+ for (const line of lines) {
5502
+ if (/^```/.test(line)) {
5503
+ inFence = !inFence;
5504
+ clean.push(line);
5505
+ continue;
5506
+ }
5507
+ if (!inFence) {
5508
+ const fm = footnoteDefRe.exec(line);
5509
+ if (fm) {
5510
+ footnoteIds.add(fm[1]);
5511
+ continue;
5512
+ }
5513
+ const lm = linkDefRe.exec(line);
5514
+ if (lm) {
5515
+ linkDefs.set(lm[1].trim().toLowerCase(), {
5516
+ href: lm[2],
5517
+ title: lm[3]
5518
+ });
5519
+ continue;
5520
+ }
5521
+ }
5522
+ clean.push(line);
5523
+ }
5524
+ return {
5525
+ clean,
5526
+ linkDefs,
5527
+ footnoteIds
5528
+ };
5529
+ }
5530
+ /**
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']
5423
5558
  * @param {string} row
5424
5559
  * @returns {string[]}
5425
5560
  */
5426
5561
  function _parseTableRow(row) {
5427
- 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());
5428
5580
  }
5429
5581
  function _parseListBlock(lines, startIdx) {
5430
5582
  const baseIndent = lines[startIdx].match(/^(\s*)/)[1].length;
5431
5583
  const isOL = /^\s*\d+\. /.test(lines[startIdx]);
5432
5584
  const items = [];
5433
5585
  let firstIsCB = null;
5586
+ let loose = false;
5587
+ let pendingBlank = false;
5434
5588
  let i = startIdx;
5435
5589
  while (i < lines.length) {
5436
5590
  const line = lines[i];
5437
- 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
+ }
5438
5602
  const indent = line.match(/^(\s*)/)[1].length;
5439
5603
  if (indent < baseIndent) break;
5440
5604
  if (indent === baseIndent) {
@@ -5447,11 +5611,12 @@
5447
5611
  const checked = isCB && raw[1].toLowerCase() === "x";
5448
5612
  const text = isCB ? raw.replace(/^\[[ xX]\]\s+/, "") : raw;
5449
5613
  items.push({
5450
- text,
5614
+ paras: [text],
5451
5615
  isCB,
5452
5616
  checked,
5453
5617
  sub: ""
5454
5618
  });
5619
+ pendingBlank = false;
5455
5620
  i++;
5456
5621
  } else {
5457
5622
  if (!items.length) {
@@ -5462,40 +5627,143 @@
5462
5627
  const nested = _parseListBlock(lines, i);
5463
5628
  items[items.length - 1].sub += nested.html;
5464
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++;
5465
5635
  } else {
5466
- items[items.length - 1].text += " " + line.trim();
5636
+ const paras = items[items.length - 1].paras;
5637
+ paras[paras.length - 1] += " " + line.trim();
5467
5638
  i++;
5468
5639
  }
5469
5640
  }
5470
5641
  }
5471
- 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>";
5472
5646
  const close = isOL ? "</ol>" : "</ul>";
5473
5647
  return {
5474
- html: `${open}${items.map(({ text, isCB, checked, sub }) => {
5475
- 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>`;
5476
5651
  }).join("")}${close}`,
5477
5652
  endIdx: i
5478
5653
  };
5479
5654
  }
5480
- function _inline(text) {
5481
- text = text.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, (_, alt, src) => `<img src="${_escAttr(src)}" alt="${_escAttr(alt)}" class="an-image">`);
5482
- text = text.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_, label, href) => `<a href="${_escAttr(href)}">${_esc(label)}</a>`);
5483
- text = text.replace(/\*{3}([^*\n]+?)\*{3}/g, (_, c) => `<strong><em>${_esc(c)}</em></strong>`);
5484
- text = text.replace(/_{3}([^_\n]+?)_{3}/g, (_, c) => `<strong><em>${_esc(c)}</em></strong>`);
5485
- text = text.replace(/\*{2}([^*\n]+?)\*{2}/g, (_, c) => `<strong>${_esc(c)}</strong>`);
5486
- text = text.replace(/_{2}([^_\n]+?)_{2}/g, (_, c) => `<strong>${_esc(c)}</strong>`);
5487
- text = text.replace(/\*([^*\n]+?)\*/g, (_, c) => `<em>${_esc(c)}</em>`);
5488
- text = text.replace(/_([^_\n]+?)_/g, (_, c) => `<em>${_esc(c)}</em>`);
5489
- text = text.replace(/~~([^~\n]+?)~~/g, (_, c) => `<del>${_esc(c)}</del>`);
5490
- text = text.replace(/`([^`]+)`/g, (_, c) => `<code>${_esc(c)}</code>`);
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>`);
5693
+ text = text.replace(/\[([^\]]+)\]\[([^\]]*)\]/g, (m, label, ref) => {
5694
+ const def = _linkDefs.get(_unescAmpLtGt(ref || label).trim().toLowerCase());
5695
+ if (!def) return m;
5696
+ const titleAttr = def.title ? ` title="${_escAttr(def.title)}"` : "";
5697
+ return `<a href="${_escAttr(def.href)}"${titleAttr}>${label}</a>`;
5698
+ });
5699
+ text = text.replace(/\[([^\]]+)\]/g, (m, label) => {
5700
+ const def = _linkDefs.get(_unescAmpLtGt(label).trim().toLowerCase());
5701
+ if (!def) return m;
5702
+ const titleAttr = def.title ? ` title="${_escAttr(def.title)}"` : "";
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);
5491
5706
  return text;
5492
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}`;
5723
+ });
5724
+ return text;
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
+ }
5493
5753
  function _esc(v) {
5494
5754
  return String(v).replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
5495
5755
  }
5496
5756
  function _escAttr(v) {
5497
5757
  return String(v).replaceAll("&", "&amp;").replaceAll("\"", "&quot;").replaceAll("'", "&#39;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
5498
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
+ }
5499
5767
  //#endregion
5500
5768
  //#region src/js/core/detectLang.js
5501
5769
  /**
@@ -7051,6 +7319,17 @@
7051
7319
  return doc.body.innerHTML;
7052
7320
  }
7053
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
+ /**
7054
7333
  * Forces the next paste operation to strip all HTML formatting.
7055
7334
  * Called by Editor when Ctrl+Shift+V is pressed.
7056
7335
  * @param {boolean} val
@@ -7067,8 +7346,16 @@
7067
7346
  if (maxBytes > 0) {
7068
7347
  const text = clipboardData.getData("text/plain") || "";
7069
7348
  const html = clipboardData.getData("text/html") || "";
7070
- if (Math.max(text.length, html.length) > maxBytes) {
7349
+ const size = Math.max(text.length, html.length);
7350
+ if (size > maxBytes) {
7071
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}`);
7072
7359
  return;
7073
7360
  }
7074
7361
  }
@@ -7091,9 +7378,12 @@
7091
7378
  this.context.invoke("editor.afterCommand");
7092
7379
  return;
7093
7380
  }
7094
- 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);
7095
7385
  const text = clipboardData.getData("text/plain");
7096
- if (text && isMarkdown(text)) {
7386
+ if (text && htmlTriviallyPlain && isMarkdown(text)) {
7097
7387
  event.preventDefault();
7098
7388
  execCommand("insertHTML", sanitiseHTML(markdownToHTML(text)));
7099
7389
  this.context.invoke("editor.afterCommand");
@@ -7126,11 +7416,51 @@
7126
7416
  const dt = event.dataTransfer;
7127
7417
  if (!dt?.files?.length) return;
7128
7418
  const imageFiles = Array.from(dt.files).filter((f) => f.type.startsWith("image/"));
7129
- if (imageFiles.length === 0) return;
7130
- event.preventDefault();
7131
- event.stopPropagation();
7132
- this._placeCaretAtPoint(event.clientX, event.clientY);
7133
- 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);
7134
7464
  }
7135
7465
  /**
7136
7466
  * Inserts one or more image Files into the editor.
@@ -17168,7 +17498,7 @@
17168
17498
  /** All pre-built button definitions — accessible in every module format including UMD/CJS. */
17169
17499
  buttons,
17170
17500
  /** Library version */
17171
- version: "1.9.1"
17501
+ version: "1.11.0"
17172
17502
  };
17173
17503
  /**
17174
17504
  * @param {string|Element|NodeList|Element[]} selector