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.
@@ -4356,7 +4356,10 @@
4356
4356
  "object",
4357
4357
  "embed",
4358
4358
  "form",
4359
- "base"
4359
+ "base",
4360
+ "template",
4361
+ "link",
4362
+ "meta"
4360
4363
  ];
4361
4364
  /** Tags whose element wrapper is stripped but content (child nodes) is preserved. */
4362
4365
  var UNWRAP_TAGS = new Set(["button"]);
@@ -4365,8 +4368,28 @@
4365
4368
  "href",
4366
4369
  "src",
4367
4370
  "action",
4368
- "formaction"
4371
+ "formaction",
4372
+ "xlink:href"
4369
4373
  ];
4374
+ /** Inline style properties the editor's own toolbar/table features persist on saved content. */
4375
+ var ALLOWED_STYLE_PROPS = new Set([
4376
+ "color",
4377
+ "background-color",
4378
+ "font-size",
4379
+ "line-height",
4380
+ "text-align",
4381
+ "vertical-align",
4382
+ "width",
4383
+ "min-width",
4384
+ "height",
4385
+ "min-height",
4386
+ "border-width",
4387
+ "border-style",
4388
+ "border-color",
4389
+ "padding"
4390
+ ]);
4391
+ /** Value patterns that are never safe regardless of property. */
4392
+ var DANGEROUS_STYLE_VALUE_RE = /url\s*\(|expression\s*\(|@import|javascript:|vbscript:|behavior\s*:|-moz-binding/i;
4370
4393
  /** Trusted hosts for iframe embeds when allowIframes is enabled. */
4371
4394
  var TRUSTED_IFRAME_HOSTS = new Set([
4372
4395
  "www.youtube.com",
@@ -4407,6 +4430,12 @@
4407
4430
  el.removeAttribute(attr.name);
4408
4431
  continue;
4409
4432
  }
4433
+ if (attr.name === "style") {
4434
+ const cleaned = sanitiseStyleValue(attr.value);
4435
+ if (cleaned) el.setAttribute("style", cleaned);
4436
+ else el.removeAttribute("style");
4437
+ continue;
4438
+ }
4410
4439
  if (URL_ATTRS.includes(attr.name)) {
4411
4440
  const val = attr.value.trim();
4412
4441
  if (/^(javascript|vbscript):/i.test(val)) {
@@ -4435,6 +4464,28 @@
4435
4464
  return doc.body.innerHTML;
4436
4465
  }
4437
4466
  /**
4467
+ * Filters a style attribute value down to an allowlisted set of CSS
4468
+ * properties (ALLOWED_STYLE_PROPS), dropping any declaration whose value
4469
+ * contains a dangerous construct — url(), expression(), an "import" rule,
4470
+ * javascript:/vbscript:, IE behavior/-moz-binding — regardless of property.
4471
+ * @param {string} value
4472
+ * @returns {string} The filtered declaration list, or '' if nothing survives.
4473
+ */
4474
+ function sanitiseStyleValue(value) {
4475
+ const kept = [];
4476
+ for (const decl of (value || "").split(";")) {
4477
+ const idx = decl.indexOf(":");
4478
+ if (idx === -1) continue;
4479
+ const prop = decl.slice(0, idx).trim().toLowerCase();
4480
+ const val = decl.slice(idx + 1).trim();
4481
+ if (!prop || !val) continue;
4482
+ if (!ALLOWED_STYLE_PROPS.has(prop)) continue;
4483
+ if (DANGEROUS_STYLE_VALUE_RE.test(val)) continue;
4484
+ kept.push(`${prop}: ${val}`);
4485
+ }
4486
+ return kept.join("; ");
4487
+ }
4488
+ /**
4438
4489
  * Returns true if iframe src points to an approved video host.
4439
4490
  * Relative, protocol-relative and invalid URLs are rejected.
4440
4491
  * @param {string} src
@@ -5215,6 +5266,17 @@
5215
5266
  * @param {number} [depth=0] - Current nesting depth used to indent nested list items.
5216
5267
  * @returns {string} The Markdown representation of the node subtree.
5217
5268
  */
5269
+ /**
5270
+ * Direct child elements matching a tag name. Used instead of the CSS
5271
+ * `:scope > tag` combinator, which this project's jsdom version resolves
5272
+ * incorrectly (matches descendants at any depth, not just direct children).
5273
+ * @param {Element} el
5274
+ * @param {string} tagName
5275
+ * @returns {Element[]}
5276
+ */
5277
+ function _directChildren(el, tagName) {
5278
+ return Array.from(el.children).filter((c) => c.tagName === tagName.toUpperCase());
5279
+ }
5218
5280
  function _domToMd(node, depth = 0) {
5219
5281
  if (node.nodeType === 3) return node.textContent.replace(/\s+/g, " ");
5220
5282
  if (node.nodeType !== 1) return "";
@@ -5240,6 +5302,12 @@
5240
5302
  case "strike": return `~~${inner()}~~`;
5241
5303
  case "sup": return `^${inner()}^`;
5242
5304
  case "sub": return `~${inner()}~`;
5305
+ case "u": return `<u>${inner()}</u>`;
5306
+ case "span": {
5307
+ const style = el.getAttribute("style") || "";
5308
+ if (/\b(color|background-color|font-size)\s*:/.test(style)) return `<span style="${_escAttr(style)}">${inner()}</span>`;
5309
+ return inner();
5310
+ }
5243
5311
  case "code":
5244
5312
  if (el.closest("pre")) return inner();
5245
5313
  return `\`${inner()}\``;
@@ -5248,7 +5316,10 @@
5248
5316
  const langMatch = /language-(\S+)/.exec(codeEl?.className || "");
5249
5317
  return `\n\n\`\`\`${langMatch ? langMatch[1] : ""}\n${(codeEl || el).textContent || ""}\n\`\`\`\n\n`;
5250
5318
  }
5251
- case "blockquote": return `\n\n${inner().trim().split("\n").map((l) => `> ${l}`).join("\n")}\n\n`;
5319
+ case "blockquote": {
5320
+ const rawLines = inner().trim().split("\n");
5321
+ return `\n\n${rawLines.filter((l, idx) => l.trim() !== "" || (rawLines[idx - 1] ?? "").trim() !== "").map((l) => l.trim() === "" ? ">" : `> ${l}`).join("\n")}\n\n`;
5322
+ }
5252
5323
  case "a": {
5253
5324
  const href = el.getAttribute("href") || "";
5254
5325
  return `[${inner()}](${href})`;
@@ -5258,22 +5329,20 @@
5258
5329
  return `![${el.getAttribute("alt") || ""}](${src})`;
5259
5330
  }
5260
5331
  case "ul": {
5261
- const items = Array.from(el.querySelectorAll(":scope > li"));
5332
+ const items = _directChildren(el, "li");
5262
5333
  if (!items.length) return inner();
5263
5334
  const indent = " ".repeat(depth);
5264
5335
  const isChecklist = el.classList.contains("an-checklist");
5265
5336
  const lines = items.map((li) => {
5337
+ const cb = _directChildren(li, "input").find((c) => c.getAttribute("type") === "checkbox");
5266
5338
  let prefix = "- ";
5267
- if (isChecklist) {
5268
- const cb = li.querySelector("input[type=\"checkbox\"]");
5269
- prefix = (cb ? cb.checked : false) ? "- [x] " : "- [ ] ";
5270
- }
5339
+ if (isChecklist || cb) prefix = (cb ? cb.checked : false) ? "- [x] " : "- [ ] ";
5271
5340
  return `${indent}${prefix}${_domToMd(li, depth + 1).trim()}`;
5272
5341
  }).join("\n");
5273
5342
  return depth === 0 ? `\n\n${lines}\n\n` : `\n${lines}`;
5274
5343
  }
5275
5344
  case "ol": {
5276
- const items = Array.from(el.querySelectorAll(":scope > li"));
5345
+ const items = _directChildren(el, "li");
5277
5346
  if (!items.length) return inner();
5278
5347
  const indent = " ".repeat(depth);
5279
5348
  const lines = items.map((li, i) => `${indent}${i + 1}. ${_domToMd(li, depth + 1).trim()}`).join("\n");
@@ -5282,19 +5351,22 @@
5282
5351
  case "li": return inner();
5283
5352
  case "hr": return "\n\n---\n\n";
5284
5353
  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`\|`)));
5354
+ const allRows = Array.from(el.querySelectorAll("tr"));
5355
+ if (!allRows.length) return inner();
5356
+ const firstRowIsHeader = !!_directChildren(el, "thead")[0] || allRows[0].children.length > 0 && Array.from(allRows[0].children).every((c) => c.tagName === "TH");
5357
+ const cellTexts = allRows.map((tr) => Array.from(tr.querySelectorAll("th, td")).map((c) => c.textContent.trim().replaceAll("|", String.raw`\|`)));
5288
5358
  const cols = Math.max(...cellTexts.map((r) => r.length));
5289
5359
  const padRow = (row) => {
5290
5360
  const r = [...row];
5291
5361
  while (r.length < cols) r.push("");
5292
5362
  return r;
5293
5363
  };
5364
+ const bodyStart = firstRowIsHeader ? 1 : 0;
5365
+ const headerCells = firstRowIsHeader ? padRow(cellTexts[0]) : new Array(cols).fill("");
5294
5366
  let md = "\n\n";
5295
- md += `| ${padRow(cellTexts[0]).join(" | ")} |\n`;
5367
+ md += `| ${headerCells.join(" | ")} |\n`;
5296
5368
  md += `| ${new Array(cols).fill("---").join(" | ")} |\n`;
5297
- for (let r = 1; r < cellTexts.length; r++) md += `| ${padRow(cellTexts[r]).join(" | ")} |\n`;
5369
+ for (let r = bodyStart; r < cellTexts.length; r++) md += `| ${padRow(cellTexts[r]).join(" | ")} |\n`;
5298
5370
  return md + "\n";
5299
5371
  }
5300
5372
  default: return inner();
@@ -5309,8 +5381,11 @@
5309
5381
  * @returns {boolean} `true` if any Markdown-like pattern is present, `false` otherwise.
5310
5382
  */
5311
5383
  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);
5384
+ 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
5385
  }
5386
+ var BQ_RE = /^ {0,3}>( ?)(.*)$/;
5387
+ var HR_RE = /^ {0,3}([-*_])( *\1){2,}\s*$/;
5388
+ var HARD_BREAK = String.fromCharCode(1);
5314
5389
  /**
5315
5390
  * Converts a Markdown string to an HTML string.
5316
5391
  * @param {string} text
@@ -5323,6 +5398,16 @@
5323
5398
  lines = refs.clean;
5324
5399
  _linkDefs = refs.linkDefs;
5325
5400
  _footnoteIds = refs.footnoteIds;
5401
+ return _parseBlocks(lines);
5402
+ }
5403
+ /**
5404
+ * Parses a line array into block-level HTML. Called recursively for content
5405
+ * nested inside a blockquote so nested quotes and block content (lists,
5406
+ * headings, etc.) inside `>` are parsed the same as top-level content.
5407
+ * @param {string[]} lines
5408
+ * @returns {string}
5409
+ */
5410
+ function _parseBlocks(lines) {
5326
5411
  const out = [];
5327
5412
  let i = 0;
5328
5413
  while (i < lines.length) {
@@ -5341,7 +5426,7 @@
5341
5426
  i++;
5342
5427
  continue;
5343
5428
  }
5344
- if (line.trim() && !/^(-{3,}|\*{3,}|_{3,})\s*$/.test(line) && !/^#{1,6} /.test(line) && i + 1 < lines.length) {
5429
+ if (line.trim() && !HR_RE.test(line) && !/^#{1,6} /.test(line) && i + 1 < lines.length) {
5345
5430
  if (/^=+\s*$/.test(lines[i + 1])) {
5346
5431
  out.push(`<h1>${_inline(line.trim())}</h1>`);
5347
5432
  i += 2;
@@ -5353,7 +5438,7 @@
5353
5438
  continue;
5354
5439
  }
5355
5440
  }
5356
- if (/^(-{3,}|\*{3,}|_{3,})\s*$/.test(line)) {
5441
+ if (HR_RE.test(line)) {
5357
5442
  out.push("<hr>");
5358
5443
  i++;
5359
5444
  continue;
@@ -5361,17 +5446,18 @@
5361
5446
  const hMatch = /^(#{1,6})\s+(.+)$/.exec(line);
5362
5447
  if (hMatch) {
5363
5448
  const level = hMatch[1].length;
5364
- out.push(`<h${level}>${_inline(hMatch[2])}</h${level}>`);
5449
+ const content = hMatch[2].replace(/(?:^|\s)#+\s*$/, "");
5450
+ out.push(`<h${level}>${_inline(content)}</h${level}>`);
5365
5451
  i++;
5366
5452
  continue;
5367
5453
  }
5368
- if (line.startsWith("> ")) {
5454
+ if (BQ_RE.test(line)) {
5369
5455
  const bqLines = [];
5370
- while (i < lines.length && lines[i].startsWith("> ")) {
5371
- bqLines.push(lines[i].slice(2));
5456
+ while (i < lines.length && BQ_RE.test(lines[i])) {
5457
+ bqLines.push(BQ_RE.exec(lines[i])[2]);
5372
5458
  i++;
5373
5459
  }
5374
- out.push(`<blockquote>${bqLines.map(_inline).join("<br>")}</blockquote>`);
5460
+ out.push(`<blockquote>${_parseBlocks(bqLines)}</blockquote>`);
5375
5461
  continue;
5376
5462
  }
5377
5463
  if (/^[-*+] /.test(line)) {
@@ -5414,11 +5500,11 @@
5414
5500
  continue;
5415
5501
  }
5416
5502
  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]))) {
5503
+ 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
5504
  paraLines.push(lines[i]);
5419
5505
  i++;
5420
5506
  }
5421
- if (paraLines.length) out.push(`<p>${_inline(paraLines.join(" "))}</p>`);
5507
+ if (paraLines.length) out.push(`<p>${_inline(_joinParagraphLines(paraLines)).replaceAll(HARD_BREAK, "<br>")}</p>`);
5422
5508
  }
5423
5509
  return out.join("");
5424
5510
  }
@@ -5493,23 +5579,77 @@
5493
5579
  };
5494
5580
  }
5495
5581
  /**
5496
- * Splits a GFM table row string into trimmed cell strings.
5497
- * '| a | b | c |' → ['a', 'b', 'c']
5582
+ * Joins a paragraph's source lines into one string, converting CommonMark
5583
+ * hard-break markers (a trailing backslash, or 2+ trailing spaces) on all
5584
+ * but the last line into a HARD_BREAK placeholder instead of a plain space.
5585
+ * @param {string[]} paraLines
5586
+ * @returns {string}
5587
+ */
5588
+ function _joinParagraphLines(paraLines) {
5589
+ let joined = "";
5590
+ for (let idx = 0; idx < paraLines.length; idx++) {
5591
+ const isLast = idx === paraLines.length - 1;
5592
+ const ln = paraLines[idx];
5593
+ if (!isLast && /\\$/.test(ln)) {
5594
+ joined += ln.replace(/\\$/, "") + HARD_BREAK;
5595
+ continue;
5596
+ }
5597
+ if (!isLast && / {2,}$/.test(ln)) {
5598
+ joined += ln.replace(/ {2,}$/, "") + HARD_BREAK;
5599
+ continue;
5600
+ }
5601
+ joined += ln + (isLast ? "" : " ");
5602
+ }
5603
+ return joined;
5604
+ }
5605
+ /**
5606
+ * Splits a GFM table row string into trimmed cell strings, treating an
5607
+ * escaped pipe (`\|`) as a literal character rather than a cell separator.
5608
+ * '| a | b | c |' → ['a', 'b', 'c']; '| a\|b | c |' → ['a|b', 'c']
5498
5609
  * @param {string} row
5499
5610
  * @returns {string[]}
5500
5611
  */
5501
5612
  function _parseTableRow(row) {
5502
- return row.replace(/^\|/, "").replace(/\|$/, "").split("|").map((c) => c.trim());
5613
+ const trimmed = row.replace(/^\|/, "").replace(/\|$/, "");
5614
+ const cells = [];
5615
+ let cur = "";
5616
+ for (let i = 0; i < trimmed.length; i++) {
5617
+ if (trimmed[i] === "\\" && trimmed[i + 1] === "|") {
5618
+ cur += "|";
5619
+ i++;
5620
+ continue;
5621
+ }
5622
+ if (trimmed[i] === "|") {
5623
+ cells.push(cur);
5624
+ cur = "";
5625
+ continue;
5626
+ }
5627
+ cur += trimmed[i];
5628
+ }
5629
+ cells.push(cur);
5630
+ return cells.map((c) => c.trim());
5503
5631
  }
5504
5632
  function _parseListBlock(lines, startIdx) {
5505
5633
  const baseIndent = lines[startIdx].match(/^(\s*)/)[1].length;
5506
5634
  const isOL = /^\s*\d+\. /.test(lines[startIdx]);
5507
5635
  const items = [];
5508
5636
  let firstIsCB = null;
5637
+ let loose = false;
5638
+ let pendingBlank = false;
5509
5639
  let i = startIdx;
5510
5640
  while (i < lines.length) {
5511
5641
  const line = lines[i];
5512
- if (line.trim() === "") break;
5642
+ if (line.trim() === "") {
5643
+ const next = lines[i + 1];
5644
+ const nextIndent = next !== void 0 ? next.match(/^(\s*)/)[1].length : -1;
5645
+ const nextIsSameItem = next !== void 0 && /^\s*(?:[-*+]|\d+\.) /.test(next) && /^\s*\d+\. /.test(next) === isOL && nextIndent === baseIndent;
5646
+ const nextIsContinuation = next !== void 0 && next.trim() !== "" && nextIndent > baseIndent;
5647
+ if (!items.length || !nextIsSameItem && !nextIsContinuation) break;
5648
+ loose = true;
5649
+ pendingBlank = true;
5650
+ i++;
5651
+ continue;
5652
+ }
5513
5653
  const indent = line.match(/^(\s*)/)[1].length;
5514
5654
  if (indent < baseIndent) break;
5515
5655
  if (indent === baseIndent) {
@@ -5522,11 +5662,12 @@
5522
5662
  const checked = isCB && raw[1].toLowerCase() === "x";
5523
5663
  const text = isCB ? raw.replace(/^\[[ xX]\]\s+/, "") : raw;
5524
5664
  items.push({
5525
- text,
5665
+ paras: [text],
5526
5666
  isCB,
5527
5667
  checked,
5528
5668
  sub: ""
5529
5669
  });
5670
+ pendingBlank = false;
5530
5671
  i++;
5531
5672
  } else {
5532
5673
  if (!items.length) {
@@ -5537,53 +5678,143 @@
5537
5678
  const nested = _parseListBlock(lines, i);
5538
5679
  items[items.length - 1].sub += nested.html;
5539
5680
  i = nested.endIdx;
5681
+ pendingBlank = false;
5682
+ } else if (pendingBlank) {
5683
+ items[items.length - 1].paras.push(line.trim());
5684
+ pendingBlank = false;
5685
+ i++;
5540
5686
  } else {
5541
- items[items.length - 1].text += " " + line.trim();
5687
+ const paras = items[items.length - 1].paras;
5688
+ paras[paras.length - 1] += " " + line.trim();
5542
5689
  i++;
5543
5690
  }
5544
5691
  }
5545
5692
  }
5546
- const open = isOL ? "<ol>" : !isOL && firstIsCB === true ? "<ul class=\"an-checklist\">" : "<ul>";
5693
+ const hasCB = !isOL && firstIsCB === true;
5694
+ const startMatch = isOL ? /^\s*(\d+)\. /.exec(lines[startIdx]) : null;
5695
+ const startNum = startMatch ? Number.parseInt(startMatch[1], 10) : 1;
5696
+ const open = isOL ? startNum !== 1 ? `<ol start="${startNum}">` : "<ol>" : hasCB ? "<ul class=\"an-checklist\">" : "<ul>";
5547
5697
  const close = isOL ? "</ol>" : "</ul>";
5548
5698
  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>`;
5699
+ html: `${open}${items.map(({ paras, isCB, checked, sub }) => {
5700
+ const cbHTML = isCB ? `<input type="checkbox" contenteditable="false"${checked ? " checked" : ""}>` : "";
5701
+ return `<li>${loose ? paras.map((p, idx) => `<p>${idx === 0 ? cbHTML : ""}${_inline(p)}</p>`).join("") : `${cbHTML}${_inline(paras[0])}`}${sub}</li>`;
5551
5702
  }).join("")}${close}`,
5552
5703
  endIdx: i
5553
5704
  };
5554
5705
  }
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>`);
5706
+ var ESCAPABLE_RE = /\\([*_`#[\]()>\\~|])/g;
5707
+ var MARK = String.fromCharCode(0);
5708
+ /**
5709
+ * Step 0 of _inline(): replaces backslash-escaped punctuation with inert
5710
+ * placeholders so later syntax regexes can't match them.
5711
+ * @param {string} text
5712
+ * @returns {{ text: string, literals: string[] }}
5713
+ */
5714
+ function _extractBackslashEscapes(text) {
5715
+ const literals = [];
5716
+ return {
5717
+ text: text.replace(ESCAPABLE_RE, (_, ch) => {
5718
+ literals.push(ch);
5719
+ return `${MARK}${literals.length - 1}${MARK}`;
5720
+ }),
5721
+ literals
5722
+ };
5723
+ }
5724
+ /**
5725
+ * Restores placeholders from _extractBackslashEscapes(), HTML-escaping each
5726
+ * literal since it's inserted directly into the output.
5727
+ * @param {string} text
5728
+ * @param {string[]} literals
5729
+ * @returns {string}
5730
+ */
5731
+ function _restoreBackslashEscapes(text, literals) {
5732
+ return text.replace(new RegExp(`${MARK}(\\d+)${MARK}`, "g"), (_, idx) => _esc(literals[Number(idx)]));
5733
+ }
5734
+ /**
5735
+ * Resolves images, inline links, GFM reference-style links (explicit,
5736
+ * shortcut, and bare/implicit forms), and footnote markers. Must run on text
5737
+ * already passed through _esc() — see _inline()'s Step 1 comment.
5738
+ * @param {string} text
5739
+ * @returns {string}
5740
+ */
5741
+ function _resolveLinksAndFootnotes(text) {
5742
+ text = text.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, (_, alt, src) => `<img src="${_escAttrQuotes(src)}" alt="${_escAttrQuotes(alt)}" class="an-image">`);
5743
+ text = text.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_, label, href) => `<a href="${_escAttrQuotes(href)}">${label}</a>`);
5558
5744
  text = text.replace(/\[([^\]]+)\]\[([^\]]*)\]/g, (m, label, ref) => {
5559
- const def = _linkDefs.get((ref || label).trim().toLowerCase());
5745
+ const def = _linkDefs.get(_unescAmpLtGt(ref || label).trim().toLowerCase());
5560
5746
  if (!def) return m;
5561
5747
  const titleAttr = def.title ? ` title="${_escAttr(def.title)}"` : "";
5562
- return `<a href="${_escAttr(def.href)}"${titleAttr}>${_esc(label)}</a>`;
5748
+ return `<a href="${_escAttr(def.href)}"${titleAttr}>${label}</a>`;
5563
5749
  });
5564
5750
  text = text.replace(/\[([^\]]+)\]/g, (m, label) => {
5565
- const def = _linkDefs.get(label.trim().toLowerCase());
5751
+ const def = _linkDefs.get(_unescAmpLtGt(label).trim().toLowerCase());
5566
5752
  if (!def) return m;
5567
5753
  const titleAttr = def.title ? ` title="${_escAttr(def.title)}"` : "";
5568
- return `<a href="${_escAttr(def.href)}"${titleAttr}>${_esc(label)}</a>`;
5754
+ return `<a href="${_escAttr(def.href)}"${titleAttr}>${label}</a>`;
5569
5755
  });
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>`);
5756
+ text = text.replace(/\[\^([^\]]+)\]/g, (m, id) => _footnoteIds.has(_unescAmpLtGt(id)) ? `<sup>[${id}]</sup>` : m);
5579
5757
  return text;
5580
5758
  }
5759
+ /**
5760
+ * Converts angle-bracket (`<https://...>`) and bare (`https://...`)
5761
+ * autolinks. Runs after _resolveLinksAndFootnotes() so an already-linked URL
5762
+ * isn't reprocessed, and on already-_esc()'d text (see _inline()).
5763
+ * @param {string} text
5764
+ * @returns {string}
5765
+ */
5766
+ function _applyAutolinks(text) {
5767
+ text = text.replace(/&lt;(https?:\/\/[^\s&]+?)&gt;/g, (_, url) => `<a href="${_escAttrQuotes(url)}">${url}</a>`);
5768
+ text = text.replace(/(^|[\s(])(https?:\/\/[^\s()]+)/g, (m, pre, rawUrl) => {
5769
+ const trail = /[.,;:!?)]+$/.exec(rawUrl);
5770
+ const url = trail ? rawUrl.slice(0, -trail[0].length) : rawUrl;
5771
+ if (!url) return m;
5772
+ const suffix = trail ? trail[0] : "";
5773
+ return `${pre}<a href="${_escAttrQuotes(url)}">${url}</a>${suffix}`;
5774
+ });
5775
+ return text;
5776
+ }
5777
+ /**
5778
+ * Applies bold/italic/bold-italic (asterisk and underscore forms — underscore
5779
+ * requires a non-word-character boundary per CommonMark), strikethrough, and
5780
+ * inline code.
5781
+ * @param {string} text
5782
+ * @returns {string}
5783
+ */
5784
+ function _applyEmphasisAndCode(text) {
5785
+ text = text.replace(/\*{3}([^*\n]+?)\*{3}/g, (_, c) => `<strong><em>${c}</em></strong>`);
5786
+ text = text.replace(/(?<!\w)_{3}([^_\n]+?)_{3}(?!\w)/g, (_, c) => `<strong><em>${c}</em></strong>`);
5787
+ text = text.replace(/\*{2}([^*\n]+?)\*{2}/g, (_, c) => `<strong>${c}</strong>`);
5788
+ text = text.replace(/(?<!\w)_{2}([^_\n]+?)_{2}(?!\w)/g, (_, c) => `<strong>${c}</strong>`);
5789
+ text = text.replace(/\*([^*\n]+?)\*/g, (_, c) => `<em>${c}</em>`);
5790
+ text = text.replace(/(?<!\w)_([^_\n]+?)_(?!\w)/g, (_, c) => `<em>${c}</em>`);
5791
+ text = text.replace(/~~([^~\n]+?)~~/g, (_, c) => `<del>${c}</del>`);
5792
+ text = text.replace(/``([\s\S]*?)``/g, (_, c) => `<code>${c}</code>`);
5793
+ text = text.replace(/`([^`]+)`/g, (_, c) => `<code>${c}</code>`);
5794
+ return text;
5795
+ }
5796
+ function _inline(text) {
5797
+ const { text: withoutEscapes, literals } = _extractBackslashEscapes(text);
5798
+ let result = _esc(withoutEscapes);
5799
+ result = _resolveLinksAndFootnotes(result);
5800
+ result = _applyAutolinks(result);
5801
+ result = _applyEmphasisAndCode(result);
5802
+ return _restoreBackslashEscapes(result, literals);
5803
+ }
5581
5804
  function _esc(v) {
5582
5805
  return String(v).replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
5583
5806
  }
5584
5807
  function _escAttr(v) {
5585
5808
  return String(v).replaceAll("&", "&amp;").replaceAll("\"", "&quot;").replaceAll("'", "&#39;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
5586
5809
  }
5810
+ /** Escapes only quote characters — for attribute values already run through _esc(). */
5811
+ function _escAttrQuotes(v) {
5812
+ return String(v).replaceAll("\"", "&quot;").replaceAll("'", "&#39;");
5813
+ }
5814
+ /** Reverses _esc()'s &amp;/&lt;/&gt; substitutions, for matching against un-escaped _linkDefs/_footnoteIds keys. */
5815
+ function _unescAmpLtGt(v) {
5816
+ return String(v).replaceAll("&lt;", "<").replaceAll("&gt;", ">").replaceAll("&amp;", "&");
5817
+ }
5587
5818
  //#endregion
5588
5819
  //#region src/js/core/detectLang.js
5589
5820
  /**
@@ -7139,6 +7370,17 @@
7139
7370
  return doc.body.innerHTML;
7140
7371
  }
7141
7372
  /**
7373
+ * Checks whether an HTML payload has no semantic markup beyond plain
7374
+ * wrapper elements (e.g. a bare <div>/<p>). Used to decide whether a
7375
+ * markdown-shaped plain-text paste should win over an accompanying HTML
7376
+ * payload that isn't actually carrying any real rich-text formatting.
7377
+ * @param {string} html
7378
+ * @returns {boolean}
7379
+ */
7380
+ _isTriviallyPlainHtml(html) {
7381
+ 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");
7382
+ }
7383
+ /**
7142
7384
  * Forces the next paste operation to strip all HTML formatting.
7143
7385
  * Called by Editor when Ctrl+Shift+V is pressed.
7144
7386
  * @param {boolean} val
@@ -7155,8 +7397,16 @@
7155
7397
  if (maxBytes > 0) {
7156
7398
  const text = clipboardData.getData("text/plain") || "";
7157
7399
  const html = clipboardData.getData("text/html") || "";
7158
- if (Math.max(text.length, html.length) > maxBytes) {
7400
+ const size = Math.max(text.length, html.length);
7401
+ if (size > maxBytes) {
7159
7402
  event.preventDefault();
7403
+ const message = `Pasted content (${size} bytes) exceeds the ${this.options.maxPasteSize ?? 5} MB paste size limit.`;
7404
+ this.context.triggerEvent("pasteError", {
7405
+ size,
7406
+ maxBytes,
7407
+ message
7408
+ });
7409
+ console.warn(`[AutumnNote] ${message}`);
7160
7410
  return;
7161
7411
  }
7162
7412
  }
@@ -7179,9 +7429,12 @@
7179
7429
  this.context.invoke("editor.afterCommand");
7180
7430
  return;
7181
7431
  }
7182
- if (this.options.markdownPaste !== false && !clipboardData.types.includes("text/html")) {
7432
+ if (this.options.markdownPaste !== false) {
7433
+ const hasHtml = clipboardData.types.includes("text/html");
7434
+ const html = hasHtml ? clipboardData.getData("text/html") : "";
7435
+ const htmlTriviallyPlain = !hasHtml || this._isTriviallyPlainHtml(html);
7183
7436
  const text = clipboardData.getData("text/plain");
7184
- if (text && isMarkdown(text)) {
7437
+ if (text && htmlTriviallyPlain && isMarkdown(text)) {
7185
7438
  event.preventDefault();
7186
7439
  execCommand("insertHTML", sanitiseHTML(markdownToHTML(text)));
7187
7440
  this.context.invoke("editor.afterCommand");
@@ -7214,11 +7467,51 @@
7214
7467
  const dt = event.dataTransfer;
7215
7468
  if (!dt?.files?.length) return;
7216
7469
  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);
7470
+ if (imageFiles.length > 0) {
7471
+ event.preventDefault();
7472
+ event.stopPropagation();
7473
+ this._placeCaretAtPoint(event.clientX, event.clientY);
7474
+ this._insertImageFiles(imageFiles);
7475
+ return;
7476
+ }
7477
+ if (this.options.markdownPaste !== false) {
7478
+ const mdFile = Array.from(dt.files).find((f) => /\.md$/i.test(f.name) || f.type === "text/markdown");
7479
+ if (mdFile) {
7480
+ event.preventDefault();
7481
+ event.stopPropagation();
7482
+ this._placeCaretAtPoint(event.clientX, event.clientY);
7483
+ this._insertMarkdownFile(mdFile);
7484
+ }
7485
+ }
7486
+ }
7487
+ /**
7488
+ * Reads a dropped `.md` File and inserts it converted to HTML at the
7489
+ * current caret. Skips the isMarkdown() heuristic — an explicit `.md`
7490
+ * extension/MIME type is an unambiguous signal, unlike pasted plain text.
7491
+ * @param {File} file
7492
+ */
7493
+ _insertMarkdownFile(file) {
7494
+ const maxBytes = (this.options.maxPasteSize ?? 5) * 1024 * 1024;
7495
+ if (maxBytes > 0 && file.size > maxBytes) {
7496
+ const message = `Dropped file "${file.name}" (${file.size} bytes) exceeds the ${this.options.maxPasteSize ?? 5} MB paste size limit.`;
7497
+ this.context.triggerEvent("pasteError", {
7498
+ size: file.size,
7499
+ maxBytes,
7500
+ message
7501
+ });
7502
+ console.warn(`[AutumnNote] ${message}`);
7503
+ return;
7504
+ }
7505
+ const reader = new FileReader();
7506
+ reader.onload = (e) => {
7507
+ execCommand("insertHTML", sanitiseHTML(markdownToHTML(
7508
+ /** @type {string} */
7509
+ e.target.result || ""
7510
+ )));
7511
+ this.context.invoke("editor.afterCommand");
7512
+ };
7513
+ reader.onerror = () => console.warn("[AutumnNote] Failed to read dropped markdown file", file.name);
7514
+ reader.readAsText(file);
7222
7515
  }
7223
7516
  /**
7224
7517
  * Inserts one or more image Files into the editor.
@@ -17256,7 +17549,7 @@
17256
17549
  /** All pre-built button definitions — accessible in every module format including UMD/CJS. */
17257
17550
  buttons,
17258
17551
  /** Library version */
17259
- version: "1.10.0"
17552
+ version: "1.11.1"
17260
17553
  };
17261
17554
  /**
17262
17555
  * @param {string|Element|NodeList|Element[]} selector