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.es.js
CHANGED
|
@@ -4643,7 +4643,10 @@ var PROHIBITED_TAGS = [
|
|
|
4643
4643
|
"object",
|
|
4644
4644
|
"embed",
|
|
4645
4645
|
"form",
|
|
4646
|
-
"base"
|
|
4646
|
+
"base",
|
|
4647
|
+
"template",
|
|
4648
|
+
"link",
|
|
4649
|
+
"meta"
|
|
4647
4650
|
];
|
|
4648
4651
|
/** Tags whose element wrapper is stripped but content (child nodes) is preserved. */
|
|
4649
4652
|
var UNWRAP_TAGS = new Set(["button"]);
|
|
@@ -4652,8 +4655,28 @@ var URL_ATTRS = [
|
|
|
4652
4655
|
"href",
|
|
4653
4656
|
"src",
|
|
4654
4657
|
"action",
|
|
4655
|
-
"formaction"
|
|
4658
|
+
"formaction",
|
|
4659
|
+
"xlink:href"
|
|
4656
4660
|
];
|
|
4661
|
+
/** Inline style properties the editor's own toolbar/table features persist on saved content. */
|
|
4662
|
+
var ALLOWED_STYLE_PROPS = new Set([
|
|
4663
|
+
"color",
|
|
4664
|
+
"background-color",
|
|
4665
|
+
"font-size",
|
|
4666
|
+
"line-height",
|
|
4667
|
+
"text-align",
|
|
4668
|
+
"vertical-align",
|
|
4669
|
+
"width",
|
|
4670
|
+
"min-width",
|
|
4671
|
+
"height",
|
|
4672
|
+
"min-height",
|
|
4673
|
+
"border-width",
|
|
4674
|
+
"border-style",
|
|
4675
|
+
"border-color",
|
|
4676
|
+
"padding"
|
|
4677
|
+
]);
|
|
4678
|
+
/** Value patterns that are never safe regardless of property. */
|
|
4679
|
+
var DANGEROUS_STYLE_VALUE_RE = /url\s*\(|expression\s*\(|@import|javascript:|vbscript:|behavior\s*:|-moz-binding/i;
|
|
4657
4680
|
/** Trusted hosts for iframe embeds when allowIframes is enabled. */
|
|
4658
4681
|
var TRUSTED_IFRAME_HOSTS = new Set([
|
|
4659
4682
|
"www.youtube.com",
|
|
@@ -4694,6 +4717,12 @@ function sanitiseHTML(html, { allowIframes = false } = {}) {
|
|
|
4694
4717
|
el.removeAttribute(attr.name);
|
|
4695
4718
|
continue;
|
|
4696
4719
|
}
|
|
4720
|
+
if (attr.name === "style") {
|
|
4721
|
+
const cleaned = sanitiseStyleValue(attr.value);
|
|
4722
|
+
if (cleaned) el.setAttribute("style", cleaned);
|
|
4723
|
+
else el.removeAttribute("style");
|
|
4724
|
+
continue;
|
|
4725
|
+
}
|
|
4697
4726
|
if (URL_ATTRS.includes(attr.name)) {
|
|
4698
4727
|
const val = attr.value.trim();
|
|
4699
4728
|
if (/^(javascript|vbscript):/i.test(val)) {
|
|
@@ -4722,6 +4751,28 @@ function sanitiseHTML(html, { allowIframes = false } = {}) {
|
|
|
4722
4751
|
return doc.body.innerHTML;
|
|
4723
4752
|
}
|
|
4724
4753
|
/**
|
|
4754
|
+
* Filters a style attribute value down to an allowlisted set of CSS
|
|
4755
|
+
* properties (ALLOWED_STYLE_PROPS), dropping any declaration whose value
|
|
4756
|
+
* contains a dangerous construct — url(), expression(), an "import" rule,
|
|
4757
|
+
* javascript:/vbscript:, IE behavior/-moz-binding — regardless of property.
|
|
4758
|
+
* @param {string} value
|
|
4759
|
+
* @returns {string} The filtered declaration list, or '' if nothing survives.
|
|
4760
|
+
*/
|
|
4761
|
+
function sanitiseStyleValue(value) {
|
|
4762
|
+
const kept = [];
|
|
4763
|
+
for (const decl of (value || "").split(";")) {
|
|
4764
|
+
const idx = decl.indexOf(":");
|
|
4765
|
+
if (idx === -1) continue;
|
|
4766
|
+
const prop = decl.slice(0, idx).trim().toLowerCase();
|
|
4767
|
+
const val = decl.slice(idx + 1).trim();
|
|
4768
|
+
if (!prop || !val) continue;
|
|
4769
|
+
if (!ALLOWED_STYLE_PROPS.has(prop)) continue;
|
|
4770
|
+
if (DANGEROUS_STYLE_VALUE_RE.test(val)) continue;
|
|
4771
|
+
kept.push(`${prop}: ${val}`);
|
|
4772
|
+
}
|
|
4773
|
+
return kept.join("; ");
|
|
4774
|
+
}
|
|
4775
|
+
/**
|
|
4725
4776
|
* Returns true if iframe src points to an approved video host.
|
|
4726
4777
|
* Relative, protocol-relative and invalid URLs are rejected.
|
|
4727
4778
|
* @param {string} src
|
|
@@ -5502,6 +5553,17 @@ function htmlToMarkdown(html) {
|
|
|
5502
5553
|
* @param {number} [depth=0] - Current nesting depth used to indent nested list items.
|
|
5503
5554
|
* @returns {string} The Markdown representation of the node subtree.
|
|
5504
5555
|
*/
|
|
5556
|
+
/**
|
|
5557
|
+
* Direct child elements matching a tag name. Used instead of the CSS
|
|
5558
|
+
* `:scope > tag` combinator, which this project's jsdom version resolves
|
|
5559
|
+
* incorrectly (matches descendants at any depth, not just direct children).
|
|
5560
|
+
* @param {Element} el
|
|
5561
|
+
* @param {string} tagName
|
|
5562
|
+
* @returns {Element[]}
|
|
5563
|
+
*/
|
|
5564
|
+
function _directChildren(el, tagName) {
|
|
5565
|
+
return Array.from(el.children).filter((c) => c.tagName === tagName.toUpperCase());
|
|
5566
|
+
}
|
|
5505
5567
|
function _domToMd(node, depth = 0) {
|
|
5506
5568
|
if (node.nodeType === 3) return node.textContent.replace(/\s+/g, " ");
|
|
5507
5569
|
if (node.nodeType !== 1) return "";
|
|
@@ -5527,6 +5589,12 @@ function _domToMd(node, depth = 0) {
|
|
|
5527
5589
|
case "strike": return `~~${inner()}~~`;
|
|
5528
5590
|
case "sup": return `^${inner()}^`;
|
|
5529
5591
|
case "sub": return `~${inner()}~`;
|
|
5592
|
+
case "u": return `<u>${inner()}</u>`;
|
|
5593
|
+
case "span": {
|
|
5594
|
+
const style = el.getAttribute("style") || "";
|
|
5595
|
+
if (/\b(color|background-color|font-size)\s*:/.test(style)) return `<span style="${_escAttr(style)}">${inner()}</span>`;
|
|
5596
|
+
return inner();
|
|
5597
|
+
}
|
|
5530
5598
|
case "code":
|
|
5531
5599
|
if (el.closest("pre")) return inner();
|
|
5532
5600
|
return `\`${inner()}\``;
|
|
@@ -5535,7 +5603,10 @@ function _domToMd(node, depth = 0) {
|
|
|
5535
5603
|
const langMatch = /language-(\S+)/.exec(codeEl?.className || "");
|
|
5536
5604
|
return `\n\n\`\`\`${langMatch ? langMatch[1] : ""}\n${(codeEl || el).textContent || ""}\n\`\`\`\n\n`;
|
|
5537
5605
|
}
|
|
5538
|
-
case "blockquote":
|
|
5606
|
+
case "blockquote": {
|
|
5607
|
+
const rawLines = inner().trim().split("\n");
|
|
5608
|
+
return `\n\n${rawLines.filter((l, idx) => l.trim() !== "" || (rawLines[idx - 1] ?? "").trim() !== "").map((l) => l.trim() === "" ? ">" : `> ${l}`).join("\n")}\n\n`;
|
|
5609
|
+
}
|
|
5539
5610
|
case "a": {
|
|
5540
5611
|
const href = el.getAttribute("href") || "";
|
|
5541
5612
|
return `[${inner()}](${href})`;
|
|
@@ -5545,22 +5616,20 @@ function _domToMd(node, depth = 0) {
|
|
|
5545
5616
|
return ``;
|
|
5546
5617
|
}
|
|
5547
5618
|
case "ul": {
|
|
5548
|
-
const items =
|
|
5619
|
+
const items = _directChildren(el, "li");
|
|
5549
5620
|
if (!items.length) return inner();
|
|
5550
5621
|
const indent = " ".repeat(depth);
|
|
5551
5622
|
const isChecklist = el.classList.contains("an-checklist");
|
|
5552
5623
|
const lines = items.map((li) => {
|
|
5624
|
+
const cb = _directChildren(li, "input").find((c) => c.getAttribute("type") === "checkbox");
|
|
5553
5625
|
let prefix = "- ";
|
|
5554
|
-
if (isChecklist)
|
|
5555
|
-
const cb = li.querySelector("input[type=\"checkbox\"]");
|
|
5556
|
-
prefix = (cb ? cb.checked : false) ? "- [x] " : "- [ ] ";
|
|
5557
|
-
}
|
|
5626
|
+
if (isChecklist || cb) prefix = (cb ? cb.checked : false) ? "- [x] " : "- [ ] ";
|
|
5558
5627
|
return `${indent}${prefix}${_domToMd(li, depth + 1).trim()}`;
|
|
5559
5628
|
}).join("\n");
|
|
5560
5629
|
return depth === 0 ? `\n\n${lines}\n\n` : `\n${lines}`;
|
|
5561
5630
|
}
|
|
5562
5631
|
case "ol": {
|
|
5563
|
-
const items =
|
|
5632
|
+
const items = _directChildren(el, "li");
|
|
5564
5633
|
if (!items.length) return inner();
|
|
5565
5634
|
const indent = " ".repeat(depth);
|
|
5566
5635
|
const lines = items.map((li, i) => `${indent}${i + 1}. ${_domToMd(li, depth + 1).trim()}`).join("\n");
|
|
@@ -5569,19 +5638,22 @@ function _domToMd(node, depth = 0) {
|
|
|
5569
5638
|
case "li": return inner();
|
|
5570
5639
|
case "hr": return "\n\n---\n\n";
|
|
5571
5640
|
case "table": {
|
|
5572
|
-
const
|
|
5573
|
-
if (!
|
|
5574
|
-
const
|
|
5641
|
+
const allRows = Array.from(el.querySelectorAll("tr"));
|
|
5642
|
+
if (!allRows.length) return inner();
|
|
5643
|
+
const firstRowIsHeader = !!_directChildren(el, "thead")[0] || allRows[0].children.length > 0 && Array.from(allRows[0].children).every((c) => c.tagName === "TH");
|
|
5644
|
+
const cellTexts = allRows.map((tr) => Array.from(tr.querySelectorAll("th, td")).map((c) => c.textContent.trim().replaceAll("|", String.raw`\|`)));
|
|
5575
5645
|
const cols = Math.max(...cellTexts.map((r) => r.length));
|
|
5576
5646
|
const padRow = (row) => {
|
|
5577
5647
|
const r = [...row];
|
|
5578
5648
|
while (r.length < cols) r.push("");
|
|
5579
5649
|
return r;
|
|
5580
5650
|
};
|
|
5651
|
+
const bodyStart = firstRowIsHeader ? 1 : 0;
|
|
5652
|
+
const headerCells = firstRowIsHeader ? padRow(cellTexts[0]) : new Array(cols).fill("");
|
|
5581
5653
|
let md = "\n\n";
|
|
5582
|
-
md += `| ${
|
|
5654
|
+
md += `| ${headerCells.join(" | ")} |\n`;
|
|
5583
5655
|
md += `| ${new Array(cols).fill("---").join(" | ")} |\n`;
|
|
5584
|
-
for (let r =
|
|
5656
|
+
for (let r = bodyStart; r < cellTexts.length; r++) md += `| ${padRow(cellTexts[r]).join(" | ")} |\n`;
|
|
5585
5657
|
return md + "\n";
|
|
5586
5658
|
}
|
|
5587
5659
|
default: return inner();
|
|
@@ -5596,8 +5668,11 @@ function _domToMd(node, depth = 0) {
|
|
|
5596
5668
|
* @returns {boolean} `true` if any Markdown-like pattern is present, `false` otherwise.
|
|
5597
5669
|
*/
|
|
5598
5670
|
function isMarkdown(text) {
|
|
5599
|
-
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);
|
|
5671
|
+
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);
|
|
5600
5672
|
}
|
|
5673
|
+
var BQ_RE = /^ {0,3}>( ?)(.*)$/;
|
|
5674
|
+
var HR_RE = /^ {0,3}([-*_])( *\1){2,}\s*$/;
|
|
5675
|
+
var HARD_BREAK = String.fromCharCode(1);
|
|
5601
5676
|
/**
|
|
5602
5677
|
* Converts a Markdown string to an HTML string.
|
|
5603
5678
|
* @param {string} text
|
|
@@ -5610,6 +5685,16 @@ function markdownToHTML(text) {
|
|
|
5610
5685
|
lines = refs.clean;
|
|
5611
5686
|
_linkDefs = refs.linkDefs;
|
|
5612
5687
|
_footnoteIds = refs.footnoteIds;
|
|
5688
|
+
return _parseBlocks(lines);
|
|
5689
|
+
}
|
|
5690
|
+
/**
|
|
5691
|
+
* Parses a line array into block-level HTML. Called recursively for content
|
|
5692
|
+
* nested inside a blockquote so nested quotes and block content (lists,
|
|
5693
|
+
* headings, etc.) inside `>` are parsed the same as top-level content.
|
|
5694
|
+
* @param {string[]} lines
|
|
5695
|
+
* @returns {string}
|
|
5696
|
+
*/
|
|
5697
|
+
function _parseBlocks(lines) {
|
|
5613
5698
|
const out = [];
|
|
5614
5699
|
let i = 0;
|
|
5615
5700
|
while (i < lines.length) {
|
|
@@ -5628,7 +5713,7 @@ function markdownToHTML(text) {
|
|
|
5628
5713
|
i++;
|
|
5629
5714
|
continue;
|
|
5630
5715
|
}
|
|
5631
|
-
if (line.trim() &&
|
|
5716
|
+
if (line.trim() && !HR_RE.test(line) && !/^#{1,6} /.test(line) && i + 1 < lines.length) {
|
|
5632
5717
|
if (/^=+\s*$/.test(lines[i + 1])) {
|
|
5633
5718
|
out.push(`<h1>${_inline(line.trim())}</h1>`);
|
|
5634
5719
|
i += 2;
|
|
@@ -5640,7 +5725,7 @@ function markdownToHTML(text) {
|
|
|
5640
5725
|
continue;
|
|
5641
5726
|
}
|
|
5642
5727
|
}
|
|
5643
|
-
if (
|
|
5728
|
+
if (HR_RE.test(line)) {
|
|
5644
5729
|
out.push("<hr>");
|
|
5645
5730
|
i++;
|
|
5646
5731
|
continue;
|
|
@@ -5648,17 +5733,18 @@ function markdownToHTML(text) {
|
|
|
5648
5733
|
const hMatch = /^(#{1,6})\s+(.+)$/.exec(line);
|
|
5649
5734
|
if (hMatch) {
|
|
5650
5735
|
const level = hMatch[1].length;
|
|
5651
|
-
|
|
5736
|
+
const content = hMatch[2].replace(/(?:^|\s)#+\s*$/, "");
|
|
5737
|
+
out.push(`<h${level}>${_inline(content)}</h${level}>`);
|
|
5652
5738
|
i++;
|
|
5653
5739
|
continue;
|
|
5654
5740
|
}
|
|
5655
|
-
if (
|
|
5741
|
+
if (BQ_RE.test(line)) {
|
|
5656
5742
|
const bqLines = [];
|
|
5657
|
-
while (i < lines.length && lines[i]
|
|
5658
|
-
bqLines.push(lines[i]
|
|
5743
|
+
while (i < lines.length && BQ_RE.test(lines[i])) {
|
|
5744
|
+
bqLines.push(BQ_RE.exec(lines[i])[2]);
|
|
5659
5745
|
i++;
|
|
5660
5746
|
}
|
|
5661
|
-
out.push(`<blockquote>${bqLines
|
|
5747
|
+
out.push(`<blockquote>${_parseBlocks(bqLines)}</blockquote>`);
|
|
5662
5748
|
continue;
|
|
5663
5749
|
}
|
|
5664
5750
|
if (/^[-*+] /.test(line)) {
|
|
@@ -5701,11 +5787,11 @@ function markdownToHTML(text) {
|
|
|
5701
5787
|
continue;
|
|
5702
5788
|
}
|
|
5703
5789
|
const paraLines = [];
|
|
5704
|
-
while (i < lines.length && lines[i].trim() !== "" && !/^(#{1,6}
|
|
5790
|
+
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]))) {
|
|
5705
5791
|
paraLines.push(lines[i]);
|
|
5706
5792
|
i++;
|
|
5707
5793
|
}
|
|
5708
|
-
if (paraLines.length) out.push(`<p>${_inline(paraLines.
|
|
5794
|
+
if (paraLines.length) out.push(`<p>${_inline(_joinParagraphLines(paraLines)).replaceAll(HARD_BREAK, "<br>")}</p>`);
|
|
5709
5795
|
}
|
|
5710
5796
|
return out.join("");
|
|
5711
5797
|
}
|
|
@@ -5780,23 +5866,77 @@ function _extractReferenceDefinitions(lines) {
|
|
|
5780
5866
|
};
|
|
5781
5867
|
}
|
|
5782
5868
|
/**
|
|
5783
|
-
*
|
|
5784
|
-
*
|
|
5869
|
+
* Joins a paragraph's source lines into one string, converting CommonMark
|
|
5870
|
+
* hard-break markers (a trailing backslash, or 2+ trailing spaces) on all
|
|
5871
|
+
* but the last line into a HARD_BREAK placeholder instead of a plain space.
|
|
5872
|
+
* @param {string[]} paraLines
|
|
5873
|
+
* @returns {string}
|
|
5874
|
+
*/
|
|
5875
|
+
function _joinParagraphLines(paraLines) {
|
|
5876
|
+
let joined = "";
|
|
5877
|
+
for (let idx = 0; idx < paraLines.length; idx++) {
|
|
5878
|
+
const isLast = idx === paraLines.length - 1;
|
|
5879
|
+
const ln = paraLines[idx];
|
|
5880
|
+
if (!isLast && /\\$/.test(ln)) {
|
|
5881
|
+
joined += ln.replace(/\\$/, "") + HARD_BREAK;
|
|
5882
|
+
continue;
|
|
5883
|
+
}
|
|
5884
|
+
if (!isLast && / {2,}$/.test(ln)) {
|
|
5885
|
+
joined += ln.replace(/ {2,}$/, "") + HARD_BREAK;
|
|
5886
|
+
continue;
|
|
5887
|
+
}
|
|
5888
|
+
joined += ln + (isLast ? "" : " ");
|
|
5889
|
+
}
|
|
5890
|
+
return joined;
|
|
5891
|
+
}
|
|
5892
|
+
/**
|
|
5893
|
+
* Splits a GFM table row string into trimmed cell strings, treating an
|
|
5894
|
+
* escaped pipe (`\|`) as a literal character rather than a cell separator.
|
|
5895
|
+
* '| a | b | c |' → ['a', 'b', 'c']; '| a\|b | c |' → ['a|b', 'c']
|
|
5785
5896
|
* @param {string} row
|
|
5786
5897
|
* @returns {string[]}
|
|
5787
5898
|
*/
|
|
5788
5899
|
function _parseTableRow(row) {
|
|
5789
|
-
|
|
5900
|
+
const trimmed = row.replace(/^\|/, "").replace(/\|$/, "");
|
|
5901
|
+
const cells = [];
|
|
5902
|
+
let cur = "";
|
|
5903
|
+
for (let i = 0; i < trimmed.length; i++) {
|
|
5904
|
+
if (trimmed[i] === "\\" && trimmed[i + 1] === "|") {
|
|
5905
|
+
cur += "|";
|
|
5906
|
+
i++;
|
|
5907
|
+
continue;
|
|
5908
|
+
}
|
|
5909
|
+
if (trimmed[i] === "|") {
|
|
5910
|
+
cells.push(cur);
|
|
5911
|
+
cur = "";
|
|
5912
|
+
continue;
|
|
5913
|
+
}
|
|
5914
|
+
cur += trimmed[i];
|
|
5915
|
+
}
|
|
5916
|
+
cells.push(cur);
|
|
5917
|
+
return cells.map((c) => c.trim());
|
|
5790
5918
|
}
|
|
5791
5919
|
function _parseListBlock(lines, startIdx) {
|
|
5792
5920
|
const baseIndent = lines[startIdx].match(/^(\s*)/)[1].length;
|
|
5793
5921
|
const isOL = /^\s*\d+\. /.test(lines[startIdx]);
|
|
5794
5922
|
const items = [];
|
|
5795
5923
|
let firstIsCB = null;
|
|
5924
|
+
let loose = false;
|
|
5925
|
+
let pendingBlank = false;
|
|
5796
5926
|
let i = startIdx;
|
|
5797
5927
|
while (i < lines.length) {
|
|
5798
5928
|
const line = lines[i];
|
|
5799
|
-
if (line.trim() === "")
|
|
5929
|
+
if (line.trim() === "") {
|
|
5930
|
+
const next = lines[i + 1];
|
|
5931
|
+
const nextIndent = next !== void 0 ? next.match(/^(\s*)/)[1].length : -1;
|
|
5932
|
+
const nextIsSameItem = next !== void 0 && /^\s*(?:[-*+]|\d+\.) /.test(next) && /^\s*\d+\. /.test(next) === isOL && nextIndent === baseIndent;
|
|
5933
|
+
const nextIsContinuation = next !== void 0 && next.trim() !== "" && nextIndent > baseIndent;
|
|
5934
|
+
if (!items.length || !nextIsSameItem && !nextIsContinuation) break;
|
|
5935
|
+
loose = true;
|
|
5936
|
+
pendingBlank = true;
|
|
5937
|
+
i++;
|
|
5938
|
+
continue;
|
|
5939
|
+
}
|
|
5800
5940
|
const indent = line.match(/^(\s*)/)[1].length;
|
|
5801
5941
|
if (indent < baseIndent) break;
|
|
5802
5942
|
if (indent === baseIndent) {
|
|
@@ -5809,11 +5949,12 @@ function _parseListBlock(lines, startIdx) {
|
|
|
5809
5949
|
const checked = isCB && raw[1].toLowerCase() === "x";
|
|
5810
5950
|
const text = isCB ? raw.replace(/^\[[ xX]\]\s+/, "") : raw;
|
|
5811
5951
|
items.push({
|
|
5812
|
-
text,
|
|
5952
|
+
paras: [text],
|
|
5813
5953
|
isCB,
|
|
5814
5954
|
checked,
|
|
5815
5955
|
sub: ""
|
|
5816
5956
|
});
|
|
5957
|
+
pendingBlank = false;
|
|
5817
5958
|
i++;
|
|
5818
5959
|
} else {
|
|
5819
5960
|
if (!items.length) {
|
|
@@ -5824,53 +5965,143 @@ function _parseListBlock(lines, startIdx) {
|
|
|
5824
5965
|
const nested = _parseListBlock(lines, i);
|
|
5825
5966
|
items[items.length - 1].sub += nested.html;
|
|
5826
5967
|
i = nested.endIdx;
|
|
5968
|
+
pendingBlank = false;
|
|
5969
|
+
} else if (pendingBlank) {
|
|
5970
|
+
items[items.length - 1].paras.push(line.trim());
|
|
5971
|
+
pendingBlank = false;
|
|
5972
|
+
i++;
|
|
5827
5973
|
} else {
|
|
5828
|
-
items[items.length - 1].
|
|
5974
|
+
const paras = items[items.length - 1].paras;
|
|
5975
|
+
paras[paras.length - 1] += " " + line.trim();
|
|
5829
5976
|
i++;
|
|
5830
5977
|
}
|
|
5831
5978
|
}
|
|
5832
5979
|
}
|
|
5833
|
-
const
|
|
5980
|
+
const hasCB = !isOL && firstIsCB === true;
|
|
5981
|
+
const startMatch = isOL ? /^\s*(\d+)\. /.exec(lines[startIdx]) : null;
|
|
5982
|
+
const startNum = startMatch ? Number.parseInt(startMatch[1], 10) : 1;
|
|
5983
|
+
const open = isOL ? startNum !== 1 ? `<ol start="${startNum}">` : "<ol>" : hasCB ? "<ul class=\"an-checklist\">" : "<ul>";
|
|
5834
5984
|
const close = isOL ? "</ol>" : "</ul>";
|
|
5835
5985
|
return {
|
|
5836
|
-
html: `${open}${items.map(({
|
|
5837
|
-
|
|
5986
|
+
html: `${open}${items.map(({ paras, isCB, checked, sub }) => {
|
|
5987
|
+
const cbHTML = isCB ? `<input type="checkbox" contenteditable="false"${checked ? " checked" : ""}>` : "";
|
|
5988
|
+
return `<li>${loose ? paras.map((p, idx) => `<p>${idx === 0 ? cbHTML : ""}${_inline(p)}</p>`).join("") : `${cbHTML}${_inline(paras[0])}`}${sub}</li>`;
|
|
5838
5989
|
}).join("")}${close}`,
|
|
5839
5990
|
endIdx: i
|
|
5840
5991
|
};
|
|
5841
5992
|
}
|
|
5842
|
-
|
|
5843
|
-
|
|
5844
|
-
|
|
5993
|
+
var ESCAPABLE_RE = /\\([*_`#[\]()>\\~|])/g;
|
|
5994
|
+
var MARK = String.fromCharCode(0);
|
|
5995
|
+
/**
|
|
5996
|
+
* Step 0 of _inline(): replaces backslash-escaped punctuation with inert
|
|
5997
|
+
* placeholders so later syntax regexes can't match them.
|
|
5998
|
+
* @param {string} text
|
|
5999
|
+
* @returns {{ text: string, literals: string[] }}
|
|
6000
|
+
*/
|
|
6001
|
+
function _extractBackslashEscapes(text) {
|
|
6002
|
+
const literals = [];
|
|
6003
|
+
return {
|
|
6004
|
+
text: text.replace(ESCAPABLE_RE, (_, ch) => {
|
|
6005
|
+
literals.push(ch);
|
|
6006
|
+
return `${MARK}${literals.length - 1}${MARK}`;
|
|
6007
|
+
}),
|
|
6008
|
+
literals
|
|
6009
|
+
};
|
|
6010
|
+
}
|
|
6011
|
+
/**
|
|
6012
|
+
* Restores placeholders from _extractBackslashEscapes(), HTML-escaping each
|
|
6013
|
+
* literal since it's inserted directly into the output.
|
|
6014
|
+
* @param {string} text
|
|
6015
|
+
* @param {string[]} literals
|
|
6016
|
+
* @returns {string}
|
|
6017
|
+
*/
|
|
6018
|
+
function _restoreBackslashEscapes(text, literals) {
|
|
6019
|
+
return text.replace(new RegExp(`${MARK}(\\d+)${MARK}`, "g"), (_, idx) => _esc(literals[Number(idx)]));
|
|
6020
|
+
}
|
|
6021
|
+
/**
|
|
6022
|
+
* Resolves images, inline links, GFM reference-style links (explicit,
|
|
6023
|
+
* shortcut, and bare/implicit forms), and footnote markers. Must run on text
|
|
6024
|
+
* already passed through _esc() — see _inline()'s Step 1 comment.
|
|
6025
|
+
* @param {string} text
|
|
6026
|
+
* @returns {string}
|
|
6027
|
+
*/
|
|
6028
|
+
function _resolveLinksAndFootnotes(text) {
|
|
6029
|
+
text = text.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, (_, alt, src) => `<img src="${_escAttrQuotes(src)}" alt="${_escAttrQuotes(alt)}" class="an-image">`);
|
|
6030
|
+
text = text.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_, label, href) => `<a href="${_escAttrQuotes(href)}">${label}</a>`);
|
|
5845
6031
|
text = text.replace(/\[([^\]]+)\]\[([^\]]*)\]/g, (m, label, ref) => {
|
|
5846
|
-
const def = _linkDefs.get((ref || label).trim().toLowerCase());
|
|
6032
|
+
const def = _linkDefs.get(_unescAmpLtGt(ref || label).trim().toLowerCase());
|
|
5847
6033
|
if (!def) return m;
|
|
5848
6034
|
const titleAttr = def.title ? ` title="${_escAttr(def.title)}"` : "";
|
|
5849
|
-
return `<a href="${_escAttr(def.href)}"${titleAttr}>${
|
|
6035
|
+
return `<a href="${_escAttr(def.href)}"${titleAttr}>${label}</a>`;
|
|
5850
6036
|
});
|
|
5851
6037
|
text = text.replace(/\[([^\]]+)\]/g, (m, label) => {
|
|
5852
|
-
const def = _linkDefs.get(label.trim().toLowerCase());
|
|
6038
|
+
const def = _linkDefs.get(_unescAmpLtGt(label).trim().toLowerCase());
|
|
5853
6039
|
if (!def) return m;
|
|
5854
6040
|
const titleAttr = def.title ? ` title="${_escAttr(def.title)}"` : "";
|
|
5855
|
-
return `<a href="${_escAttr(def.href)}"${titleAttr}>${
|
|
6041
|
+
return `<a href="${_escAttr(def.href)}"${titleAttr}>${label}</a>`;
|
|
5856
6042
|
});
|
|
5857
|
-
text = text.replace(/\[\^([^\]]+)\]/g, (m, id) => _footnoteIds.has(id) ? `<sup>[${
|
|
5858
|
-
text = text.replace(/\*{3}([^*\n]+?)\*{3}/g, (_, c) => `<strong><em>${_esc(c)}</em></strong>`);
|
|
5859
|
-
text = text.replace(/_{3}([^_\n]+?)_{3}/g, (_, c) => `<strong><em>${_esc(c)}</em></strong>`);
|
|
5860
|
-
text = text.replace(/\*{2}([^*\n]+?)\*{2}/g, (_, c) => `<strong>${_esc(c)}</strong>`);
|
|
5861
|
-
text = text.replace(/_{2}([^_\n]+?)_{2}/g, (_, c) => `<strong>${_esc(c)}</strong>`);
|
|
5862
|
-
text = text.replace(/\*([^*\n]+?)\*/g, (_, c) => `<em>${_esc(c)}</em>`);
|
|
5863
|
-
text = text.replace(/_([^_\n]+?)_/g, (_, c) => `<em>${_esc(c)}</em>`);
|
|
5864
|
-
text = text.replace(/~~([^~\n]+?)~~/g, (_, c) => `<del>${_esc(c)}</del>`);
|
|
5865
|
-
text = text.replace(/`([^`]+)`/g, (_, c) => `<code>${_esc(c)}</code>`);
|
|
6043
|
+
text = text.replace(/\[\^([^\]]+)\]/g, (m, id) => _footnoteIds.has(_unescAmpLtGt(id)) ? `<sup>[${id}]</sup>` : m);
|
|
5866
6044
|
return text;
|
|
5867
6045
|
}
|
|
6046
|
+
/**
|
|
6047
|
+
* Converts angle-bracket (`<https://...>`) and bare (`https://...`)
|
|
6048
|
+
* autolinks. Runs after _resolveLinksAndFootnotes() so an already-linked URL
|
|
6049
|
+
* isn't reprocessed, and on already-_esc()'d text (see _inline()).
|
|
6050
|
+
* @param {string} text
|
|
6051
|
+
* @returns {string}
|
|
6052
|
+
*/
|
|
6053
|
+
function _applyAutolinks(text) {
|
|
6054
|
+
text = text.replace(/<(https?:\/\/[^\s&]+?)>/g, (_, url) => `<a href="${_escAttrQuotes(url)}">${url}</a>`);
|
|
6055
|
+
text = text.replace(/(^|[\s(])(https?:\/\/[^\s()]+)/g, (m, pre, rawUrl) => {
|
|
6056
|
+
const trail = /[.,;:!?)]+$/.exec(rawUrl);
|
|
6057
|
+
const url = trail ? rawUrl.slice(0, -trail[0].length) : rawUrl;
|
|
6058
|
+
if (!url) return m;
|
|
6059
|
+
const suffix = trail ? trail[0] : "";
|
|
6060
|
+
return `${pre}<a href="${_escAttrQuotes(url)}">${url}</a>${suffix}`;
|
|
6061
|
+
});
|
|
6062
|
+
return text;
|
|
6063
|
+
}
|
|
6064
|
+
/**
|
|
6065
|
+
* Applies bold/italic/bold-italic (asterisk and underscore forms — underscore
|
|
6066
|
+
* requires a non-word-character boundary per CommonMark), strikethrough, and
|
|
6067
|
+
* inline code.
|
|
6068
|
+
* @param {string} text
|
|
6069
|
+
* @returns {string}
|
|
6070
|
+
*/
|
|
6071
|
+
function _applyEmphasisAndCode(text) {
|
|
6072
|
+
text = text.replace(/\*{3}([^*\n]+?)\*{3}/g, (_, c) => `<strong><em>${c}</em></strong>`);
|
|
6073
|
+
text = text.replace(/(?<!\w)_{3}([^_\n]+?)_{3}(?!\w)/g, (_, c) => `<strong><em>${c}</em></strong>`);
|
|
6074
|
+
text = text.replace(/\*{2}([^*\n]+?)\*{2}/g, (_, c) => `<strong>${c}</strong>`);
|
|
6075
|
+
text = text.replace(/(?<!\w)_{2}([^_\n]+?)_{2}(?!\w)/g, (_, c) => `<strong>${c}</strong>`);
|
|
6076
|
+
text = text.replace(/\*([^*\n]+?)\*/g, (_, c) => `<em>${c}</em>`);
|
|
6077
|
+
text = text.replace(/(?<!\w)_([^_\n]+?)_(?!\w)/g, (_, c) => `<em>${c}</em>`);
|
|
6078
|
+
text = text.replace(/~~([^~\n]+?)~~/g, (_, c) => `<del>${c}</del>`);
|
|
6079
|
+
text = text.replace(/``([\s\S]*?)``/g, (_, c) => `<code>${c}</code>`);
|
|
6080
|
+
text = text.replace(/`([^`]+)`/g, (_, c) => `<code>${c}</code>`);
|
|
6081
|
+
return text;
|
|
6082
|
+
}
|
|
6083
|
+
function _inline(text) {
|
|
6084
|
+
const { text: withoutEscapes, literals } = _extractBackslashEscapes(text);
|
|
6085
|
+
let result = _esc(withoutEscapes);
|
|
6086
|
+
result = _resolveLinksAndFootnotes(result);
|
|
6087
|
+
result = _applyAutolinks(result);
|
|
6088
|
+
result = _applyEmphasisAndCode(result);
|
|
6089
|
+
return _restoreBackslashEscapes(result, literals);
|
|
6090
|
+
}
|
|
5868
6091
|
function _esc(v) {
|
|
5869
6092
|
return String(v).replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">");
|
|
5870
6093
|
}
|
|
5871
6094
|
function _escAttr(v) {
|
|
5872
6095
|
return String(v).replaceAll("&", "&").replaceAll("\"", """).replaceAll("'", "'").replaceAll("<", "<").replaceAll(">", ">");
|
|
5873
6096
|
}
|
|
6097
|
+
/** Escapes only quote characters — for attribute values already run through _esc(). */
|
|
6098
|
+
function _escAttrQuotes(v) {
|
|
6099
|
+
return String(v).replaceAll("\"", """).replaceAll("'", "'");
|
|
6100
|
+
}
|
|
6101
|
+
/** Reverses _esc()'s &/</> substitutions, for matching against un-escaped _linkDefs/_footnoteIds keys. */
|
|
6102
|
+
function _unescAmpLtGt(v) {
|
|
6103
|
+
return String(v).replaceAll("<", "<").replaceAll(">", ">").replaceAll("&", "&");
|
|
6104
|
+
}
|
|
5874
6105
|
//#endregion
|
|
5875
6106
|
//#region src/js/core/detectLang.js
|
|
5876
6107
|
/**
|
|
@@ -7426,6 +7657,17 @@ var Clipboard = class {
|
|
|
7426
7657
|
return doc.body.innerHTML;
|
|
7427
7658
|
}
|
|
7428
7659
|
/**
|
|
7660
|
+
* Checks whether an HTML payload has no semantic markup beyond plain
|
|
7661
|
+
* wrapper elements (e.g. a bare <div>/<p>). Used to decide whether a
|
|
7662
|
+
* markdown-shaped plain-text paste should win over an accompanying HTML
|
|
7663
|
+
* payload that isn't actually carrying any real rich-text formatting.
|
|
7664
|
+
* @param {string} html
|
|
7665
|
+
* @returns {boolean}
|
|
7666
|
+
*/
|
|
7667
|
+
_isTriviallyPlainHtml(html) {
|
|
7668
|
+
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");
|
|
7669
|
+
}
|
|
7670
|
+
/**
|
|
7429
7671
|
* Forces the next paste operation to strip all HTML formatting.
|
|
7430
7672
|
* Called by Editor when Ctrl+Shift+V is pressed.
|
|
7431
7673
|
* @param {boolean} val
|
|
@@ -7442,8 +7684,16 @@ var Clipboard = class {
|
|
|
7442
7684
|
if (maxBytes > 0) {
|
|
7443
7685
|
const text = clipboardData.getData("text/plain") || "";
|
|
7444
7686
|
const html = clipboardData.getData("text/html") || "";
|
|
7445
|
-
|
|
7687
|
+
const size = Math.max(text.length, html.length);
|
|
7688
|
+
if (size > maxBytes) {
|
|
7446
7689
|
event.preventDefault();
|
|
7690
|
+
const message = `Pasted content (${size} bytes) exceeds the ${this.options.maxPasteSize ?? 5} MB paste size limit.`;
|
|
7691
|
+
this.context.triggerEvent("pasteError", {
|
|
7692
|
+
size,
|
|
7693
|
+
maxBytes,
|
|
7694
|
+
message
|
|
7695
|
+
});
|
|
7696
|
+
console.warn(`[AutumnNote] ${message}`);
|
|
7447
7697
|
return;
|
|
7448
7698
|
}
|
|
7449
7699
|
}
|
|
@@ -7466,9 +7716,12 @@ var Clipboard = class {
|
|
|
7466
7716
|
this.context.invoke("editor.afterCommand");
|
|
7467
7717
|
return;
|
|
7468
7718
|
}
|
|
7469
|
-
if (this.options.markdownPaste !== false
|
|
7719
|
+
if (this.options.markdownPaste !== false) {
|
|
7720
|
+
const hasHtml = clipboardData.types.includes("text/html");
|
|
7721
|
+
const html = hasHtml ? clipboardData.getData("text/html") : "";
|
|
7722
|
+
const htmlTriviallyPlain = !hasHtml || this._isTriviallyPlainHtml(html);
|
|
7470
7723
|
const text = clipboardData.getData("text/plain");
|
|
7471
|
-
if (text && isMarkdown(text)) {
|
|
7724
|
+
if (text && htmlTriviallyPlain && isMarkdown(text)) {
|
|
7472
7725
|
event.preventDefault();
|
|
7473
7726
|
execCommand("insertHTML", sanitiseHTML(markdownToHTML(text)));
|
|
7474
7727
|
this.context.invoke("editor.afterCommand");
|
|
@@ -7501,11 +7754,51 @@ var Clipboard = class {
|
|
|
7501
7754
|
const dt = event.dataTransfer;
|
|
7502
7755
|
if (!dt?.files?.length) return;
|
|
7503
7756
|
const imageFiles = Array.from(dt.files).filter((f) => f.type.startsWith("image/"));
|
|
7504
|
-
if (imageFiles.length
|
|
7505
|
-
|
|
7506
|
-
|
|
7507
|
-
|
|
7508
|
-
|
|
7757
|
+
if (imageFiles.length > 0) {
|
|
7758
|
+
event.preventDefault();
|
|
7759
|
+
event.stopPropagation();
|
|
7760
|
+
this._placeCaretAtPoint(event.clientX, event.clientY);
|
|
7761
|
+
this._insertImageFiles(imageFiles);
|
|
7762
|
+
return;
|
|
7763
|
+
}
|
|
7764
|
+
if (this.options.markdownPaste !== false) {
|
|
7765
|
+
const mdFile = Array.from(dt.files).find((f) => /\.md$/i.test(f.name) || f.type === "text/markdown");
|
|
7766
|
+
if (mdFile) {
|
|
7767
|
+
event.preventDefault();
|
|
7768
|
+
event.stopPropagation();
|
|
7769
|
+
this._placeCaretAtPoint(event.clientX, event.clientY);
|
|
7770
|
+
this._insertMarkdownFile(mdFile);
|
|
7771
|
+
}
|
|
7772
|
+
}
|
|
7773
|
+
}
|
|
7774
|
+
/**
|
|
7775
|
+
* Reads a dropped `.md` File and inserts it converted to HTML at the
|
|
7776
|
+
* current caret. Skips the isMarkdown() heuristic — an explicit `.md`
|
|
7777
|
+
* extension/MIME type is an unambiguous signal, unlike pasted plain text.
|
|
7778
|
+
* @param {File} file
|
|
7779
|
+
*/
|
|
7780
|
+
_insertMarkdownFile(file) {
|
|
7781
|
+
const maxBytes = (this.options.maxPasteSize ?? 5) * 1024 * 1024;
|
|
7782
|
+
if (maxBytes > 0 && file.size > maxBytes) {
|
|
7783
|
+
const message = `Dropped file "${file.name}" (${file.size} bytes) exceeds the ${this.options.maxPasteSize ?? 5} MB paste size limit.`;
|
|
7784
|
+
this.context.triggerEvent("pasteError", {
|
|
7785
|
+
size: file.size,
|
|
7786
|
+
maxBytes,
|
|
7787
|
+
message
|
|
7788
|
+
});
|
|
7789
|
+
console.warn(`[AutumnNote] ${message}`);
|
|
7790
|
+
return;
|
|
7791
|
+
}
|
|
7792
|
+
const reader = new FileReader();
|
|
7793
|
+
reader.onload = (e) => {
|
|
7794
|
+
execCommand("insertHTML", sanitiseHTML(markdownToHTML(
|
|
7795
|
+
/** @type {string} */
|
|
7796
|
+
e.target.result || ""
|
|
7797
|
+
)));
|
|
7798
|
+
this.context.invoke("editor.afterCommand");
|
|
7799
|
+
};
|
|
7800
|
+
reader.onerror = () => console.warn("[AutumnNote] Failed to read dropped markdown file", file.name);
|
|
7801
|
+
reader.readAsText(file);
|
|
7509
7802
|
}
|
|
7510
7803
|
/**
|
|
7511
7804
|
* Inserts one or more image Files into the editor.
|
|
@@ -17669,7 +17962,7 @@ var AutumnNote = {
|
|
|
17669
17962
|
/** All pre-built button definitions — accessible in every module format including UMD/CJS. */
|
|
17670
17963
|
buttons,
|
|
17671
17964
|
/** Library version */
|
|
17672
|
-
version: "1.
|
|
17965
|
+
version: "1.11.1"
|
|
17673
17966
|
};
|
|
17674
17967
|
/**
|
|
17675
17968
|
* @param {string|Element|NodeList|Element[]} selector
|