From 4fb61429784b8ccb53802a5f724039ad46753b91 Mon Sep 17 00:00:00 2001 From: JK Date: Thu, 4 Jun 2026 17:54:46 +0200 Subject: [PATCH] Fix tikz for PDF printing --- esbuild.config.mjs | 19 +- src/features/tikz/row-layout.ts | 334 +++++++++++++++++++++----------- 2 files changed, 236 insertions(+), 117 deletions(-) diff --git a/esbuild.config.mjs b/esbuild.config.mjs index fc2936f..bc87054 100644 --- a/esbuild.config.mjs +++ b/esbuild.config.mjs @@ -43,15 +43,28 @@ const context = await esbuild.context({ minify: prod, sourcemap: prod ? false : "inline", treeShaking: true, - outfile: "../plugin-full-calendar/obsidian-dev-vault/.obsidian/plugins/Latex-like-equations/main.js", // Ensure this path is correct for your setup + outfile: "main.js", }); if (prod) { await context.rebuild(); + try { + fs.copyFileSync("main.js", "../plugin-full-calendar/obsidian-dev-vault/.obsidian/plugins/Latex-like-equations/main.js"); + } catch (e) { + console.warn("Could not copy main.js to dev-vault:", e.message); + } process.exit(0); } else { await context.watch(); } -fs.copyFileSync("src/styles/main.css", "../plugin-full-calendar/obsidian-dev-vault/.obsidian/plugins/Latex-like-equations/styles.css"); -fs.copyFileSync("./manifest.json", "../plugin-full-calendar/obsidian-dev-vault/.obsidian/plugins/Latex-like-equations/manifest.json"); \ No newline at end of file +try { + fs.copyFileSync("src/styles/main.css", "../plugin-full-calendar/obsidian-dev-vault/.obsidian/plugins/Latex-like-equations/styles.css"); +} catch (e) { + console.warn("Could not copy main.css to dev-vault:", e.message); +} +try { + fs.copyFileSync("./manifest.json", "../plugin-full-calendar/obsidian-dev-vault/.obsidian/plugins/Latex-like-equations/manifest.json"); +} catch (e) { + console.warn("Could not copy manifest.json to dev-vault:", e.message); +} \ No newline at end of file diff --git a/src/features/tikz/row-layout.ts b/src/features/tikz/row-layout.ts index 9bb68ff..f68359f 100644 --- a/src/features/tikz/row-layout.ts +++ b/src/features/tikz/row-layout.ts @@ -27,6 +27,126 @@ function formatWidth(w: string): string { return w; } +function splitParagraphAtNodeBoundary(p: HTMLParagraphElement, delimiterNode: Node) { + const parent = p.parentElement; + if (!parent) return; + + const newP = document.createElement("p"); + for (let attr of Array.from(p.attributes)) { + newP.setAttribute(attr.name, attr.value); + } + + let nextNode = delimiterNode.nextSibling as Node; + while (nextNode) { + const toMove = nextNode; + nextNode = nextNode.nextSibling as Node; + newP.appendChild(toMove); + } + + if (p.nextSibling) { + parent.insertBefore(newP, p.nextSibling); + } else { + parent.appendChild(newP); + } + + const cleanupBr = (el: HTMLElement) => { + while (el.firstChild && el.firstChild.nodeName.toLowerCase() === "br") el.removeChild(el.firstChild); + while (el.lastChild && el.lastChild.nodeName.toLowerCase() === "br") el.removeChild(el.lastChild); + }; + cleanupBr(p); + cleanupBr(newP); +} + +function preprocessContainerRows(container: HTMLElement) { + let mutated = true; + while (mutated) { + mutated = false; + const paragraphs = Array.from(container.querySelectorAll("p")); + + for (const p of paragraphs) { + if (!p.parentElement) continue; + + const childNodes = Array.from(p.childNodes); + for (let i = 0; i < childNodes.length; i++) { + const node = childNodes[i]; + if (node.nodeType !== Node.TEXT_NODE) continue; + + const text = node.textContent || ""; + const lines = text.split("\n"); + let offset = 0; + + for (let lineIdx = 0; lineIdx < lines.length; lineIdx++) { + const line = lines[lineIdx].trim(); + const isStart = line.startsWith(";;;row"); + const isDelimiter = line === ";;"; + const isClose = line === ";;;"; + + if (isStart || isDelimiter || isClose) { + const lineStartIdx = text.indexOf(line, offset); + if (lineStartIdx !== -1) { + const textNode = node as Text; + + // Split after the delimiter line first + const delimEndIdx = lineStartIdx + line.length; + if (delimEndIdx < text.length) { + textNode.splitText(delimEndIdx); + } + + // Split before the delimiter line + let delimNode: Node = textNode; + if (lineStartIdx > 0) { + delimNode = textNode.splitText(lineStartIdx); + } + + // Split the paragraph before the delimiter node if it's not the first child + if (delimNode.previousSibling) { + const newP = document.createElement("p"); + for (let attr of Array.from(p.attributes)) { + newP.setAttribute(attr.name, attr.value); + } + + let curr = delimNode; + while (curr) { + const next = curr.nextSibling; + newP.appendChild(curr); + curr = next as Node; + } + + if (p.nextSibling) { + p.parentElement.insertBefore(newP, p.nextSibling); + } else { + p.parentElement.appendChild(newP); + } + + const cleanupBr = (el: HTMLElement) => { + while (el.firstChild && el.firstChild.nodeName.toLowerCase() === "br") el.removeChild(el.firstChild); + while (el.lastChild && el.lastChild.nodeName.toLowerCase() === "br") el.removeChild(el.lastChild); + }; + cleanupBr(p); + cleanupBr(newP); + + mutated = true; + break; + } + + // Split the paragraph after the delimiter node if there are subsequent siblings + if (delimNode.nextSibling) { + splitParagraphAtNodeBoundary(p, delimNode); + mutated = true; + break; + } + } + } + offset += lines[lineIdx].length + 1; + } + if (mutated) break; + } + if (mutated) break; + } + } +} + + function tightenColumn(colEl: HTMLElement) { colEl.style.setProperty("margin-top", "0", "important"); colEl.style.setProperty("margin-bottom", "0", "important"); @@ -74,126 +194,114 @@ function tightenColumn(colEl: HTMLElement) { // ========================================== export const createRowLayoutProcessor = (plugin: LatexReferencer): MarkdownPostProcessor => { return (el, ctx) => { - const file = plugin.app.vault.getAbstractFileByPath(ctx.sourcePath); - if (!(file instanceof TFile)) return; + // Find the main container (e.g. .markdown-preview-view or .markdown-preview-section) + let cont: HTMLElement | null = el.parentElement; + while (cont && + !cont.classList.contains("markdown-preview-view") && + !cont.classList.contains("markdown-rendered") && + !cont.classList.contains("markdown-preview-section")) { + cont = cont.parentElement; + } + if (!cont) { + cont = el; + } + + // Split merged paragraphs to isolate delimiters into standalone blocks + preprocessContainerRows(cont); - // Find if this element itself is a row start paragraph, or contains one - const startP = el.tagName.toLowerCase() === "p" && el.textContent?.trim().startsWith(";;;row") - ? el - : el.querySelector("p"); - - if (!startP) return; - - const text = startP.textContent?.trim() || ""; - if (!text.startsWith(";;;row")) return; - - const section = ctx.getSectionInfo(el); - if (!section) return; - - // Read the entire active page content to build the line index mapping - plugin.app.vault.cachedRead(file).then(content => { - const lines = content.split(/\r?\n/); - const rows: MarkdownRow[] = []; - let currentRow: Partial | null = null; - - for (let i = 0; i < lines.length; i++) { - const line = lines[i].trim(); - if (line.startsWith(";;;row")) { - const widthsPart = line.substring(";;;row".length).trim().replace(/^:/, "").trim(); - let widths: string[] = []; - if (widthsPart) { - widths = widthsPart.split(/\s*\|\s*|\s*,\s*|\s+/).map(w => w.trim()).filter(w => w).map(formatWidth); - } - currentRow = { - startLine: i, - delimiters: [], - widths - }; - } else if (line === ";;") { - if (currentRow) { - currentRow.delimiters?.push(i); - } - } else if (line === ";;;") { - if (currentRow) { - currentRow.endLine = i; - rows.push(currentRow as MarkdownRow); - currentRow = null; - } + // Find all target paragraphs inside el that start with ;;;row + const targetParagraphs: HTMLElement[] = []; + if (el.tagName.toLowerCase() === "p" && el.textContent?.trim().startsWith(";;;row")) { + targetParagraphs.push(el); + } else { + el.querySelectorAll("p").forEach((p) => { + if (p.textContent?.trim().startsWith(";;;row")) { + targetParagraphs.push(p); } - } + }); + } - // Find the row corresponding to this start element - const activeRow = rows.find(r => r.startLine === section.lineStart); - if (!activeRow) return; + if (targetParagraphs.length === 0) return; + + targetParagraphs.forEach((startP) => { + const text = startP.textContent?.trim() || ""; // Defer DOM operations slightly to ensure siblings are attached setTimeout(() => { - const parent = el.parentElement; + const parent = startP.parentElement; if (!parent) return; - if (el.dataset.rowProcessed === "true") return; - el.dataset.rowProcessed = "true"; + if (startP.dataset.rowProcessed === "true") return; - const siblings = Array.from(parent.children) as HTMLElement[]; - const startIndex = siblings.indexOf(el as HTMLElement); - if (startIndex === -1) return; + // Find the main container (e.g. .markdown-preview-view or .markdown-preview-section) + let container: HTMLElement | null = startP.parentElement; + while (container && + !container.classList.contains("markdown-preview-view") && + !container.classList.contains("markdown-rendered") && + !container.classList.contains("markdown-preview-section")) { + container = container.parentElement; + } + if (!container) { + container = startP.parentElement; + } - // Group elements into columns based on their source line ranges - const numColumns = activeRow.delimiters.length + 1; - const columnsElements: HTMLElement[][] = Array.from({ length: numColumns }, () => []); + // Find topBlock (direct child of the container) + let topBlock: HTMLElement | null = startP; + while (topBlock && topBlock.parentElement !== container) { + topBlock = topBlock.parentElement; + } + if (!topBlock) return; + + const getBlockContent = (block: HTMLElement): HTMLElement => { + if (block.tagName.toLowerCase() === "div" && block.children.length === 1 && !block.className) { + return block.firstElementChild as HTMLElement; + } + return block; + }; + + // Traverse next siblings of topBlock under container + const columnsElements: HTMLElement[][] = [[]]; const delimitersToRemove: HTMLElement[] = []; let closingElement: HTMLElement | null = null; + let foundEnd = false; - siblings.forEach((sib) => { - const sibSec = ctx.getSectionInfo(sib); - if (!sibSec) return; + let sibBlock = topBlock.nextElementSibling as HTMLElement | null; + + while (sibBlock) { + const contentEl = getBlockContent(sibBlock); + const textVal = contentEl.textContent?.trim() || ""; + const isDelimiter = contentEl.tagName.toLowerCase() === "p" && textVal === ";;"; + const isClose = contentEl.tagName.toLowerCase() === "p" && textVal === ";;;"; - const sibLine = sibSec.lineStart; - - if (sibLine === activeRow.startLine) return; - - if (activeRow.delimiters.includes(sibLine)) { - delimitersToRemove.push(sib); - return; - } - - if (sibLine === activeRow.endLine) { - closingElement = sib; - return; - } - - for (let colIdx = 0; colIdx < numColumns; colIdx++) { - const startBound = colIdx === 0 ? activeRow.startLine : activeRow.delimiters[colIdx - 1]; - const endBound = colIdx === numColumns - 1 ? activeRow.endLine : activeRow.delimiters[colIdx]; - - if (sibLine > startBound && sibLine < endBound) { - columnsElements[colIdx].push(sib); - break; - } - } - }); - - // Extract column markdown texts from lines - const columnsMarkdown: string[] = []; - const rowLines = lines.slice(activeRow.startLine + 1, activeRow.endLine); - let currentColLines: string[] = []; - - for (let rIdx = 0; rIdx < rowLines.length; rIdx++) { - const rLine = rowLines[rIdx]; - if (rLine.trim() === ";;") { - columnsMarkdown.push(currentColLines.join("\n")); - currentColLines = []; + if (isDelimiter) { + delimitersToRemove.push(sibBlock); + columnsElements.push([]); + } else if (isClose) { + closingElement = sibBlock; + foundEnd = true; + break; } else { - currentColLines.push(rLine); + columnsElements[columnsElements.length - 1].push(sibBlock); } + sibBlock = sibBlock.nextElementSibling as HTMLElement | null; } - columnsMarkdown.push(currentColLines.join("\n")); - // Create CSS grid layout columns + if (!foundEnd) return; + + startP.dataset.rowProcessed = "true"; + + // Parse widths from the start line + const widthsPart = text.substring(";;;row".length).trim().replace(/^:/, "").trim(); + let widths: string[] = []; + if (widthsPart) { + widths = widthsPart.split(/\s*\|\s*|\s*,\s*|\s+/).map(w => w.trim()).filter(w => w).map(formatWidth); + } + + const numColumns = columnsElements.length; const gridTracks: string[] = []; for (let colIdx = 0; colIdx < numColumns; colIdx++) { - if (colIdx < activeRow.widths.length) { - gridTracks.push(activeRow.widths[colIdx]); + if (colIdx < widths.length) { + gridTracks.push(widths[colIdx]); } else { gridTracks.push("1fr"); } @@ -208,31 +316,29 @@ export const createRowLayoutProcessor = (plugin: LatexReferencer): MarkdownPostP rowEl.style.alignItems = "center"; rowEl.style.margin = "-0.5em 0"; - columnsMarkdown.forEach((colMarkdown) => { + columnsElements.forEach((colEls) => { const colEl = rowEl.createEl("div", { cls: "latex-referencer-column" }); colEl.style.display = "flex"; colEl.style.flexDirection = "column"; colEl.style.justifyContent = "center"; colEl.style.minWidth = "0"; - // Asynchronously render markdown inside layout column - const comp = new MarkdownRenderChild(colEl); - ctx.addChild(comp); - MarkdownRenderer.render(plugin.app, colMarkdown, colEl, ctx.sourcePath, comp) - .then(() => { - tightenColumn(colEl); - setTimeout(() => tightenColumn(colEl), 50); - setTimeout(() => tightenColumn(colEl), 150); - setTimeout(() => tightenColumn(colEl), 500); - }) - .catch((err) => console.error("Latex Referencer: Failed to render column markdown", err)); + // Move existing elements into the column + colEls.forEach(item => colEl.appendChild(item)); + + tightenColumn(colEl); + setTimeout(() => tightenColumn(colEl), 50); + setTimeout(() => tightenColumn(colEl), 150); + setTimeout(() => tightenColumn(colEl), 500); }); // Replace start element and remove all old intermediate DOM nodes - parent.replaceChild(rowEl, el); + if (topBlock.parentElement) { + topBlock.parentElement.replaceChild(rowEl, topBlock); + } + delimitersToRemove.forEach(sib => sib.remove()); if (closingElement) (closingElement as HTMLElement).remove(); - columnsElements.forEach(colEls => colEls.forEach(sib => sib.remove())); }, 0); }); };