fixes and features

This commit is contained in:
Mateusz Gruszczyński
2026-07-23 13:47:17 +02:00
parent 3ed74b1eac
commit dc6227896b
21 changed files with 831 additions and 82 deletions
+43 -20
View File
@@ -54,7 +54,7 @@ function inline(value) {
return html.replace(/\u0000T(\d+)\u0000/g, (_, index) => tokens[Number(index)] || "");
}
const attrs = (line, editable = false, prefix = "", suffix = "") => ` class="preview-source-line${editable ? " preview-editable" : ""}" data-source-line="${line + 1}"${editable ? ` contenteditable="true" spellcheck="true" data-source-prefix="${escapeHtml(prefix)}" data-source-suffix="${escapeHtml(suffix)}"` : ""}`;
const attrs = (line, editable = false, prefix = "", suffix = "", lineOffset = 0) => ` class="preview-source-line${editable ? " preview-editable" : ""}" data-source-line="${line + lineOffset + 1}"${editable ? ` contenteditable="true" spellcheck="true" data-source-prefix="${escapeHtml(prefix)}" data-source-suffix="${escapeHtml(suffix)}"` : ""}`;
const isPlainText = line => !/[`*_~^=\[\]<>|:#]/.test(line) && !/^\s*(?:[-+*>]|\d+\.)\s/.test(line);
function splitTableRow(line) {
@@ -70,7 +70,7 @@ function tableDelimiter(line) {
return cells.map(cell => cell.startsWith(":") && cell.endsWith(":") ? "center" : cell.endsWith(":") ? "right" : "left");
}
export function renderMarkdown(source) {
export function renderMarkdown(source, lineOffset = 0) {
let html = "", inCode = false, fence = "", language = "", code = [], codeStart = 0, list = null;
const lines = String(source).split("\n");
const footnotes = new Map();
@@ -93,14 +93,14 @@ export function renderMarkdown(source) {
const closeCode = () => {
const body = escapeHtml(code.join("\n"));
html += language.toLowerCase() === "mermaid"
? `<div class="mermaid preview-source-line" data-source-line="${codeStart + 1}">${body}</div>`
: `<pre${attrs(codeStart)}><code class="language-${escapeHtml(language)}">${body}</code></pre>`;
? `<div class="mermaid preview-source-line" data-source-line="${codeStart + lineOffset + 1}">${body}</div>`
: `<pre${attrs(codeStart, false, "", "", lineOffset)}><code class="language-${escapeHtml(language)}">${body}</code></pre>`;
code = []; language = ""; fence = "";
};
for (let index = 0; index < lines.length; index++) {
const line = lines[index];
const fenceMatch = line.match(/^(```+|~~~+)\s*([^\s]*)\s*$/);
const fenceMatch = line.match(/^\s*(```+|~~~+)\s*([^\s]*)\s*$/);
if (fenceMatch) {
closeList();
if (inCode && fenceMatch[1][0] === fence[0] && fenceMatch[1].length >= fence.length) closeCode();
@@ -114,14 +114,14 @@ export function renderMarkdown(source) {
if (line.includes("|") && delimiter) {
closeList();
const headers = splitTableRow(line);
html += `<div class="table-wrap preview-source-line" data-source-line="${index + 1}"><table><thead><tr>`;
headers.forEach((cell, i) => html += `<th class="preview-editable" contenteditable="true" spellcheck="true" data-source-line="${index + 1}" data-table-cell="${i}" style="text-align:${delimiter[i] || "left"}">${inline(cell)}</th>`);
html += `<div class="table-wrap preview-source-line" data-source-line="${index + lineOffset + 1}"><table><thead><tr>`;
headers.forEach((cell, i) => html += `<th class="preview-editable" contenteditable="true" spellcheck="true" data-source-line="${index + lineOffset + 1}" data-table-cell="${i}" style="text-align:${delimiter[i] || "left"}">${inline(cell)}</th>`);
html += `</tr></thead><tbody>`;
index += 2;
while (index < lines.length && lines[index].includes("|") && lines[index].trim()) {
const cells = splitTableRow(lines[index]);
html += `<tr>`;
headers.forEach((_, i) => html += `<td class="preview-editable" contenteditable="true" spellcheck="true" data-source-line="${index + 1}" data-table-cell="${i}" style="text-align:${delimiter[i] || "left"}">${inline(cells[i] || "")}</td>`);
headers.forEach((_, i) => html += `<td class="preview-editable" contenteditable="true" spellcheck="true" data-source-line="${index + lineOffset + 1}" data-table-cell="${i}" style="text-align:${delimiter[i] || "left"}">${inline(cells[i] || "")}</td>`);
html += `</tr>`;
index++;
}
@@ -130,39 +130,62 @@ export function renderMarkdown(source) {
continue;
}
const heading = line.match(/^(#{1,6})\s+(.+?)(?:\s+\{#([A-Za-z][\w:.-]*)\})?\s*$/);
if (/^<details>\s*$/i.test(line.trim())) {
closeList();
let end = index + 1;
while (end < lines.length && !/^<\/details>\s*$/i.test(lines[end].trim())) end++;
if (end < lines.length) {
const block = lines.slice(index + 1, end);
let summary = "Details";
while (block.length && !block[0].trim()) block.shift();
if (block.length) {
const summaryMatch = block[0].trim().match(/^<summary>([\s\S]*?)<\/summary>$/i);
if (summaryMatch) { summary = summaryMatch[1].trim() || "Details"; block.shift(); }
}
while (block.length && !block[0].trim()) block.shift();
html += `<details class="markdown-details preview-source-line" data-source-line="${index + lineOffset + 1}"><summary>${inline(summary)}</summary><div class="markdown-details__content">${renderMarkdown(block.join("\n"), lineOffset + index + 1)}</div></details>`;
index = end;
continue;
}
}
const heading = line.match(/^\s{0,4}(#{1,6})\s+(.+?)(?:\s+\{#([A-Za-z][\w:.-]*)\})?\s*$/);
const task = line.match(/^(\s*)[-*+]\s+\[([ xX])\]\s+(.+)$/);
const ul = line.match(/^\s*[-*+]\s+(.+)$/);
const ol = line.match(/^\s*\d+\.\s+(.+)$/);
const ul = line.match(/^(\s*)[-*+]\s+(.+)$/);
const ol = line.match(/^(\s*)\d+\.\s+(.+)$/);
if (heading) {
closeList();
const n = heading[1].length;
const id = heading[3] ? ` id="${escapeHtml(heading[3])}"` : "";
const suffix = heading[3] ? ` {#${heading[3]}}` : "";
html += `<h${n}${id}${attrs(index, true, `${heading[1]} `, suffix)}>${inline(heading[2])}</h${n}>`;
html += `<h${n}${id}${attrs(index, true, `${heading[1]} `, suffix, lineOffset)}>${inline(heading[2])}</h${n}>`;
} else if (task) {
if (list !== "ul") { closeList(); html += `<ul class="task-list">`; list = "ul"; }
const checked = task[2].toLowerCase() === "x";
html += `<li class="preview-source-line task-list-item" data-source-line="${index + 1}"><input type="checkbox" class="task-checkbox" data-source-line="${index + 1}"${checked ? " checked" : ""}><span class="preview-editable" contenteditable="true" spellcheck="true" data-source-line="${index + 1}" data-source-prefix="${escapeHtml(`${task[1]}- [${checked ? "x" : " "}] `)}">${inline(task[3])}</span></li>`;
html += `<li class="preview-source-line task-list-item" data-source-line="${index + lineOffset + 1}"><input type="checkbox" class="task-checkbox" data-source-line="${index + lineOffset + 1}"${checked ? " checked" : ""}><span class="preview-editable" contenteditable="true" spellcheck="true" data-source-line="${index + lineOffset + 1}" data-source-prefix="${escapeHtml(`${task[1]}- [${checked ? "x" : " "}] `)}">${inline(task[3])}</span></li>`;
} else if (ul || ol) {
const type = ul ? "ul" : "ol";
if (list !== type) { closeList(); html += `<${type}>`; list = type; }
html += `<li${attrs(index, true, ul ? "- " : `${(line.match(/^\s*(\d+)\./)||[])[1] || 1}. `)}>${inline((ul || ol)[1])}</li>`;
const match = ul || ol;
const indentWidth = match[1].replace(/\t/g, " ").length;
const prefix = ul ? `${match[1]}- ` : `${match[1]}${(line.match(/^\s*(\d+)\./)||[])[1] || 1}. `;
const indentStyle = indentWidth ? ` style="margin-left:${Math.min(indentWidth, 24) * 0.45}em"` : "";
html += `<li${attrs(index, true, prefix, "", lineOffset)}${indentStyle}>${inline(match[2])}</li>`;
} else {
closeList();
const definition = index + 1 < lines.length && /^:\s+/.test(lines[index + 1]);
if (line.trim() && definition) {
html += `<dl${attrs(index)}><dt>${inline(line)}</dt>`;
html += `<dl${attrs(index, false, "", "", lineOffset)}><dt>${inline(line)}</dt>`;
while (index + 1 < lines.length && /^:\s+/.test(lines[index + 1])) {
index++;
html += `<dd data-source-line="${index + 1}">${inline(lines[index].replace(/^:\s+/, ""))}</dd>`;
html += `<dd data-source-line="${index + lineOffset + 1}">${inline(lines[index].replace(/^:\s+/, ""))}</dd>`;
}
html += `</dl>`;
} else if (/^---+$/.test(line.trim())) html += `<hr${attrs(index)}>`;
else if (line.startsWith("> ")) html += `<blockquote${attrs(index, true, "> ")}>${inline(line.slice(2))}</blockquote>`;
else if (line.trim()) html += `<p${attrs(index, true)}>${inline(line)}</p>`;
else html += `<div${attrs(index, true)}><br></div>`;
} else if (/^---+$/.test(line.trim())) html += `<hr${attrs(index, false, "", "", lineOffset)}>`;
else if (line.startsWith("> ")) html += `<blockquote${attrs(index, true, "> ", "", lineOffset)}>${inline(line.slice(2))}</blockquote>`;
else if (line.trim()) html += `<p${attrs(index, true, "", "", lineOffset)}>${inline(line)}</p>`;
else html += `<div${attrs(index, true, "", "", lineOffset)}><br></div>`;
}
}