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.
- package/dist/autumnnote.css +40 -0
- package/dist/autumnnote.es.js +351 -58
- package/dist/autumnnote.es.js.map +1 -1
- package/dist/autumnnote.umd.js +351 -58
- package/dist/autumnnote.umd.js.map +1 -1
- package/package.json +1 -1
- package/src/js/core/markdown.js +282 -61
- package/src/js/core/sanitise.js +47 -2
- 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
|
@@ -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":
|
|
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 ``;
|
|
5259
5330
|
}
|
|
5260
5331
|
case "ul": {
|
|
5261
|
-
const items =
|
|
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 =
|
|
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
|
|
5286
|
-
if (!
|
|
5287
|
-
const
|
|
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 += `| ${
|
|
5367
|
+
md += `| ${headerCells.join(" | ")} |\n`;
|
|
5296
5368
|
md += `| ${new Array(cols).fill("---").join(" | ")} |\n`;
|
|
5297
|
-
for (let r =
|
|
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() &&
|
|
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 (
|
|
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
|
-
|
|
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 (
|
|
5454
|
+
if (BQ_RE.test(line)) {
|
|
5369
5455
|
const bqLines = [];
|
|
5370
|
-
while (i < lines.length && lines[i]
|
|
5371
|
-
bqLines.push(lines[i]
|
|
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
|
|
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}
|
|
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.
|
|
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
|
-
*
|
|
5497
|
-
*
|
|
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
|
-
|
|
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() === "")
|
|
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].
|
|
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
|
|
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(({
|
|
5550
|
-
|
|
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
|
-
|
|
5556
|
-
|
|
5557
|
-
|
|
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}>${
|
|
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}>${
|
|
5754
|
+
return `<a href="${_escAttr(def.href)}"${titleAttr}>${label}</a>`;
|
|
5569
5755
|
});
|
|
5570
|
-
text = text.replace(/\[\^([^\]]+)\]/g, (m, id) => _footnoteIds.has(id) ? `<sup>[${
|
|
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(/<(https?:\/\/[^\s&]+?)>/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("&", "&").replaceAll("<", "<").replaceAll(">", ">");
|
|
5583
5806
|
}
|
|
5584
5807
|
function _escAttr(v) {
|
|
5585
5808
|
return String(v).replaceAll("&", "&").replaceAll("\"", """).replaceAll("'", "'").replaceAll("<", "<").replaceAll(">", ">");
|
|
5586
5809
|
}
|
|
5810
|
+
/** Escapes only quote characters — for attribute values already run through _esc(). */
|
|
5811
|
+
function _escAttrQuotes(v) {
|
|
5812
|
+
return String(v).replaceAll("\"", """).replaceAll("'", "'");
|
|
5813
|
+
}
|
|
5814
|
+
/** Reverses _esc()'s &/</> substitutions, for matching against un-escaped _linkDefs/_footnoteIds keys. */
|
|
5815
|
+
function _unescAmpLtGt(v) {
|
|
5816
|
+
return String(v).replaceAll("<", "<").replaceAll(">", ">").replaceAll("&", "&");
|
|
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
|
-
|
|
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
|
|
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
|
|
7218
|
-
|
|
7219
|
-
|
|
7220
|
-
|
|
7221
|
-
|
|
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.
|
|
17552
|
+
version: "1.11.1"
|
|
17260
17553
|
};
|
|
17261
17554
|
/**
|
|
17262
17555
|
* @param {string|Element|NodeList|Element[]} selector
|