new funtions and fixes
This commit is contained in:
+120
-8
@@ -54,6 +54,41 @@ function inline(value) {
|
||||
return html.replace(/\u0000T(\d+)\u0000/g, (_, index) => tokens[Number(index)] || "");
|
||||
}
|
||||
|
||||
|
||||
const languageAliases = {
|
||||
js: "javascript", javascript: "javascript", jsx: "javascript",
|
||||
ts: "typescript", typescript: "typescript", tsx: "typescript",
|
||||
py: "python", python: "python",
|
||||
rb: "ruby", ruby: "ruby",
|
||||
rs: "rust", rust: "rust",
|
||||
php: "php",
|
||||
sh: "bash", shell: "bash", bash: "bash", zsh: "bash",
|
||||
c: "c", h: "c",
|
||||
cpp: "cpp", "c++": "cpp", cxx: "cpp", hpp: "cpp",
|
||||
cs: "csharp", "c#": "csharp", csharp: "csharp",
|
||||
java: "java", kotlin: "kotlin", kt: "kotlin",
|
||||
go: "go", golang: "go",
|
||||
swift: "swift", dart: "dart", scala: "scala",
|
||||
html: "html", htm: "html", xml: "xml", svg: "xml",
|
||||
css: "css", scss: "scss", sass: "scss", less: "less",
|
||||
json: "json", jsonc: "json", yaml: "yaml", yml: "yaml", toml: "ini", ini: "ini",
|
||||
sql: "sql", graphql: "graphql", gql: "graphql",
|
||||
md: "markdown", markdown: "markdown",
|
||||
dockerfile: "dockerfile", docker: "dockerfile",
|
||||
makefile: "makefile", make: "makefile",
|
||||
powershell: "powershell", ps1: "powershell",
|
||||
lua: "lua", perl: "perl", pl: "perl", r: "r", matlab: "matlab",
|
||||
nginx: "nginx", apache: "apache", diff: "diff", patch: "diff",
|
||||
text: "plaintext", txt: "plaintext", plaintext: "plaintext", none: "plaintext",
|
||||
mermaid: "mermaid"
|
||||
};
|
||||
|
||||
function normalizeLanguage(value) {
|
||||
const language = String(value || "").trim().toLowerCase();
|
||||
if (!language) return "";
|
||||
return languageAliases[language] || language.replace(/[^a-z0-9_-]/g, "");
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
@@ -70,9 +105,44 @@ function tableDelimiter(line) {
|
||||
return cells.map(cell => cell.startsWith(":") && cell.endsWith(":") ? "center" : cell.endsWith(":") ? "right" : "left");
|
||||
}
|
||||
|
||||
function headingSlug(value) {
|
||||
return String(value)
|
||||
.replace(/\{#[A-Za-z][\w:.-]*\}\s*$/, "")
|
||||
.replace(/[`*_~^=<>]/g, "")
|
||||
.replace(/:([a-z0-9_+-]+):/gi, "$1")
|
||||
.toLowerCase().trim()
|
||||
.replace(/[^a-z0-9\u00c0-\u024f\u1e00-\u1eff]+/g, "-")
|
||||
.replace(/^-+|-+$/g, "") || "section";
|
||||
}
|
||||
|
||||
function collectHeadings(lines) {
|
||||
const used = new Map();
|
||||
const headings = [];
|
||||
let fence = null;
|
||||
lines.forEach((line, index) => {
|
||||
const fenceMatch = line.match(/^\s*(```+|~~~+)/);
|
||||
if (fenceMatch) {
|
||||
if (!fence) fence = fenceMatch[1];
|
||||
else if (fenceMatch[1][0] === fence[0] && fenceMatch[1].length >= fence.length) fence = null;
|
||||
return;
|
||||
}
|
||||
if (fence) return;
|
||||
const match = line.match(/^\s{0,4}(#{1,6})\s+(.+?)(?:\s+\{#([A-Za-z][\w:.-]*)\})?\s*$/);
|
||||
if (!match) return;
|
||||
const base = match[3] || headingSlug(match[2]);
|
||||
const count = used.get(base) || 0;
|
||||
used.set(base, count + 1);
|
||||
const id = count ? `${base}-${count + 1}` : base;
|
||||
headings.push({level: match[1].length, text: match[2], id, index});
|
||||
});
|
||||
return headings;
|
||||
}
|
||||
|
||||
export function renderMarkdown(source, lineOffset = 0) {
|
||||
let html = "", inCode = false, fence = "", language = "", code = [], codeStart = 0, list = null;
|
||||
let html = "", inCode = false, fence = "", language = "", codeLineStart = null, code = [], codeStart = 0, list = null;
|
||||
const lines = String(source).split("\n");
|
||||
const headings = collectHeadings(lines);
|
||||
const headingByLine = new Map(headings.map(item => [item.index, item]));
|
||||
const footnotes = new Map();
|
||||
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
@@ -92,10 +162,18 @@ export function renderMarkdown(source, lineOffset = 0) {
|
||||
const closeList = () => { if (list) { html += `</${list}>`; list = null; } };
|
||||
const closeCode = () => {
|
||||
const body = escapeHtml(code.join("\n"));
|
||||
html += language.toLowerCase() === "mermaid"
|
||||
? `<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 = "";
|
||||
const lang = normalizeLanguage(language);
|
||||
if (lang === "mermaid") {
|
||||
html += `<div class="mermaid preview-source-line" data-source-line="${codeStart + lineOffset + 1}">${body}</div>`;
|
||||
} else if (codeLineStart !== null) {
|
||||
const numbered = body.split("\n").map((line, index) => `<span class="code-line" data-line="${codeLineStart + index}">${line || " "}</span>`).join("\n");
|
||||
const languageClass = lang ? ` class="language-${escapeHtml(lang)}"` : "";
|
||||
html += `<pre${attrs(codeStart, false, "", "", lineOffset).replace(' class="', ' class="code-with-lines ')}><code${languageClass}>${numbered}</code></pre>`;
|
||||
} else {
|
||||
const languageClass = lang ? ` class="language-${escapeHtml(lang)}"` : "";
|
||||
html += `<pre${attrs(codeStart, false, "", "", lineOffset)}><code${languageClass}>${body}</code></pre>`;
|
||||
}
|
||||
code = []; language = ""; codeLineStart = null; fence = "";
|
||||
};
|
||||
|
||||
for (let index = 0; index < lines.length; index++) {
|
||||
@@ -104,7 +182,16 @@ export function renderMarkdown(source, lineOffset = 0) {
|
||||
if (fenceMatch) {
|
||||
closeList();
|
||||
if (inCode && fenceMatch[1][0] === fence[0] && fenceMatch[1].length >= fence.length) closeCode();
|
||||
else if (!inCode) { fence = fenceMatch[1]; language = fenceMatch[2] || ""; codeStart = index; }
|
||||
else if (!inCode) {
|
||||
fence = fenceMatch[1];
|
||||
const info = fenceMatch[2] || "";
|
||||
// Generic syntax for every fenced code block:
|
||||
// ```rust=, ```python=101, ```= or ```=101.
|
||||
const numbered = info.match(/^(.*?)=(\d*)$/);
|
||||
language = numbered ? numbered[1] : info;
|
||||
codeLineStart = numbered ? Number(numbered[2] || 1) : null;
|
||||
codeStart = index;
|
||||
}
|
||||
inCode = !inCode;
|
||||
continue;
|
||||
}
|
||||
@@ -149,6 +236,30 @@ export function renderMarkdown(source, lineOffset = 0) {
|
||||
}
|
||||
}
|
||||
|
||||
if (/^\s*\[TOC\]\s*$/i.test(line)) {
|
||||
closeList();
|
||||
if (headings.length) {
|
||||
html += `<nav class="markdown-toc preview-source-line" data-source-line="${index + lineOffset + 1}" aria-label="Table of contents"><ol>`;
|
||||
headings.forEach(item => html += `<li class="toc-level-${item.level}"><a href="#${escapeHtml(item.id)}">${inline(item.text)}</a></li>`);
|
||||
html += `</ol></nav>`;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
const alertStart = line.match(/^\s*:::(success|info|warning|danger)\s*$/i);
|
||||
if (alertStart) {
|
||||
closeList();
|
||||
let end = index + 1;
|
||||
while (end < lines.length && !/^\s*:::\s*$/.test(lines[end])) end++;
|
||||
if (end < lines.length) {
|
||||
const type = alertStart[1].toLowerCase();
|
||||
const body = lines.slice(index + 1, end).join("\n");
|
||||
html += `<aside class="markdown-alert markdown-alert--${type}" role="note">${renderMarkdown(body, lineOffset + index + 1)}</aside>`;
|
||||
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+(.+)$/);
|
||||
@@ -157,9 +268,10 @@ export function renderMarkdown(source, lineOffset = 0) {
|
||||
if (heading) {
|
||||
closeList();
|
||||
const n = heading[1].length;
|
||||
const id = heading[3] ? ` id="${escapeHtml(heading[3])}"` : "";
|
||||
const resolved = headingByLine.get(index);
|
||||
const id = resolved?.id || heading[3] || headingSlug(heading[2]);
|
||||
const suffix = heading[3] ? ` {#${heading[3]}}` : "";
|
||||
html += `<h${n}${id}${attrs(index, true, `${heading[1]} `, suffix, lineOffset)}>${inline(heading[2])}</h${n}>`;
|
||||
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";
|
||||
|
||||
Reference in New Issue
Block a user