fixes
This commit is contained in:
+77
-18
@@ -92,6 +92,63 @@ function normalizeLanguage(value) {
|
||||
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 listLine(line) {
|
||||
const match = line.match(/^(\s*)([-*+]|(\d+)\.)\s+(?:\[([ xX])\]\s+)?(.+)$/);
|
||||
if (!match) return null;
|
||||
const indent = match[1].replace(/\t/g, " ").length;
|
||||
return {
|
||||
indent,
|
||||
whitespace: match[1],
|
||||
type: match[3] ? "ol" : "ul",
|
||||
number: match[3] ? Number(match[3]) : null,
|
||||
checked: match[4] == null ? null : match[4].toLowerCase() === "x",
|
||||
text: match[5],
|
||||
marker: match[3] ? `${match[3]}. ` : `${match[2]} `
|
||||
};
|
||||
}
|
||||
|
||||
function renderList(lines, start, lineOffset = 0, baseIndent = null, forcedType = null, depth = 0) {
|
||||
const first = listLine(lines[start]);
|
||||
if (!first) return null;
|
||||
const indent = baseIndent == null ? first.indent : baseIndent;
|
||||
const type = forcedType || first.type;
|
||||
let index = start;
|
||||
let body = "";
|
||||
let hasTask = false;
|
||||
|
||||
while (index < lines.length) {
|
||||
const item = listLine(lines[index]);
|
||||
if (!item || item.indent < indent || item.indent !== indent || item.type !== type) break;
|
||||
|
||||
const sourceLine = index + lineOffset + 1;
|
||||
const prefix = `${item.whitespace}${item.marker}${item.checked == null ? "" : `[${item.checked ? "x" : " "}] `}`;
|
||||
const valueAttr = "";
|
||||
const taskClass = item.checked == null ? "" : " task-list-item";
|
||||
hasTask ||= item.checked != null;
|
||||
const gutterOffset = `${(depth + 1) * 1.75}em`;
|
||||
body += `<li class="preview-source-line list-source-line${taskClass}" data-source-line="${sourceLine}"${valueAttr} style="--list-gutter-offset:${gutterOffset}">`;
|
||||
if (item.checked != null) {
|
||||
body += `<input type="checkbox" class="task-checkbox" data-source-line="${sourceLine}"${item.checked ? " checked" : ""}>`;
|
||||
}
|
||||
body += `<span class="preview-editable list-item-content" contenteditable="true" spellcheck="true" data-source-line="${sourceLine}" data-source-prefix="${escapeHtml(prefix)}">${inline(item.text)}</span>`;
|
||||
index++;
|
||||
|
||||
while (index < lines.length) {
|
||||
const nested = listLine(lines[index]);
|
||||
if (!nested || nested.indent <= indent) break;
|
||||
const rendered = renderList(lines, index, lineOffset, nested.indent, nested.type, depth + 1);
|
||||
if (!rendered) break;
|
||||
body += rendered.html;
|
||||
index = rendered.end;
|
||||
}
|
||||
body += `</li>`;
|
||||
}
|
||||
|
||||
const classAttr = hasTask ? ` class="contains-task-items"` : "";
|
||||
return {html: `<${type}${classAttr}>${body}</${type}>`, end: index};
|
||||
}
|
||||
|
||||
function splitTableRow(line) {
|
||||
let value = line.trim();
|
||||
if (value.startsWith("|")) value = value.slice(1);
|
||||
@@ -138,8 +195,20 @@ function collectHeadings(lines) {
|
||||
return headings;
|
||||
}
|
||||
|
||||
|
||||
export function alignPreviewLineNumbers(root) {
|
||||
if (!root) return;
|
||||
const styles = getComputedStyle(root);
|
||||
const targetLeft = parseFloat(styles.paddingLeft || "0") - 50;
|
||||
const rootLeft = root.getBoundingClientRect().left;
|
||||
root.querySelectorAll(".preview-source-line").forEach(line => {
|
||||
const lineLeft = line.getBoundingClientRect().left - rootLeft;
|
||||
line.style.setProperty("--preview-line-left", `${targetLeft - lineLeft}px`);
|
||||
});
|
||||
}
|
||||
|
||||
export function renderMarkdown(source, lineOffset = 0) {
|
||||
let html = "", inCode = false, fence = "", language = "", codeLineStart = null, code = [], codeStart = 0, list = null;
|
||||
let html = "", inCode = false, fence = "", language = "", codeLineStart = null, code = [], codeStart = 0;
|
||||
const lines = String(source).split("\n");
|
||||
const headings = collectHeadings(lines);
|
||||
const headingByLine = new Map(headings.map(item => [item.index, item]));
|
||||
@@ -159,7 +228,7 @@ export function renderMarkdown(source, lineOffset = 0) {
|
||||
lines[i] = "";
|
||||
}
|
||||
|
||||
const closeList = () => { if (list) { html += `</${list}>`; list = null; } };
|
||||
const closeList = () => {};
|
||||
const closeCode = () => {
|
||||
const body = escapeHtml(code.join("\n"));
|
||||
const lang = normalizeLanguage(language);
|
||||
@@ -261,29 +330,19 @@ export function renderMarkdown(source, lineOffset = 0) {
|
||||
}
|
||||
|
||||
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 listItem = listLine(line);
|
||||
|
||||
if (heading) {
|
||||
if (listItem) {
|
||||
const rendered = renderList(lines, index, lineOffset, listItem.indent, listItem.type);
|
||||
html += rendered.html;
|
||||
index = rendered.end - 1;
|
||||
} else if (heading) {
|
||||
closeList();
|
||||
const n = heading[1].length;
|
||||
const resolved = headingByLine.get(index);
|
||||
const id = resolved?.id || heading[3] || headingSlug(heading[2]);
|
||||
const suffix = heading[3] ? ` {#${heading[3]}}` : "";
|
||||
html += `<h${n} id="${escapeHtml(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 + 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; }
|
||||
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]);
|
||||
|
||||
Reference in New Issue
Block a user