Files
WorkNote/.obsidian/plugins/obsidian-webpage-export-master/scripts/render-api.ts
2025-04-10 14:07:13 +08:00

1079 lines
33 KiB
TypeScript

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<string | undefined>
{
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<HTMLElement | undefined>
{
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<string | undefined>
{
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<Webpage | undefined>
{
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<string | undefined>
{
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 = "<svg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2' stroke-linecap='round' stroke-linejoin='round' class='svg-icon right-triangle'><path d='M3 8L12 17L21 8'></path></svg>";
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<HTMLElement | 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(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<HTMLElement | undefined>
{
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<any>[] = [];
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 <p> 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<HTMLElement | undefined>
{
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<HTMLElement | undefined>
{
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<HTMLElement | undefined>
{
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 =
`
<div class="html-render-log-title" style="font-weight: bold; margin-left: 1em;"></div>
<div class="html-render-log-message" style="margin-left: 2em; font-size: 0.8em;white-space: pre-wrap;"></div>
`;
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 =
`
<div class="html-render-progress-container" style="height: 100%;min-width: 100%;display:flex;flex-direction:column;">
<div style="display: flex;height: 100%;">
<div style="flex-grow: 1;display: flex;flex-direction: column;align-items: center;justify-content: center;">
<h1 style="">Generating HTML</h1>
<progress class="html-render-progressbar" value="0" min="0" max="1" style="width: 300px; height: 15px; background-color: transparent; color: var(--interactive-accent);"></progress>
<span class="html-render-submessage" style="margin-block-start: 2em;"></span>
</div>
<div class="html-render-log" style="display:none; flex-direction: column; border-left: 1px solid var(--divider-color); overflow-y: auto; width: 300px; max-width: 300px; min-width: 300px;">
<h1 style="color: var(--color-yellow);padding: 0.3em;background-color: rgba(100, 70, 20, 0.1);margin: 0;">Export Log</h1>
</div>
</div>
</div>
`
// @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));
}
}