import { MarkdownRendererAPIOptions, MarkdownWebpageRendererAPIOptions } from "./api-options"; import { Component, Notice, WorkspaceLeaf, MarkdownRenderer as ObsidianRenderer, MarkdownPreviewView, loadMermaid, TFile, MarkdownView, View } from "obsidian"; import { Utils } from "scripts/utils/utils"; import { TabManager } from "scripts/utils/tab-manager"; import { Webpage } from "scripts/objects/webpage"; import * as electron from 'electron'; import { ExportLog } from "./html-generation/render-log"; import { AssetHandler } from "./html-generation/asset-handler"; export namespace MarkdownRendererAPI { export let convertableExtensions = ["md", "canvas", "drawing", "excalidraw"]; // drawing is an alias for excalidraw function makeHeadingsTrees(html: HTMLElement) { // make headers into format: /* - .heading-wrapper - h1.heading - .heading-collapse-indicator.collapse-indicator.collapse-icon - "Text" - .heading-children */ function getHeaderEl(headingContainer: HTMLDivElement) { let first = headingContainer.firstElementChild; if (first && /[Hh][1-6]/g.test(first.tagName)) return first; else return; } function makeHeaderTree(headerDiv: HTMLDivElement, childrenContainer: HTMLElement) { let headerEl = getHeaderEl(headerDiv); if (!headerEl) return; let possibleChild = headerDiv.nextElementSibling; while (possibleChild != null) { let possibleChildHeader = getHeaderEl(possibleChild as HTMLDivElement); if(possibleChildHeader) { // if header is a sibling of this header then break if (possibleChildHeader.tagName <= headerEl.tagName) { break; } // if we reached the footer then break if (possibleChildHeader.querySelector(":has(section.footnotes)") || possibleChildHeader.classList.contains("mod-footer")) { break; } } let nextEl = possibleChild.nextElementSibling; childrenContainer.appendChild(possibleChild); possibleChild = nextEl; } } html.querySelectorAll("div:has(> :is(h1, h2, h3, h4, h5, h6):not([class^='block-language-'] *)):not(.markdown-preview-sizer)").forEach(function (header: HTMLDivElement) { header.classList.add("heading-wrapper"); let hEl = getHeaderEl(header) as HTMLHeadingElement; if (!hEl || hEl.classList.contains("heading")) return; hEl.classList.add("heading"); let collapseIcon = hEl.querySelector(".heading-collapse-indicator"); if (!collapseIcon) { collapseIcon = hEl.createDiv({ cls: "heading-collapse-indicator collapse-indicator collapse-icon" }); collapseIcon.innerHTML = _MarkdownRendererInternal.arrowHTML; hEl.prepend(collapseIcon); } let children = header.createDiv({ cls: "heading-children" }); makeHeaderTree(header, children); }); // add "heading" class to all headers that don't have it html.querySelectorAll(":is(h1, h2, h3, h4, h5, h6):not(.heading)").forEach((el) => el.classList.add("heading")); // remove collapsible arrows from h1 and inline titles html.querySelectorAll("div h1, div .inline-title").forEach((element) => { element.querySelector(".heading-collapse-indicator")?.remove(); }); // remove all new lines from header elements which cause spacing issues html.querySelectorAll("h1, h2, h3, h4, h5, h6").forEach((el) => el.innerHTML = el.innerHTML.replaceAll("\n", "")); } export async function renderMarkdownToString(markdown: string, options?: MarkdownRendererAPIOptions): Promise { options = Object.assign(new MarkdownRendererAPIOptions(), options); let html = await _MarkdownRendererInternal.renderMarkdown(markdown, options); if (!html) return; if(options.postProcess) await _MarkdownRendererInternal.postProcessHTML(html, options); if (options.makeHeadersTrees) makeHeadingsTrees(html); let text = html.innerHTML; if (!options.container) html.remove(); return text; } export async function renderMarkdownToElement(markdown: string, options?: MarkdownRendererAPIOptions): Promise { options = Object.assign(new MarkdownRendererAPIOptions(), options); let html = await _MarkdownRendererInternal.renderMarkdown(markdown, options); if (!html) return; if(options.postProcess) await _MarkdownRendererInternal.postProcessHTML(html, options); if (options.makeHeadersTrees) makeHeadingsTrees(html); return html; } // export async function renderMarkdownsToStrings(markdowns: string[], options?: MarkdownRendererAPIOptions): Promise<(string | undefined)[]> // { // options = Object.assign(new MarkdownRendererAPIOptions(), options); // await _MarkdownRendererInternal.beginBatch(options); // let results = await Promise.all(markdowns.map(markdown => this.renderMarkdownToString(markdown, options))); // _MarkdownRendererInternal.endBatch(); // return results; // } // export async function renderMarkdownsToElements(markdowns: string[], options?: MarkdownRendererAPIOptions): Promise<(HTMLElement | undefined)[]> // { // options = Object.assign(new MarkdownRendererAPIOptions(), options); // await _MarkdownRendererInternal.beginBatch(options); // let results = await Promise.all(markdowns.map(markdown => this.renderMarkdownToElement(markdown, options))); // _MarkdownRendererInternal.endBatch(); // return results; // } export async function renderFile(file: TFile, options?: MarkdownRendererAPIOptions): Promise<{contentEl: HTMLElement; viewType: string;} | undefined> { options = Object.assign(new MarkdownRendererAPIOptions(), options); let result = await _MarkdownRendererInternal.renderFile(file, options); if (!result) return; if (options.postProcess) await _MarkdownRendererInternal.postProcessHTML(result.contentEl, options); if (options.makeHeadersTrees) makeHeadingsTrees(result.contentEl); return result; } export async function renderFileToString(file: TFile, options?: MarkdownRendererAPIOptions): Promise { options = Object.assign(new MarkdownRendererAPIOptions(), options); let result = await this.renderFile(file, options); if (!result) return; let text = result.contentEl.innerHTML; if (!options.container) result.contentEl.remove(); return text; } export async function renderFileToWebpage(file: TFile, options?: MarkdownWebpageRendererAPIOptions): Promise { options = Object.assign(new MarkdownWebpageRendererAPIOptions(), options); this.beginBatch(options); let webpage : Webpage | undefined = new Webpage(file, undefined, file.basename, undefined, options); webpage = await webpage.create(); if (!webpage) { ExportLog.error("Failed to create webpage for file " + file.path); return; } this.endBatch(); return webpage; } export async function renderMarkdownSimple(markdown: string): Promise { let container = document.body.createDiv(); await _MarkdownRendererInternal.renderSimpleMarkdown(markdown, container); let text = container.innerHTML; container.remove(); return text; } export async function renderMarkdownSimpleEl(markdown: string, container: HTMLElement) { await _MarkdownRendererInternal.renderSimpleMarkdown(markdown, container); } export function isConvertable(extention: string) { if (extention.startsWith(".")) extention = extention.substring(1); return this.convertableExtensions.contains(extention); } export function checkCancelled(): boolean { return _MarkdownRendererInternal.checkCancelled(); } export async function beginBatch(options?: MarkdownRendererAPIOptions) { options = Object.assign(new MarkdownRendererAPIOptions(), options); await _MarkdownRendererInternal.beginBatch(options); } export function endBatch() { _MarkdownRendererInternal.endBatch(); } } export namespace _MarkdownRendererInternal { export let renderLeaf: WorkspaceLeaf | undefined; export let electronWindow: electron.BrowserWindow | undefined; export let errorInBatch: boolean = false; export let cancelled: boolean = false; export let batchStarted: boolean = false; let logContainer: HTMLElement | undefined; let loadingContainer: HTMLElement | undefined; const infoColor = "var(--text-normal)"; const warningColor = "var(--color-yellow)"; const errorColor = "var(--color-red)"; const infoBoxColor = "rgba(0,0,0,0.15)" const warningBoxColor = "rgba(var(--color-yellow-rgb), 0.15)"; const errorBoxColor = "rgba(var(--color-red-rgb), 0.15)"; export const arrowHTML = ""; export function checkCancelled(): boolean { if (_MarkdownRendererInternal.cancelled || !_MarkdownRendererInternal.renderLeaf) { ExportLog.log("cancelled"); _MarkdownRendererInternal.endBatch(); return true; } return false; } function failRender(file: TFile | undefined, message: any): undefined { if (checkCancelled()) return undefined; ExportLog.error(message, `Rendering ${file?.path ?? " custom markdown "} failed: `); return; } export async function renderFile(file: TFile, options: MarkdownRendererAPIOptions): Promise<{contentEl: HTMLElement, viewType: string} | undefined> { let loneFile = !batchStarted; if (loneFile) { ExportLog.log("Exporting single file, starting batch"); await _MarkdownRendererInternal.beginBatch(options); } let success = await Utils.waitUntil(() => renderLeaf != undefined || checkCancelled(), 2000, 1); if (!success || !renderLeaf) return failRender(file, "Failed to get leaf for rendering!"); try { await renderLeaf.openFile(file, { active: false}); } catch (e) { return failRender(file, e); } let html: HTMLElement | undefined; let view = renderLeaf.view; let viewType = view.getViewType(); switch(viewType) { case "markdown": // @ts-ignore let preview = view.previewMode; html = await renderMarkdownView(preview, options); break; case "kanban": html = await renderGeneric(view, options); break; case "excalidraw": html = await renderExcalidraw(view, options); break; case "canvas": html = await renderCanvas(view, options); break; default: html = await renderGeneric(view, options); break; } if(checkCancelled()) return undefined; if (!html) return failRender(file, "Failed to render file!"); if (loneFile) _MarkdownRendererInternal.endBatch(); return {contentEl: html, viewType: viewType}; } export async function renderMarkdown(markdown: string, options: MarkdownRendererAPIOptions): Promise { let loneFile = !batchStarted; if (loneFile) { ExportLog.log("Exporting single file, starting batch"); await _MarkdownRendererInternal.beginBatch(options); } let success = await Utils.waitUntil(() => renderLeaf != undefined || checkCancelled(), 2000, 1); if (!success || !renderLeaf) return failRender(undefined, "Failed to get leaf for rendering!"); let view = new MarkdownView(renderLeaf); renderLeaf.view = view; try { view.setViewData(markdown, true); } catch (e) { return failRender(undefined, e); } let html: HTMLElement | undefined; // @ts-ignore let preview = view.previewMode; html = await renderMarkdownView(preview, options); if(checkCancelled()) return undefined; if (!html) return failRender(undefined, "Failed to render file!"); if (loneFile) _MarkdownRendererInternal.endBatch(); return html; } export async function renderMarkdownView(preview: MarkdownPreviewView, options: MarkdownRendererAPIOptions): Promise { preview.load(); // @ts-ignore let renderer = preview.renderer; await renderer.unfoldAllHeadings(); await renderer.unfoldAllLists(); await renderer.parseSync(); // @ts-ignore if (!window.mermaid) { await loadMermaid(); } let sections = renderer.sections as {"rendered": boolean, "height": number, "computed": boolean, "lines": number, "lineStart": number, "lineEnd": number, "used": boolean, "highlightRanges": number, "level": number, "headingCollapsed": boolean, "shown": boolean, "usesFrontMatter": boolean, "html": string, "el": HTMLElement}[]; let newMarkdownEl = document.body.createDiv({ cls: "markdown-preview-view markdown-rendered" }); let newSizerEl = newMarkdownEl.createDiv({ cls: "markdown-preview-sizer markdown-preview-section" }); if (!newMarkdownEl || !newSizerEl) return failRender(preview.file, "Please specify a container element, or enable keepViewContainer!"); preview.containerEl = newSizerEl; // @ts-ignore let promises: Promise[] = []; let foldedCallouts: HTMLElement[] = []; for (let i = 0; i < sections.length; i++) { let section = sections[i]; section.shown = true; section.rendered = false; // @ts-ignore section.resetCompute(); // @ts-ignore section.setCollapsed(false); section.el.empty(); newSizerEl.appendChild(section.el); // @ts-ignore await section.render(); // @ts-ignore let success = await Utils.waitUntil(() => (section.el && section.rendered) || checkCancelled(), 2000, 1); if (!success) return failRender(preview.file, "Failed to render section!"); await renderer.measureSection(section); success = await Utils.waitUntil(() => section.computed || checkCancelled(), 2000, 1); if (!success) return failRender(preview.file, "Failed to compute section!"); // @ts-ignore await preview.postProcess(section, promises, renderer.frontmatter); // unfold callouts let folded = Array.from(section.el.querySelectorAll(".callout-content[style*='display: none']")) as HTMLElement[]; for (let callout of folded) { callout.style.display = ""; } foldedCallouts.push(...folded); // dataview support // @ts-ignore let dataview = app.plugins.plugins["dataview"]; if (dataview) { let jsKeyword = dataview.settings?.dataviewJsKeyword ?? "dataviewjs"; let emptyDataviewSelector = `:is(.block-language-dataview, .block-language-${jsKeyword}):not(.node-insert-event), :is(.block-language-dataview, .block-language-${jsKeyword}):empty` await Utils.waitUntil(() => !section.el.querySelector(emptyDataviewSelector) || checkCancelled(), 4000, 1); if (checkCancelled()) return undefined; if (section.el.querySelector(emptyDataviewSelector)) { ExportLog.warning("Dataview plugin elements were not rendered correctly in file " + preview.file.name + "!"); } } // wait for transclusions await Utils.waitUntil(() => !section.el.querySelector(".markdown-preview-sizer:empty") || checkCancelled(), 500, 1); if (checkCancelled()) return undefined; if (section.el.querySelector(".markdown-preview-sizer:empty")) { ExportLog.warning("Transclusions were not rendered correctly in file " + preview.file.name + "!"); } // wait for generic plugins await Utils.waitUntil(() => !section.el.querySelector("[class^='block-language-']:empty") || checkCancelled(), 500, 1); if (checkCancelled()) return undefined; // convert canvas elements into images here because otherwise they will lose their data when moved let canvases = Array.from(section.el.querySelectorAll("canvas:not(.pdf-embed canvas)")) as HTMLCanvasElement[]; for (let canvas of canvases) { let data = canvas.toDataURL(); if (data.length < 100) { ExportLog.log(canvas.outerHTML, "Failed to render canvas based plugin element in file " + preview.file.name + ":"); canvas.remove(); continue; } let image = document.createElement("img"); image.src = data; image.style.width = canvas.style.width || "100%"; image.style.maxWidth = "100%"; canvas.replaceWith(image); }; console.debug(section.el.outerHTML); // for some reason adding this line here fixes an issue where some plugins wouldn't render let invalidPluginBlocks = Array.from(section.el.querySelectorAll("[class^='block-language-']:empty")); for (let block of invalidPluginBlocks) { ExportLog.warning(`Plugin element ${block.className || block.parentElement?.className || "unknown"} from ${preview.file.name} not rendered correctly!`); } } // @ts-ignore await Promise.all(promises); // refold callouts for (let callout of foldedCallouts) { callout.style.display = "none"; } newSizerEl.empty(); // move all of them back in since rendering can cause some sections to move themselves out of their container for (let i = 0; i < sections.length; i++) { let section = sections[i]; newSizerEl.appendChild(section.el.cloneNode(true)); } // get banner plugin banner and insert it before the sizer element let banner = preview.containerEl.querySelector(".obsidian-banner-wrapper"); if (banner) { newSizerEl.before(banner); } // if we aren't kepping the view element then only keep the content of the sizer element if (options.keepViewContainer === false) { newMarkdownEl.outerHTML = newSizerEl.innerHTML; console.log("keeping only sizer content"); } options.container?.appendChild(newMarkdownEl); return newMarkdownEl; } export async function renderSimpleMarkdown(markdown: string, container: HTMLElement) { let renderComp = new Component(); renderComp.load(); await ObsidianRenderer.render(app, markdown, container, "/", renderComp); renderComp.unload(); let renderedEl = container.children[container.children.length - 1]; if (renderedEl && renderedEl.tagName == "P") { renderedEl.outerHTML = renderedEl.innerHTML; // remove the outer

tag } // remove tags container.querySelectorAll("a.tag").forEach((element: HTMLAnchorElement) => { element.remove(); }); //remove rendered lists and replace them with plain text container.querySelectorAll("ol").forEach((listEl: HTMLElement) => { if(listEl.parentElement) { let start = listEl.getAttribute("start") ?? "1"; listEl.parentElement.createSpan().outerHTML = `${start}. ${listEl.innerText}`; listEl.remove(); } }); container.querySelectorAll("ul").forEach((listEl: HTMLElement) => { if(listEl.parentElement) { listEl.parentElement.createSpan().innerHTML = "- " + listEl.innerHTML; listEl.remove(); } }); container.querySelectorAll("li").forEach((listEl: HTMLElement) => { if(listEl.parentElement) { listEl.parentElement.createSpan().innerHTML = listEl.innerHTML; listEl.remove(); } }); } async function renderGeneric(view: View, options: MarkdownRendererAPIOptions): Promise { await Utils.delay(2000); if (checkCancelled()) return undefined; // @ts-ignore let contentEl = view.containerEl; options.container?.appendChild(contentEl); return contentEl; } async function renderExcalidraw(view: any, options: MarkdownRendererAPIOptions): Promise { await Utils.delay(500); // @ts-ignore let scene = view.excalidrawData.scene; // @ts-ignore let svg = await view.svg(scene, "", false); // remove rect fill let isLight = !svg.getAttribute("filter"); if (!isLight) svg.removeAttribute("filter"); svg.classList.add(isLight ? "light" : "dark"); let contentEl = document.createElement("div"); contentEl.classList.add("view-content"); let sizerEl = contentEl.createDiv(); sizerEl.classList.add("excalidraw-plugin"); sizerEl.appendChild(svg); if (checkCancelled()) return undefined; if (options.keepViewContainer === false) { contentEl = svg; } options.container?.appendChild(contentEl); return contentEl; } export async function renderCanvas(view: any, options: MarkdownRendererAPIOptions): Promise { if (checkCancelled()) return undefined; let canvas = view.canvas; let nodes = canvas.nodes; let edges = canvas.edges; for (const node of nodes) { await node[1].render(); } for (const edge of edges) { await edge[1].render(); } canvas.zoomToFit(); await Utils.delay(500); let contentEl = view.contentEl; let canvasEl = contentEl.querySelector(".canvas"); canvasEl.innerHTML = ""; let edgeContainer = canvasEl.createEl("svg", { cls: "canvas-edges" }); let edgeHeadContainer = canvasEl.createEl("svg", { cls: "canvas-edges" }); for (const node of nodes) { let nodeEl = node[1].nodeEl; let childPreview = node[1]?.child?.previewMode; let embedEl = nodeEl.querySelector(".markdown-embed-content.node-insert-event"); if (childPreview && embedEl) { node[1].render(); embedEl.innerHTML = ""; let optionsCopy = Object.assign({}, options); optionsCopy.container = embedEl; await renderMarkdownView(childPreview, optionsCopy); } canvasEl.appendChild(nodeEl); } for (const edge of edges) { let edgeEl = edge[1].lineGroupEl; let headEl = edge[1].lineEndGroupEl; edgeContainer.appendChild(edgeEl); edgeHeadContainer.appendChild(headEl); if(edge[1].label) { let labelEl = edge[1].labelElement.wrapperEl; canvasEl.appendChild(labelEl); } } if (checkCancelled()) return undefined; if (options.keepViewContainer === false) { contentEl = canvasEl; } options.container?.appendChild(contentEl); return contentEl; } export async function postProcessHTML(html: HTMLElement, options: MarkdownRendererAPIOptions) { // remove the extra elements if they are not wanted if (options.keepViewContainer === false) { html.querySelectorAll(".mod-header, .mod-footer").forEach((e: HTMLElement) => e.remove()); } // transclusions put a div inside a p tag, which is invalid html. Fix it here html.querySelectorAll("p:has(div)").forEach((element) => { // replace the p tag with a span let span = document.body.createEl("span"); span.innerHTML = element.innerHTML; element.replaceWith(span); span.style.display = "block"; span.style.marginBlockStart = "var(--p-spacing)"; span.style.marginBlockEnd = "var(--p-spacing)"; }); // encode all text input values into attributes html.querySelectorAll("input[type=text]").forEach((element: HTMLElement) => { // @ts-ignore element.setAttribute("value", element.value); // @ts-ignore element.value = ""; }); // encode all text area values into text content html.querySelectorAll("textarea").forEach((element: HTMLElement) => { // @ts-ignore element.textContent = element.value; }); // convert tag href to search query html.querySelectorAll("a.tag").forEach((element: HTMLAnchorElement) => { let split = element.href.split("#"); let tag = split[1] ?? element.href.substring(1); // remove the # element.setAttribute("href", `?query=tag:${tag}`); }); // convert all hard coded image / media widths into max widths html.querySelectorAll("img, video, .media-embed:has( > :is(img, video))").forEach((element: HTMLElement) => { let width = element.getAttribute("width"); if (width) { element.removeAttribute("width"); element.style.width = (width.trim() != "") ? (width + "px") : ""; element.style.maxWidth = "100%"; } }); // replace obsidian's pdf embeds with normal embeds // this has to happen before converting canvases because the pdf embeds use canvas elements html.querySelectorAll("span.internal-embed.pdf-embed").forEach((pdf: HTMLElement) => { let embed = document.createElement("embed"); embed.setAttribute("src", pdf.getAttribute("src") ?? ""); embed.style.width = pdf.style.width || '100%'; embed.style.maxWidth = "100%"; embed.style.height = pdf.style.height || '800px'; let container = pdf.parentElement?.parentElement; container?.querySelectorAll("*").forEach((el) => el.remove()); if (container) container.appendChild(embed); }); // remove all MAKE.md elements html.querySelectorAll("div[class^='mk-']").forEach((element: HTMLElement) => { element.remove(); }); // move frontmatter before markdown-preview-sizer let frontmatter = html.querySelector(".frontmatter"); if (frontmatter) { let frontmatterParent = frontmatter.parentElement; let sizer = html.querySelector(".markdown-preview-sizer"); if (sizer) { sizer.before(frontmatter); } frontmatterParent?.remove(); } // add lazy loading to iframe elements html.querySelectorAll("iframe").forEach((element: HTMLIFrameElement) => { element.setAttribute("loading", "lazy"); }); // add collapse icons to lists if they don't already have them var collapsableListItems = Array.from(html.querySelectorAll("li:has(ul), li:has(ol)")); for (const item of collapsableListItems) { let collapseIcon = item.querySelector(".collapse-icon"); if (!collapseIcon) { collapseIcon = item.createDiv({ cls: "list-collapse-indicator collapse-indicator collapse-icon" }); collapseIcon.innerHTML = this.arrowHTML; item.prepend(collapseIcon); } } // if the dynamic table of contents plugin is included on this page // then parse each list item and render markdown for it let tocEls = Array.from(html.querySelectorAll(".block-language-toc.dynamic-toc li > a")); for (const element of tocEls) { let renderEl = document.body.createDiv(); renderSimpleMarkdown(element.textContent ?? "", renderEl); element.textContent = renderEl.textContent; renderEl.remove(); } } export async function beginBatch(options: MarkdownRendererAPIOptions | MarkdownWebpageRendererAPIOptions) { if(batchStarted) return; errorInBatch = false; cancelled = false; batchStarted = true; loadingContainer = undefined; logContainer = undefined; logShowing = false; AssetHandler.exportOptions = options; renderLeaf = TabManager.openNewTab("window", "vertical"); // @ts-ignore let parentFound = await Utils.waitUntil(() => (renderLeaf && renderLeaf.parent) || checkCancelled(), 2000, 1); if (!parentFound) { try { renderLeaf.detach(); } catch (e) { ExportLog.error(e, "Failed to detach render leaf: "); } if (!checkCancelled()) { new Notice("Error: Failed to create leaf for rendering!"); throw new Error("Failed to create leaf for rendering!"); } return; } let obsidianWindow = renderLeaf.view.containerEl.win; // @ts-ignore electronWindow = obsidianWindow.electronWindow as electron.BrowserWindow; if (!electronWindow) { new Notice("Failed to get the render window, please try again."); errorInBatch = false; cancelled = false; batchStarted = false; renderLeaf = undefined; electronWindow = undefined; return; } if (options.displayProgress === false) { let newPosition = {x: 0, y: window.screen.height}; obsidianWindow.moveTo(newPosition.x, newPosition.y); electronWindow.hide(); } else { // hide the leaf so we can render without intruding on the user // @ts-ignore renderLeaf.parent.containerEl.style.height = "0"; // @ts-ignore renderLeaf.parent.parent.containerEl.querySelector(".clickable-icon, .workspace-tab-header-container-inner").style.display = "none"; // @ts-ignore renderLeaf.parent.containerEl.style.maxHeight = "var(--header-height)"; // @ts-ignore renderLeaf.parent.parent.containerEl.classList.remove("mod-vertical"); // @ts-ignore renderLeaf.parent.parent.containerEl.classList.add("mod-horizontal"); let newSize = { width: 800, height: 400 }; obsidianWindow.resizeTo(newSize.width, newSize.height); let newPosition = {x: window.screen.width / 2 - 450, y: window.screen.height - 450 - 75}; obsidianWindow.moveTo(newPosition.x, newPosition.y); } electronWindow.setAlwaysOnTop(true, "floating", 1); electronWindow.webContents.setBackgroundThrottling(false); function windowClosed() { if (cancelled) return; endBatch(); cancelled = true; electronWindow?.off("close", windowClosed); } electronWindow.on("close", windowClosed); createLoadingContainer(); } export function endBatch() { if (!batchStarted) return; if (renderLeaf) { if (!errorInBatch) { ExportLog.log("Closing render window"); renderLeaf.detach(); } else { ExportLog.warning("Error in batch, leaving render window open"); _reportProgress(1, 1, "Completed with errors", "Please see the log for more details.", errorColor); } } electronWindow = undefined; renderLeaf = undefined; batchStarted = false; } function generateLogEl(title: string, message: any, textColor: string, backgroundColor: string): HTMLElement { let logEl = document.createElement("div"); logEl.className = "html-render-log-item"; logEl.style.display = "flex"; logEl.style.flexDirection = "column"; logEl.style.marginBottom = "2px"; logEl.style.fontSize = "12px"; logEl.innerHTML = `

`; logEl.querySelector(".html-render-log-title")!.textContent = title; logEl.querySelector(".html-render-log-message")!.textContent = message.toString(); logEl.style.color = textColor; logEl.style.backgroundColor = backgroundColor; logEl.style.borderLeft = `5px solid ${textColor}`; logEl.style.borderBottom = "1px solid var(--divider-color)"; logEl.style.borderTop = "1px solid var(--divider-color)"; return logEl; } function createLoadingContainer() { if (!loadingContainer) { loadingContainer = document.createElement("div"); loadingContainer.className = `html-render-progress-container`; loadingContainer.setAttribute("style", "height: 100%; min-width: 100%; display:flex; flex-direction:column; align-content: center; justify-content: center; align-items: center;"); loadingContainer.innerHTML = `

Generating HTML

` // @ts-ignore renderLeaf.parent.parent.containerEl.appendChild(loadingContainer); } } let logShowing = false; function appendLogEl(logEl: HTMLElement) { logContainer = loadingContainer?.querySelector(".html-render-log") ?? undefined; if(!logContainer || !renderLeaf) { console.error("Failed to append log element, log container or render leaf is undefined!"); return; } if (!logShowing) { renderLeaf.view.containerEl.win.resizeTo(900, 500); logContainer.style.display = "flex"; logShowing = true; } logContainer.appendChild(logEl); // @ts-ignore logEl.scrollIntoView({ behavior: "instant", block: "end", inline: "end" }); } export async function _reportProgress(complete: number, total:number, message: string, subMessage: string, progressColor: string) { if (!batchStarted) return; // @ts-ignore if (!renderLeaf || !renderLeaf.parent || !renderLeaf.parent.parent) return; // @ts-ignore let loadingContainer = renderLeaf.parent.parent.containerEl.querySelector(`.html-render-progress-container`); if (!loadingContainer) return; let progress = complete / total; let progressBar = loadingContainer.querySelector("progress"); if (progressBar) { progressBar.value = progress; progressBar.style.backgroundColor = "transparent"; progressBar.style.color = progressColor; } let messageElement = loadingContainer.querySelector("h1"); if (messageElement) { messageElement.innerText = message; } let subMessageElement = loadingContainer.querySelector("span.html-render-submessage") as HTMLElement; if (subMessageElement) { subMessageElement.innerText = subMessage; } electronWindow?.setProgressBar(progress); } export async function _reportError(messageTitle: string, message: any, fatal: boolean) { if (!batchStarted) return; errorInBatch = true; // @ts-ignore let found = await Utils.waitUntil(() => renderLeaf && renderLeaf.parent && renderLeaf.parent.parent, 100, 10); if (!found) return; appendLogEl(generateLogEl(messageTitle, message, errorColor, errorBoxColor)); if (fatal) { renderLeaf = undefined; loadingContainer = undefined; logContainer = undefined; } } export async function _reportWarning(messageTitle: string, message: any) { if (!batchStarted) return; // @ts-ignore let found = await Utils.waitUntil(() => renderLeaf && renderLeaf.parent && renderLeaf.parent.parent, 100, 10); if (!found) return; appendLogEl(generateLogEl(messageTitle, message, warningColor, warningBoxColor)); } export async function _reportInfo(messageTitle: string, message: any) { if (!batchStarted) return; // @ts-ignore let found = await Utils.waitUntil(() => renderLeaf && renderLeaf.parent && renderLeaf.parent.parent, 100, 10); if (!found) return; appendLogEl(generateLogEl(messageTitle, message, infoColor, infoBoxColor)); } }