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.
- package/dist/autumnnote.css +40 -0
- package/dist/autumnnote.es.js +382 -52
- package/dist/autumnnote.es.js.map +1 -1
- package/dist/autumnnote.umd.js +382 -52
- package/dist/autumnnote.umd.js.map +1 -1
- package/package.json +1 -1
- package/src/js/core/markdown.js +361 -55
- package/src/js/index.js +1 -1
- package/src/js/module/Clipboard.js +69 -11
- package/src/styles/autumnnote.scss +23 -0
- package/types/index.d.ts +1 -1
package/dist/autumnnote.umd.js
CHANGED
|
@@ -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":
|
|
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 ``;
|
|
5259
5279
|
}
|
|
5260
5280
|
case "ul": {
|
|
5261
|
-
const items =
|
|
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 =
|
|
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
|
|
5286
|
-
if (!
|
|
5287
|
-
const
|
|
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 += `| ${
|
|
5316
|
+
md += `| ${headerCells.join(" | ")} |\n`;
|
|
5296
5317
|
md += `| ${new Array(cols).fill("---").join(" | ")} |\n`;
|
|
5297
|
-
for (let r =
|
|
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
|
-
|
|
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() &&
|
|
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 (
|
|
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
|
-
|
|
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 (
|
|
5403
|
+
if (BQ_RE.test(line)) {
|
|
5364
5404
|
const bqLines = [];
|
|
5365
|
-
while (i < lines.length && lines[i]
|
|
5366
|
-
bqLines.push(lines[i]
|
|
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
|
|
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}
|
|
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.
|
|
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
|
-
*
|
|
5422
|
-
*
|
|
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
|
-
|
|
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() === "")
|
|
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].
|
|
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
|
|
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(({
|
|
5475
|
-
|
|
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
|
-
|
|
5481
|
-
|
|
5482
|
-
|
|
5483
|
-
|
|
5484
|
-
|
|
5485
|
-
|
|
5486
|
-
|
|
5487
|
-
|
|
5488
|
-
|
|
5489
|
-
|
|
5490
|
-
|
|
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(/<(https?:\/\/[^\s&]+?)>/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("&", "&").replaceAll("<", "<").replaceAll(">", ">");
|
|
5495
5755
|
}
|
|
5496
5756
|
function _escAttr(v) {
|
|
5497
5757
|
return String(v).replaceAll("&", "&").replaceAll("\"", """).replaceAll("'", "'").replaceAll("<", "<").replaceAll(">", ">");
|
|
5498
5758
|
}
|
|
5759
|
+
/** Escapes only quote characters — for attribute values already run through _esc(). */
|
|
5760
|
+
function _escAttrQuotes(v) {
|
|
5761
|
+
return String(v).replaceAll("\"", """).replaceAll("'", "'");
|
|
5762
|
+
}
|
|
5763
|
+
/** Reverses _esc()'s &/</> substitutions, for matching against un-escaped _linkDefs/_footnoteIds keys. */
|
|
5764
|
+
function _unescAmpLtGt(v) {
|
|
5765
|
+
return String(v).replaceAll("<", "<").replaceAll(">", ">").replaceAll("&", "&");
|
|
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
|
-
|
|
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
|
|
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
|
|
7130
|
-
|
|
7131
|
-
|
|
7132
|
-
|
|
7133
|
-
|
|
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.
|
|
17501
|
+
version: "1.11.0"
|
|
17172
17502
|
};
|
|
17173
17503
|
/**
|
|
17174
17504
|
* @param {string|Element|NodeList|Element[]} selector
|