feat(pdf): add configurable PDF export and improve print rendering

This commit is contained in:
Mateusz Gruszczyński
2026-09-12 00:01:25 +02:00
parent f9cf17554a
commit c5e01916c8
12 changed files with 584 additions and 14 deletions
+32 -3
View File
@@ -360,14 +360,43 @@ function renderTableOfContentsList(nodes) {
}
function firstRenderedTextRect(element) {
const walker = document.createTreeWalker(element, NodeFilter.SHOW_TEXT);
let node;
while ((node = walker.nextNode())) {
const match = node.nodeValue?.match(/\S/);
if (!match) continue;
const range = document.createRange();
const codePoint = node.nodeValue.codePointAt(match.index);
const characterLength = codePoint > 0xffff ? 2 : 1;
range.setStart(node, match.index);
range.setEnd(node, match.index + characterLength);
const rect = range.getClientRects()[0];
range.detach?.();
if (rect?.width || rect?.height) return rect;
}
return null;
}
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 + root.scrollLeft;
const lineRect = line.getBoundingClientRect();
const lineLeft = lineRect.left - rootLeft + root.scrollLeft;
line.style.setProperty("--preview-line-left", `${targetLeft - lineLeft}px`);
const textRect = firstRenderedTextRect(line);
const lineStyles = getComputedStyle(line);
const gutterStyles = getComputedStyle(line, "::before");
const gutterLineHeight = parseFloat(gutterStyles.lineHeight) || parseFloat(gutterStyles.fontSize) || 0;
const textLineHeight = textRect?.height || 0;
const borderTop = parseFloat(lineStyles.borderTopWidth) || 0;
const textTop = textRect ? textRect.top - lineRect.top - borderTop : 0;
const gutterTop = Math.max(0, textTop + (textLineHeight - gutterLineHeight) / 2);
line.style.setProperty("--preview-line-top", `${gutterTop}px`);
});
}
@@ -399,7 +428,7 @@ export function renderMarkdown(source, lineOffset = 0) {
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 numbered = body.split("\n").map((line, index) => `<span class="code-line" data-line="${codeLineStart + index}">${line || " "}</span>`).join("");
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 {
@@ -524,7 +553,7 @@ export function renderMarkdown(source, lineOffset = 0) {
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, false, "", "", lineOffset)}>`;
} else if (/^---+$/.test(line.trim())) html += `<hr${attrs(index, false, "", "", lineOffset).replace(' class="', ' class="page-break-marker ')} data-page-break="true">`;
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>`;