diff --git a/.editorconfig b/.editorconfig index 84b8a66..4827f47 100644 --- a/.editorconfig +++ b/.editorconfig @@ -7,4 +7,4 @@ end_of_line = lf insert_final_newline = true indent_style = tab indent_size = 4 -tab_width = 4 +tab_width = 4 \ No newline at end of file diff --git a/obsidian-styles.css b/assets/obsidian-styles.css similarity index 100% rename from obsidian-styles.css rename to assets/obsidian-styles.css diff --git a/plugin-styles.css b/assets/plugin-styles.css similarity index 100% rename from plugin-styles.css rename to assets/plugin-styles.css diff --git a/support-image.png b/assets/support-image.png similarity index 100% rename from support-image.png rename to assets/support-image.png diff --git a/webpage.js b/assets/webpage.js similarity index 100% rename from webpage.js rename to assets/webpage.js diff --git a/esbuild.config.mjs b/esbuild.config.mjs index 8e2dad0..b640e65 100644 --- a/esbuild.config.mjs +++ b/esbuild.config.mjs @@ -15,7 +15,7 @@ esbuild.build({ banner: { js: banner, }, - entryPoints: ['main.ts'], + entryPoints: ['./scripts/main.ts'], bundle: true, external: [ 'obsidian', diff --git a/main.ts b/main.ts deleted file mode 100644 index 727fe72..0000000 --- a/main.ts +++ /dev/null @@ -1,1243 +0,0 @@ -import { createWriteStream, open, readdirSync, readFile, write, writeFile, WriteFileOptions, existsSync, mkdirSync } from 'fs'; -var JSZip = require("jszip"); -import { MarkdownView, Plugin, TAbstractFile, TFile, PaneType, OpenViewState, SplitDirection, FileSystemAdapter, WorkspaceLeaf, Notice, View, FileView, MarkdownEditView, TextFileView, TFolder } from 'obsidian'; -import { ExportSettings } from './settings'; -import { saveAs, FileSaverOptions } from 'file-saver'; -import { NewWindowEvent } from 'electron'; - -import jQuery from 'jquery'; -const $ = jQuery; - -/* @ts-ignore */ -const dialog: Electron.Dialog = require('electron').remote.dialog; -declare const window: any; - -export default class HTMLExportPlugin extends Plugin { - - pluginPath : string = Utils.getVaultPath() + "/.obsidian/plugins/obsidian-webpage-export"; - configPath : string = Utils.getVaultPath() + "/.obsidian"; - leafHandler : LeafHandler = new LeafHandler(); - - imagesToDownload : {original_path: string, destination_path_rel: string}[] = []; - - appStyles :string = ""; - mathStylesLoaded : boolean = false; - - - autoDownloadExtras = true; - - webpagejsURL : string = "https://raw.githubusercontent.com/KosmosisDire/obsidian-webpage-export/master/webpage.js"; - pluginStylesURL : string = "https://raw.githubusercontent.com/KosmosisDire/obsidian-webpage-export/master/plugin-styles.css"; - obsidianStylesURL : string = "https://raw.githubusercontent.com/KosmosisDire/obsidian-webpage-export/master/obsidian-styles.css"; - async downloadExtras() - { - //Download webpage.js - let webpagejs = await fetch(this.webpagejsURL); - let webpagejsText = await webpagejs.text(); - await writeFile(this.pluginPath + "/webpage.js", webpagejsText, function(err) { - if(err) { - return console.log(err); - } - console.log("webpage.js file downloaded successfully"); - }); - - //Download plugin-styles.css - let pluginStyles = await fetch(this.pluginStylesURL); - let pluginStylesText = await pluginStyles.text(); - await writeFile(this.pluginPath + "/plugin-styles.css", pluginStylesText, function(err) { - if(err) { - return console.log(err); - } - console.log("plugin-styles.css file downloaded successfully"); - }); - - //Download obsidian-styles.css - let obsidianStyles = await fetch(this.obsidianStylesURL); - let obsidianStylesText = await obsidianStyles.text(); - await writeFile(this.pluginPath + "/obsidian-styles.css", obsidianStylesText, function(err) { - if(err) { - return console.log(err); - } - console.log("obsidian-styles.css file downloaded successfully"); - }); - - } - - - darkModeToggle = - `\n\n -
- -
- \n\n` - - addTogglePostprocessor() - { - this.registerMarkdownCodeBlockProcessor("theme-toggle", (source, el, ctx) => - { - let parent = el.createEl('div'); - parent.innerHTML = this.darkModeToggle; - }); - - //also replace `theme-toggle` and ```theme-toggle``` for better inline toggles, or in places you couldn't use a normal code block - this.registerMarkdownPostProcessor((element, context) => - { - let codeBlocks = element.querySelectorAll('code, span.cm-inline-code'); - codeBlocks.forEach((codeBlock) => - { - // console.log(codeBlock); - if (codeBlock instanceof HTMLElement && codeBlock.innerText == "theme-toggle") - { - codeBlock.outerHTML = this.darkModeToggle; - } - }); - }); - } - - async onload() - { - console.log('loading obsidian-webpage-export plugin'); - - await Utils.delay(1000); - - this.appStyles = ""; - var appSheet = document.styleSheets[1]; - let stylesheets = document.styleSheets; - for (let i = 0; i < stylesheets.length; i++) - { - if (stylesheets[i].href && stylesheets[i].href?.includes("app.css")) - { - appSheet = stylesheets[i]; - break; - } - } - - this.appStyles += await Utils.getText(this.pluginPath + "/obsidian-styles.css"); - - for (var i = 0; i < appSheet.cssRules.length; i++) - { - var rule = appSheet.cssRules[i]; - if (rule) - { - if (rule.cssText.startsWith("@font-face")) continue; - if (rule.cssText.startsWith(".CodeMirror")) continue; - if (rule.cssText.startsWith(".cm-")) continue; - - this.appStyles += rule.cssText + "\n"; - } - } - - // await writeFile(this.pluginPath + "/app-styles.css", this.appStyles, function(err) { - // if(err) { - // return console.log(err); - // } - // console.log("app-styles.css file downloaded successfully"); - // }); - - - console.log("loaded app styles"); - - if(this.autoDownloadExtras) - await this.downloadExtras(); - - - new ExportSettings(this); - ExportSettings.loadSettings(); - - this.registerEvent( - this.app.workspace.on("file-menu", (menu, file, source) => { - menu.addItem((item) => {item - .setTitle("Export to HTML") - .setIcon("document") - .onClick(async () => - { - if(Utils.getFileNameFromFilePath(file.path).contains(".")) - this.exportFile(file); - - else - this.exportFolder(file.path); - }); - }); - }) - ); - - this.addRibbonIcon("folder-up", "Export Vault to HTML", async () => - { - this.exportFolder(""); - }); - - this.addTogglePostprocessor(); - } - - onunload() - { - console.log('unloading obsidian-webpage-export plugin'); - } - - async exportFile(file: TAbstractFile, fullPath: string = "", showSettings: boolean = true) - { - this.leafHandler.switchToLeafWithFile(file as TFile, true); - - // Open the settings modal and wait until it's closed - if (showSettings) - { - var exportCanceled = !await new ExportSettings(this).open(); - if (exportCanceled) return; - } - - var html = await this.GetCurrentFileHTML(); - if (!html) return; - - var toDownload = []; - - if(!ExportSettings.settings.inlineCSS) - { - let appcss = this.appStyles; - let plugincss = await Utils.getText(this.pluginPath + "/plugin-styles.css"); - let themecss = await Utils.getThemeContent(Utils.getCurrentTheme()); - - let snippetsList = await Utils.getStyleSnippetsContent(); - let snippetsNames = await Utils.getEnabledSnippets(); - var snippets = ""; - - for (var i = 0; i < snippetsList.length; i++) - { - snippets += `/* --- ${snippetsNames[i]}.css --- */ \n ${snippetsList[i]} \n\n\n`; - } - - // load 3rd party plugin css - let thirdPartyPluginStyleNames = ExportSettings.settings.includePluginCSS.split("\n"); - for (let i = 0; i < thirdPartyPluginStyleNames.length; i++) - { - if (!thirdPartyPluginStyleNames[i] || (thirdPartyPluginStyleNames[i] && !(/\S/.test(thirdPartyPluginStyleNames[i])))) continue; - - let path = this.pluginPath.replace("obsidian-webpage-export", thirdPartyPluginStyleNames[i].replace("\n", "")) + "/styles.css"; - let style = await Utils.getText(path); - if (style) plugincss += "\n" + style + "\n"; - } - - let appcssDownload = { filename: "obsidian-styles.css", data: appcss, type: "text/css" }; - let plugincssDownload = { filename: "plugin-styles.css", data: plugincss, type: "text/css" }; - let themecssDownload = { filename: "theme.css", data: themecss, type: "text/css" }; - let snippetsDownload = { filename: "snippets.css", data: snippets, type: "text/css" }; - - toDownload.push(appcssDownload); - toDownload.push(plugincssDownload); - toDownload.push(themecssDownload); - toDownload.push(snippetsDownload); - } - - if(!ExportSettings.settings.inlineJS) - { - var webpagejs = await Utils.getText(this.pluginPath + "/webpage.js"); - var webpagejsDownload = { filename: "webpage.js", data: webpagejs, type: "text/javascript" }; - toDownload.push(webpagejsDownload); - } - - // let imagesDownload : {path: string, data: string}[] = []; - - if(!ExportSettings.settings.inlineImages) - { - for (var i = 0; i < this.imagesToDownload.length; i++) - { - var image = this.imagesToDownload[i]; - var data = await Utils.getTextBase64(image.original_path); - var imageDownload = - { - filename: Utils.getFileNameFromFilePath(image.destination_path_rel), - data: data, - type: "image/png", - relativePath: Utils.getDirectoryFromFilePath(image.destination_path_rel), - unicode: false - }; - toDownload.push(imageDownload); - } - } - - var htmlDownload = { filename: file.name.replace(".md", ".html"), data: html, type: "text/html" }; - toDownload.push(htmlDownload); - - // Download file - var htmlPath : string | null = fullPath; - if (htmlPath == "") - htmlPath = await Utils.showSaveDialog(Utils.idealDefaultPath(), file.name.replace(".md", ".html"), false); - - if (!htmlPath) return; - - let filename = Utils.getFileNameFromFilePath(htmlPath); - let folderPath = Utils.getDirectoryFromFilePath(htmlPath); - - toDownload[toDownload.length - 1].filename = filename; - - Utils.downloadFiles(toDownload, folderPath); - } - - async exportFolder(folderPath : string) - { - // folder path is the path relative to the vault that we are exporting - - // Open the settings modal and wait until it's closed - var exportCanceled = !await new ExportSettings(this).open(); - if (exportCanceled) return; - - let htmlPath = await Utils.showSelectFolderDialog(Utils.idealDefaultPath()); - - var files = this.app.vault.getFiles(); - - for (var i = 0; i < files.length; i++) - { - var file = files[i]; - if(file.path.startsWith(folderPath) && file.extension == "md") - { - var fullPath = htmlPath + "/" + file.path.replace(".md", ".html"); - await this.exportFile(file, fullPath, false); - } - } - } - - //#region General HTML - - async GetCurrentFileHTML(): Promise - { - await Utils.delay(200); - - let view = await Utils.getActiveView(); - if (!view) return null; - - Utils.setLineWidth(ExportSettings.settings.customLineWidth); - - if (view instanceof MarkdownView) - await Utils.viewEnableFullRender(view); - - var head = await this.generateHead(view); - var html = this.generateBodyHTML(); - html = this.fixLinks(html); - html = this.repairOnClick(html); //replace data-onlick with onclick - - if (ExportSettings.settings.inlineImages) - html = await this.inlineImages(html); - else - html = await this.outlineImages(html, view); - - // inject darkmode toggle - if (ExportSettings.settings.addDarkModeToggle) - { - html = await this.injectToggle(html); - } - - if (ExportSettings.settings.includeOutline) - { - var headers = this.getHeaderList(html); - if (headers) - { - var outline = this.generateOutline(headers); - // put side bars on either side of content and put them in a flex container - let el = document.createElement("html"); - el.innerHTML = html; - let body = el.querySelector("body"); - if (body) - { - html = `
${body.innerHTML}
`; - body.innerHTML = html; - html = body?.outerHTML; - } - else - { - console.error("Could not find body element in html"); - } - - el.remove(); - } - } - - html = head + html; - - // enclose in tags - html = "\n\n" + html + "\n"; - - return html; - } - - generateBodyHTML() : string - { - var bodyClasses = document.body.getAttribute("class") ?? ""; - var bodyStyle = document.body.getAttribute("style") ?? ""; - /*@ts-ignore*/ - bodyClasses = bodyClasses.replaceAll("\"", "'"); - /*@ts-ignore*/ - bodyStyle = bodyStyle.replaceAll("\"", "'"); - - var htmlEl = (document.querySelector(".workspace-leaf.mod-active .markdown-reading-view") as HTMLElement) - if (!htmlEl) htmlEl = (document.querySelector(".workspace-leaf.mod-active .view-content") as HTMLElement); - - htmlEl.style.flexBasis = (htmlEl.querySelector("markdown-preview-sizer markdown-preview-section") as HTMLElement)?.style.width ?? "1000px"; - var html = htmlEl.outerHTML; - - html = "\n\n" + html + "\n\n"; - - return html; - } - - async generateHead(view: TextFileView) : Promise - { - let pluginStyles = await Utils.getText(this.pluginPath +"/plugin-styles.css"); - let cssSettings = document.getElementById("css-settings-manager")?.innerHTML ?? ""; - let snippets = await Utils.getStyleSnippetsContent(); - let snippetNames = Utils.getEnabledSnippets(); - let theme = await Utils.getThemeContent(Utils.getCurrentTheme()); - - let scripts = "\n\n" - + ((ExportSettings.settings.inlineJS ? ("\n") + "\n\n"); - - var height = 0; - // @ts-ignore - let sections = (view.currentMode?.renderer?.sections) ?? []; - for (let i = 0; i < sections.length; i++) - { - height += sections[i].height; - } - - let sizer = document.getElementsByClassName("markdown-preview-sizer markdown-preview-section"); - var width = "1000px" - if (sizer) width = (sizer[0] as HTMLElement)?.style?.maxWidth?.replace("px", ""); - - if (!this.mathStylesLoaded) - { - let mathStyles = document.styleSheets[document.styleSheets.length-1]; - var mathStylesString = ""; - - var success = true; - for (var i = 0; i < mathStyles.cssRules.length; i++) - { - var rule = mathStyles.cssRules[i]; - - if (rule) - { - if (i == 0 && !rule.cssText.startsWith("mjx")) - { - success = false; - break; - } - - if (rule.cssText.startsWith("@font-face")) continue; - - mathStylesString += rule.cssText + "\n"; - } - } - - if (success) - { - console.log("Loaded math styles"); - console.log(mathStylesString); - this.appStyles += mathStylesString; - this.mathStylesLoaded = true; - } - } - - let thirdPartyPluginStyleNames = ExportSettings.settings.includePluginCSS.split("\n"); - for (let i = 0; i < thirdPartyPluginStyleNames.length; i++) - { - if (!thirdPartyPluginStyleNames[i] || (thirdPartyPluginStyleNames[i] && !(/\S/.test(thirdPartyPluginStyleNames[i])))) continue; - - let path = this.pluginPath.replace("obsidian-webpage-export", thirdPartyPluginStyleNames[i].replace("\n", "")) + "/styles.css"; - let style = await Utils.getText(path); - if (style) pluginStyles += "\n" + style + "\n"; - } - - - let meta = - ` - - - - - ${view.file.basename} - - - - ` - - - - if (ExportSettings.settings.inlineCSS) - { - var header = - ` - - - ${meta} - - - - - - - - - - - - - - - ${scripts} - - - `; - } - else - { - header = - ` - - - ${meta} - - - - - - - - - ${scripts} - - - `; - } - - return header; - } - - //#endregion - - //#region Links and Images - - fixLinks(html: string): string - { - let el = document.createElement('html'); - el.innerHTML = html; - - let query = jQuery(el); - query.find("a.internal-link").each(function () - { - $(this).attr("target", "_self"); - - let finalHref = ""; - let href = $(this).attr("href")?.split("#"); - - if(!href) return; - - // if the file doesn't start with #, then it links to a file, or a header in another file. - if(!(href[0] == "")) - { - if(href.length == 1) - { - finalHref = href[0] + ".html"; - } - - if(href.length == 2) - { - var filePath = ""; - if(!href[0].contains("/") && !href[0].contains("\\")) - { - filePath = Utils.getDirectoryFromFilePath(Utils.getFirstFileByName(href[0])?.path ?? "") + "/"; - } - - finalHref = filePath + href[0] + ".html#" + href[1].replaceAll(" ", "_").replaceAll("#", "").replaceAll("__", "_"); - } - - if(href.length > 2) - { - let first = href.shift() ?? ""; - - var filePath = ""; - if(!first.contains("/") && !first.contains("\\")) - { - filePath = Utils.getDirectoryFromFilePath(Utils.getFirstFileByName(first)?.path ?? "") + "/"; - } - - finalHref = filePath + first + ".html#" + href.join("#").replaceAll(" ", "_").replaceAll("#", "").replaceAll("__", "_"); - } - } - else // if the file starts with #, then it links to an internal header. - { - href.shift(); - if(href.length == 1) - { - finalHref = "#"+href[0].replaceAll(" ", "_").replaceAll("#", "").replaceAll("__", "_"); - } - - if(href.length > 1) - { - finalHref = href.join("#").replaceAll(" ", "_").replaceAll("#", "").replaceAll("__", "_"); - } - } - - $(this).attr("href", finalHref); - }); - - query.find("h1, h2, h3, h4, h5, h6").each(function () - { - // use the headers inner text as the id - $(this).attr("id", $(this).text().replaceAll(" ", "_").replaceAll("#", "").replaceAll("__", "_")); - }); - - let result = el.innerHTML; - el.remove(); - return result; - } - - async inlineImages(html: string): Promise - { - let el = document.createElement('html'); - el.innerHTML = html; - - let query = jQuery(el); - let images = query.find("img").toArray(); - - for (let i = 0; i < images.length; i++) - { - let img = images[i]; - if ($(img).attr("src")?.startsWith("app://local/")) - { - let path = $(img).attr("src")?.replace("app://local/", "").replaceAll("%20", " ").split("?")[0]; - - if (path) - { - var base64 = ""; - try - { - base64 = await Utils.getTextBase64(path); - } - catch (e) - { - console.error(e); - console.warn("Failed to inline image: " + path); - new Notice("Failed to inline image: " + path, 5000); - continue; - } - - $(img).attr("src", "data:image/png;base64," + base64); - } - } - } - - let result = el.innerHTML; - el.remove(); - return result; - } - - async outlineImages(html: string, view: TextFileView): Promise - { - let el = document.createElement('html'); - el.innerHTML = html; - - let query = jQuery(el); - - this.imagesToDownload = []; - - let img2Download : {original_path: string, destination_path_rel: string}[] = []; - let vaultPath = Utils.getVaultPath(); - - query.find("img").each(function () - { - if (!$(this).attr("src")?.startsWith("app://local/")) return; - - let originalPath = $(this).attr("src")?.replaceAll("\\", "/").replaceAll("%20", " ").replace("app://local/", ""); - - // console.log("originalPath: " + originalPath); - - if (!originalPath) - { - new Notice("Failed to outline image: " + originalPath + ". Couldn't find image src", 5000); - return; - } - - if (originalPath.startsWith("data:image/png;base64,") || originalPath.startsWith("data:image/jpeg;base64,")) return; - - let relPath = originalPath.split(Utils.getDirectoryFromFilePath(vaultPath + "/" + view.file.path.replaceAll("\\", "/")) + "/")[1]; - - // console.log(originalPath, "vs.", Utils.getDirectoryFromFilePath(vaultPath + "/" + view.file.path.replaceAll("\\", "/")) + "/"); - - if (!relPath) - relPath = ("images/" + Utils.getFileNameFromFilePath($(this).attr("src")?.replaceAll("\\", "/").replaceAll("%20", " ") ?? "")) ?? "img.png"; - - // console.log("relPath: " + relPath); - - $(this).attr("src", relPath); - - img2Download.push({original_path: originalPath, destination_path_rel: relPath}); - - }); - - this.imagesToDownload = img2Download; - - let result = el.innerHTML; - el.remove(); - return result; - } - - //#endregion - - //#region Special Features - - async injectToggle(html: string) : Promise - { - if (!html.contains(this.darkModeToggle.split("\n")[4])) - { - //insert fixed toggle in corner - console.log("Injecting toggle"); - html = this.darkModeToggle.replace("theme-toggle-inline", "theme-toggle") + html; - } - - return html; - } - - repairOnClick(html: string) : string - { - html = html.replaceAll("data-onclick", "onclick"); - return html; - } - - getHeaderList(html: string) : {size: number, title: string, href: string}[] | null - { - var headers = []; - - var el = document.createElement( 'html' ); - el.innerHTML = html; - - var headerElements = el.querySelectorAll("h1, h2, h3, h4, h5, h6"); - - for (var i = 0; i < headerElements.length; i++) - { - var header = headerElements[i]; - var size = parseInt(header.tagName[1]); - var title = (header as HTMLElement).innerText; - var href = (header as HTMLHeadingElement).id; - headers.push({size, title, href}); - } - - el.remove(); - - return headers; - } - - generateOutline(headers: {size: number, title: string, href:string}[]) : string - { - var outline = - ` -
- -
- -
Table of Contents
-
- -
- `; - - var builderRoot = document.createElement( 'html' ); - builderRoot.innerHTML = outline; - - var outlineEl = builderRoot.querySelector(".outline-container"); - - if (!outlineEl) return ""; - - var listItemTemplate = - ` - -
- -
-
- - -
- - {title} -
- -
- -
-
- `; - - - var listStack = [outlineEl]; - - // function to get the data-size of the previous list item as a number - function getLastStackSize() : number - { - return parseInt(listStack[listStack.length - 1].getAttribute("data-size") ?? "0"); - } - - - for (var i = 0; i < headers.length; i++) - { - var header = headers[i]; - var listItem = listItemTemplate.replace("{size}", header.size.toString()).replace("{href}", header.href).replace("{title}", header.title); - - while (getLastStackSize() >= header.size && listStack.length > 1) - { - listStack.pop(); - } - - var builditemRoot = document.createElement( 'div' ); - builditemRoot.innerHTML = listItem; - var newOutlineItem = builditemRoot.querySelector(".outline-item"); - - if (!newOutlineItem) continue; - - var childContainer = listStack.last()?.querySelector(".outline-item-children"); - if (getLastStackSize() == 0) childContainer = listStack.last(); - - if (!childContainer) continue; - - childContainer.appendChild(newOutlineItem); - listStack.push(newOutlineItem); - - builditemRoot.remove(); - } - - var result = builderRoot.innerHTML; - builderRoot.remove(); - - return result; - } - - //#endregion - - -} - -export class Utils -{ - static async delay (ms: number) - { - return new Promise( resolve => setTimeout(resolve, ms) ); - } - - - static async getText(path: string): Promise - { - return new Promise((resolve, reject) => { - open(path, 'r', (err, fd) => { - if (err) { - reject(err); - } else { - readFile(fd, { encoding: 'utf8' }, (err, data) => { - if (err) { - reject(err); - } else { - resolve(data); - } - }); - } - }); - }); - } - - static async getTextBase64(path: string): Promise - { - return new Promise((resolve, reject) => { - open(path, 'r', (err, fd) => { - if (err) { - reject(err); - } else { - readFile(fd, { encoding: 'base64' }, (err, data) => { - if (err) { - reject(err); - } else { - resolve(data); - } - }); - } - }); - }); - } - - static changeViewMode(view: MarkdownView, modeName: "preview" | "source") - { - /*@ts-ignore*/ - const mode = view.modes[modeName]; - /*@ts-ignore*/ - mode && view.setMode(mode); - }; - - static createUnicodeArray(content: string) : Uint8Array - { - var charCode, byteArray = []; - - // BE BOM - byteArray.push(254, 255); - - // LE BOM - // byteArray.push(255, 254); - - for (var i = 0; i < content.length; ++i) { - - charCode = content.charCodeAt(i); - - // BE Bytes - byteArray.push((charCode & 0xFF00) >>> 8); - byteArray.push(charCode & 0xFF); - - // LE Bytes - // byteArray.push(charCode & 0xff); - // byteArray.push(charCode / 256 >>> 0); - } - - return new Uint8Array(byteArray); - } - - static async showSaveDialog(defaultPath: string, defaultFileName: string, showAllFilesOption: boolean = true): Promise - { - let type = (defaultFileName.split(".").pop() ?? "txt"); - - var filters = [{ - name: type.toUpperCase() + " Files", - extensions: [type] - }]; - - if (showAllFilesOption) - { - filters.push({ - name: "All Files", - extensions: ["*"] - }); - } - - let picker = await dialog.showSaveDialog({ - defaultPath: (defaultPath + "/" + defaultFileName).replaceAll("\\", "/").replaceAll("//", "/"), - filters: filters, - properties: ["showOverwriteConfirmation"] - }) - - if (picker.canceled) return null; - - let path = picker.filePath ?? ""; - - if (path != "") - { - ExportSettings.settings.lastExportPath = path; - ExportSettings.saveSettings(); - } - - return path; - } - - static async showSelectFolderDialog(defaultPath: string): Promise - { - let picker = await dialog.showOpenDialog({ - defaultPath: defaultPath, - properties: ["openDirectory"] - }); - - if (picker.canceled) return null; - - let path = picker.filePaths[0] ?? ""; - - if (path != "") - { - ExportSettings.settings.lastExportPath = path; - ExportSettings.saveSettings(); - } - - return path; - } - - static idealDefaultPath() : string - { - return ExportSettings.settings.lastExportPath == "" ? (Utils.getVaultPath() ?? "") : ExportSettings.settings.lastExportPath; - } - - static async downloadFile(data: string, filename: string, path: string = "") - { - if (path == "") - { - path = await Utils.showSaveDialog(Utils.idealDefaultPath(), filename) ?? ""; - - if (path == "") return; - } - - var array = Utils.createUnicodeArray(data); - - writeFile(path, array, (err) => { - if (err) throw err; - console.log('The file has been saved!'); - }); - } - - static async downloadFilesAsZip(files: {filename: string, data: string, type: string, relativePath?: string}[], zipFileName: string) - { - var blobs = files.map(file => new Blob([file.data], {type: file.type})); - var zip = new JSZip(); - for (var i = 0; i < files.length; i++) - { - let path = ((files[i].relativePath ?? "") + "/" + files[i].filename).replaceAll("//", "/"); - zip.file(path, blobs[i]); - } - - var zipBlob = await zip.generateAsync({type: "uint8array"}); - - var path = await Utils.showSaveDialog(Utils.idealDefaultPath(), zipFileName, false) ?? ""; - - if (path == "") return; - - writeFile(path, zipBlob, (err) => { - if (err) throw err; - console.log('The file has been saved!'); - }); - } - - static async downloadFiles(files: {filename: string, data: string, type?: string, relativePath?: string, unicode?: boolean}[], folderPath: string) - { - for (var i = 0; i < files.length; i++) - { - var array = (files[i].unicode ?? true) ? Utils.createUnicodeArray(files[i].data) : Buffer.from(files[i].data, 'base64'); - - let path = (folderPath + "/" + (files[i].relativePath ?? "") + "/" + files[i].filename).replaceAll("\\", "/").replaceAll("//", "/").replaceAll("//", "/"); - - let dir = Utils.getDirectoryFromFilePath(path); - if (!existsSync(dir)) - { - mkdirSync(dir, { recursive: true }); - } - - writeFile(path, array, (err) => { - if (err) throw err; - console.log('The file has been saved!'); - }); - } - } - - static getDirectoryFromFilePath(path: string): string - { - var forwardIndex = path.lastIndexOf("/"); - var backwardIndex = path.lastIndexOf("\\"); - - var index = forwardIndex > backwardIndex ? forwardIndex : backwardIndex; - - if (index == -1) return ""; - - return path.substring(0, index); - } - - static getFileNameFromFilePath(path: string): string - { - var forwardIndex = path.lastIndexOf("/"); - var backwardIndex = path.lastIndexOf("\\"); - - var index = forwardIndex > backwardIndex ? forwardIndex : backwardIndex; - - if (index == -1) return path; - - return path.substring(index + 1); - } - - static getVaultPath(): string | null - { - let adapter = app.vault.adapter; - if (adapter instanceof FileSystemAdapter) { - return adapter.getBasePath().replaceAll("\\", "/"); - } - - return null; - } - - //async function that awaits until a condition is met - static async waitUntil(condition: () => boolean, timeout: number = 1000, interval: number = 100): Promise - { - return new Promise((resolve, reject) => { - let timer = 0; - let intervalId = setInterval(() => { - if (condition()) { - clearInterval(intervalId); - resolve(); - } else { - timer += interval; - if (timer >= timeout) { - clearInterval(intervalId); - reject(); - } - } - }, interval); - }); - } - - static async getThemeContent(themeName: string): Promise - { - let themePath = this.getVaultPath() + "/.obsidian/themes/" + themeName + "/theme.css"; - let themeContent = await Utils.getText(themePath); - return themeContent; - } - - static getCurrentTheme(): string - { - /*@ts-ignore*/ // config does exist - return app.vault.config?.cssTheme ?? "Default"; - } - - static getEnabledSnippets(): string[] - { - /*@ts-ignore*/ - return app.vault.config?.enabledCssSnippets ?? []; - } - - static async getStyleSnippetsContent(): Promise - { - let snippetContents : string[] = []; - let enabledSnippets = this.getEnabledSnippets(); - - for (var i = 0; i < enabledSnippets.length; i++) - { - snippetContents.push(await Utils.getText(Utils.getVaultPath() + "/.obsidian/snippets/" + enabledSnippets[i] + ".css")); - } - - return snippetContents; - } - - static async viewEnableFullRender(view: MarkdownView) - { - Utils.changeViewMode(view, "preview"); - await this.delay(200); - /*@ts-ignore*/ - view.previewMode.renderer.showAll = true; - /*@ts-ignore*/ - await view.previewMode.renderer.unfoldAllHeadings(); - await Utils.delay(300); - /*@ts-ignore*/ - await view.previewMode.renderer.rerender(); - } - - static async getActiveView(): Promise - { - let view = app.workspace.getActiveViewOfType(TextFileView); - if (!view) - { - console.log("Failed to find active view"); - return null; - } - - return view; - } - - static getFirstFileByName(name: string): TFile | undefined - { - return app.vault.getFiles().find(file => - { - if(!name) return false; - return file.basename == name; - }); - } - - static setLineWidth(width: number) : void - { - if (width != 0) - { - let sizers = document.getElementsByClassName("markdown-preview-sizer markdown-preview-section"); - if (sizers.length > 0) - sizers[0].setAttribute("style", "max-width: " + width + "px"); - } - } -} - -export class LeafHandler -{ - // from obsidian-switcher-plus by darlal: https://github.com/darlal/obsidian-switcher-plus/blob/27d337039883008bcbf40ca13ea2f9287469dde4/src/Handlers/handler.ts#L388 - // only some functions are used and have been packaged into this class for easy use. - - isMainPanelLeaf(leaf: WorkspaceLeaf): boolean - { - const { workspace } = app; - const root = leaf?.getRoot(); - /*@ts-ignore*/ - return root === workspace.rootSplit || root === workspace.floatingSplit; - } - - getOpenLeaves(excludeMainPanelViewTypes?: string[], includeSidePanelViewTypes?: string[]): WorkspaceLeaf[] - { - const leaves: WorkspaceLeaf[] = []; - - const saveLeaf = (l: WorkspaceLeaf) => { - const viewType = l.view?.getViewType(); - - if (this.isMainPanelLeaf(l)) { - if (!excludeMainPanelViewTypes?.includes(viewType)) { - leaves.push(l); - } - } else if (includeSidePanelViewTypes?.includes(viewType)) { - leaves.push(l); - } - }; - - app.workspace.iterateAllLeaves(saveLeaf); - return leaves; - } - - openFileInNewLeaf( - file: TFile, - navType: PaneType | boolean, - openState?: OpenViewState, - errorContext?: string, - splitDirection: SplitDirection = 'vertical', - ): void - { - const { workspace } = app; - errorContext = errorContext ?? ''; - const message = `Switcher++: error opening file. ${errorContext}`; - - const getLeaf = () => { - return navType === 'split' - ? workspace.getLeaf(navType, splitDirection) - : workspace.getLeaf(navType); - }; - - try { - getLeaf() - .openFile(file, openState) - .catch((reason) => { - console.log(message, reason); - }); - } catch (error) { - console.log(message, error); - } - } - - getLeafByFile(file: TFile): WorkspaceLeaf | null - { - const leaves = this.getOpenLeaves(); - for (let leaf of leaves) { - if (leaf.view instanceof MarkdownView) - { - if (leaf.view.file.path === file.path) { - return leaf; - } - } - } - - return null; - } - - switchToLeafWithFile(file: TFile, openNewIfNotOpen: boolean): void - { - const { workspace } = app; - const leaf = this.getLeafByFile(file); - - if (leaf) - { - workspace.setActiveLeaf(leaf); - } - else if (openNewIfNotOpen) - { - this.openFileInNewLeaf(file, true, { active: true }, "Failed to open file to new tab after it was found to not be open yet."); - } - } -} - diff --git a/package.json b/package.json index 3b796bc..be190ee 100644 --- a/package.json +++ b/package.json @@ -26,8 +26,6 @@ "typescript": "4.7.4" }, "dependencies": { - "file-saver": "^2.0.5", - "html2pdf.js": "^0.10.1", "jquery": "^3.5.14", "jszip": "^3.10.1" } diff --git a/scripts/html-gen.ts b/scripts/html-gen.ts new file mode 100644 index 0000000..8bb628c --- /dev/null +++ b/scripts/html-gen.ts @@ -0,0 +1,677 @@ +import { writeFile } from "fs/promises"; +import { MarkdownView, Notice, TextFileView } from "obsidian"; +import { ExportSettings } from "./settings"; +import { Utils } from "./utils"; +import jQuery from 'jquery'; +const $ = jQuery; + +export class HTMLGenerator +{ + // When this is enabled the plugin will download the extra .css and .js files from github. + autoDownloadExtras = true; + + private vaultPluginsPath: string = Utils.getVaultPath() + "/.obsidian/plugins"; + private thisPluginPath: string = this.vaultPluginsPath + "/obsidian-webpage-export"; + private assetsPath: string = this.thisPluginPath + "/assets"; + + // this is a list of images that is populated during generation and then downloaded upon export + // I am sure there is a better way to handle this data flow but I am not sure what to do. + private imagesToDownload: { original_path: string, destination_path_rel: string }[] = []; + + // this is a string containing the filtered app.css file. It is populated on load. + // The math styles are attempted to load on every export until they are succesfully loaded. + // This is because they only load when a file containing latex is opened. + appStyles: string = ""; + + // short html snippet for a dark mode toggle + darkModeToggle = + `\n\n +
+ +
+ \n\n` + + // the raw github urls for the extra files + private webpagejsURL: string = "https://raw.githubusercontent.com/KosmosisDire/obsidian-webpage-export/master/webpage.js"; + private pluginStylesURL: string = "https://raw.githubusercontent.com/KosmosisDire/obsidian-webpage-export/master/plugin-styles.css"; + private obsidianStylesURL: string = "https://raw.githubusercontent.com/KosmosisDire/obsidian-webpage-export/master/obsidian-styles.css"; + private async downloadExtras() + { + //Download webpage.js + let webpagejs = await fetch(this.webpagejsURL); + let webpagejsText = await webpagejs.text(); + await writeFile(this.assetsPath + "/webpage.js", webpagejsText).catch((err) => { console.log(err); }); + + //Download plugin-styles.css + let pluginStyles = await fetch(this.pluginStylesURL); + let pluginStylesText = await pluginStyles.text(); + await writeFile(this.assetsPath + "/plugin-styles.css", pluginStylesText).catch((err) => { console.log(err); }); + + //Download obsidian-styles.css + let obsidianStyles = await fetch(this.obsidianStylesURL); + let obsidianStylesText = await obsidianStyles.text(); + await writeFile(this.assetsPath + "/obsidian-styles.css", obsidianStylesText).catch((err) => { console.log(err); }); + } + + private async loadAppStyles() + { + var appSheet = document.styleSheets[1]; + let stylesheets = document.styleSheets; + for (let i = 0; i < stylesheets.length; i++) + { + if (stylesheets[i].href && stylesheets[i].href?.includes("app.css")) + { + appSheet = stylesheets[i]; + break; + } + } + + this.appStyles += await Utils.getText(this.assetsPath + "/obsidian-styles.css"); + + for (var i = 0; i < appSheet.cssRules.length; i++) + { + var rule = appSheet.cssRules[i]; + if (rule) + { + if (rule.cssText.startsWith("@font-face")) continue; + if (rule.cssText.startsWith(".CodeMirror")) continue; + if (rule.cssText.startsWith(".cm-")) continue; + + this.appStyles += rule.cssText + "\n"; + } + } + } + + public async initialize() + { + await this.downloadExtras(); + await this.loadAppStyles(); + } + + private async getThirdPartyPluginCSS() : Promise + { + // load 3rd party plugin css + let pluginCSS = ""; + + let thirdPartyPluginStyleNames = ExportSettings.settings.includePluginCSS.split("\n"); + for (let i = 0; i < thirdPartyPluginStyleNames.length; i++) + { + if (!thirdPartyPluginStyleNames[i] || (thirdPartyPluginStyleNames[i] && !(/\S/.test(thirdPartyPluginStyleNames[i])))) continue; + + let path = this.vaultPluginsPath + "/" + thirdPartyPluginStyleNames[i].replace("\n", "") + "/styles.css"; + let style = await Utils.getText(path); + if (style) + { + pluginCSS += style; + } + } + + return pluginCSS; + } + + async getSeperateFilesToDownload() : Promise<{filename: string, data: string, type: string, relativePath?: string, unicode?: boolean}[]> + { + var toDownload: {filename: string, data: string, type: string, relativePath?: string, unicode?: boolean}[] = []; + + if (!ExportSettings.settings.inlineCSS) + { + let appcss = this.appStyles; + let plugincss = await Utils.getText(this.assetsPath + "/plugin-styles.css"); + let themecss = await Utils.getThemeContent(Utils.getCurrentTheme()); + + let snippetsList = await Utils.getStyleSnippetsContent(); + let snippetsNames = await Utils.getEnabledSnippets(); + var snippets = ""; + + for (var i = 0; i < snippetsList.length; i++) + { + snippets += `/* --- ${snippetsNames[i]}.css --- */ \n ${snippetsList[i]} \n\n\n`; + } + + let thirdPartyPluginCSS = await this.getThirdPartyPluginCSS(); + plugincss += "\n" + thirdPartyPluginCSS + "\n"; + + let appcssDownload = { filename: "obsidian-styles.css", data: appcss, type: "text/css" }; + let plugincssDownload = { filename: "plugin-styles.css", data: plugincss, type: "text/css" }; + let themecssDownload = { filename: "theme.css", data: themecss, type: "text/css" }; + let snippetsDownload = { filename: "snippets.css", data: snippets, type: "text/css" }; + + toDownload.push(appcssDownload); + toDownload.push(plugincssDownload); + toDownload.push(themecssDownload); + toDownload.push(snippetsDownload); + } + + if (!ExportSettings.settings.inlineJS) + { + var webpagejs = await Utils.getText(this.assetsPath + "/webpage.js"); + var webpagejsDownload = { filename: "webpage.js", data: webpagejs, type: "text/javascript" }; + toDownload.push(webpagejsDownload); + } + + // let imagesDownload : {path: string, data: string}[] = []; + if (!ExportSettings.settings.inlineImages) + { + for (var i = 0; i < this.imagesToDownload.length; i++) + { + var image = this.imagesToDownload[i]; + var data = await Utils.getTextBase64(image.original_path); + var imageDownload = + { + filename: Utils.getFileNameFromFilePath(image.destination_path_rel), + data: data, + type: "image/png", + relativePath: Utils.getDirectoryFromFilePath(image.destination_path_rel), + unicode: false + }; + toDownload.push(imageDownload); + } + } + + return toDownload; + } + + //#region General HTML + + public async GetCurrentFileHTML(): Promise + { + await Utils.delay(200); + + let view = await Utils.getActiveView(); + if (!view) return null; + + Utils.setLineWidth(ExportSettings.settings.customLineWidth); + + if (view instanceof MarkdownView) + await Utils.viewEnableFullRender(view); + + var head = await this.generateHead(view); + var html = this.generateBodyHTML(); + html = this.fixLinks(html); + html = this.repairOnClick(html); //replace data-onlick with onclick + + if (ExportSettings.settings.inlineImages) + html = await this.inlineImages(html); + else + html = await this.outlineImages(html, view); + + // inject darkmode toggle + if (ExportSettings.settings.addDarkModeToggle) + { + html = await this.injectToggle(html); + } + + if (ExportSettings.settings.includeOutline) + { + var headers = this.getHeaderList(html); + if (headers) + { + var outline = this.generateOutline(headers); + // put side bars on either side of content and put them in a flex container + let el = document.createElement("html"); + el.innerHTML = html; + let body = el.querySelector("body"); + if (body) + { + html = `
${body.innerHTML}
`; + body.innerHTML = html; + html = body?.outerHTML; + } + else + { + console.error("Could not find body element in html"); + } + + el.remove(); + } + } + + html = head + html; + + // enclose in tags + html = "\n\n" + html + "\n"; + + return html; + } + + private generateBodyHTML(): string + { + var bodyClasses = document.body.getAttribute("class") ?? ""; + var bodyStyle = document.body.getAttribute("style") ?? ""; + /*@ts-ignore*/ + bodyClasses = bodyClasses.replaceAll("\"", "'"); + /*@ts-ignore*/ + bodyStyle = bodyStyle.replaceAll("\"", "'"); + + var htmlEl = (document.querySelector(".workspace-leaf.mod-active .markdown-reading-view") as HTMLElement) + if (!htmlEl) htmlEl = (document.querySelector(".workspace-leaf.mod-active .view-content") as HTMLElement); + + let width = "1000px"; + if (ExportSettings.settings.customLineWidth > 0) + width = ExportSettings.settings.customLineWidth + "px"; + else + { + let sizer = (htmlEl.querySelector(".markdown-preview-sizer.markdown-preview-section") as HTMLElement); + if (sizer) + { + width = sizer.style.width; + } + } + + htmlEl.style.flexBasis = width; + htmlEl.style.width = "unset"; + var html = htmlEl.outerHTML; + + html = "\n\n" + html + "\n\n"; + + return html; + } + + private getMathStyles(): string + { + let mathStyles = document.styleSheets[document.styleSheets.length - 1]; + var mathStylesString = ""; + + var success = true; + for (var i = 0; i < mathStyles.cssRules.length; i++) + { + var rule = mathStyles.cssRules[i]; + + if (rule) + { + if (i == 0 && !rule.cssText.startsWith("mjx")) + { + success = false; + break; + } + + if (rule.cssText.startsWith("@font-face")) continue; + + mathStylesString += rule.cssText + "\n"; + } + } + + return success ? mathStylesString : ""; + } + + private async generateHead(view: TextFileView): Promise + { + let meta = + ` + + + + + ${view.file.basename} + + ` + + let mathStyles = this.getMathStyles(); + let cssSettings = document.getElementById("css-settings-manager")?.innerHTML ?? ""; + let scripts = "\n\n" + + ((ExportSettings.settings.inlineJS ? ("\n") + "\n\n"); + + if (ExportSettings.settings.inlineCSS) + { + let pluginStyles = await Utils.getText(this.assetsPath + "/plugin-styles.css"); + let snippets = await Utils.getStyleSnippetsContent(); + let snippetNames = Utils.getEnabledSnippets(); + let theme = await Utils.getThemeContent(Utils.getCurrentTheme()); + + let thirdPartyPluginStyles = await this.getThirdPartyPluginCSS(); + pluginStyles += thirdPartyPluginStyles; + + var header = + ` + + + ${meta} + + + + + + + + + + + + + + + + ${scripts} + + + `; + } + else + { + header = + ` + + + ${meta} + + + + + + + + + + ${scripts} + + + `; + } + + return header; + } + + //#endregion + + //#region Links and Images + + private fixLinks(html: string): string + { + let el = document.createElement('html'); + el.innerHTML = html; + + let query = jQuery(el); + query.find("a.internal-link").each(function () + { + $(this).attr("target", "_self"); + + let finalHref = ""; + let href = $(this).attr("href")?.split("#"); + + if (!href) return; + + // if the file doesn't start with #, then it links to a file, or a header in another file. + if (!(href[0] == "")) + { + if (href.length == 1) + { + finalHref = href[0] + ".html"; + } + + if (href.length == 2) + { + var filePath = ""; + if (!href[0].contains("/") && !href[0].contains("\\")) + { + filePath = Utils.getDirectoryFromFilePath(Utils.getFirstFileByName(href[0])?.path ?? "") + "/"; + } + + finalHref = filePath + href[0] + ".html#" + href[1].replaceAll(" ", "_").replaceAll("#", "").replaceAll("__", "_"); + } + + if (href.length > 2) + { + let first = href.shift() ?? ""; + + var filePath = ""; + if (!first.contains("/") && !first.contains("\\")) + { + filePath = Utils.getDirectoryFromFilePath(Utils.getFirstFileByName(first)?.path ?? "") + "/"; + } + + finalHref = filePath + first + ".html#" + href.join("#").replaceAll(" ", "_").replaceAll("#", "").replaceAll("__", "_"); + } + } + else // if the file starts with #, then it links to an internal header. + { + href.shift(); + if (href.length == 1) + { + finalHref = "#" + href[0].replaceAll(" ", "_").replaceAll("#", "").replaceAll("__", "_"); + } + + if (href.length > 1) + { + finalHref = href.join("#").replaceAll(" ", "_").replaceAll("#", "").replaceAll("__", "_"); + } + } + + $(this).attr("href", finalHref); + }); + + query.find("h1, h2, h3, h4, h5, h6").each(function () + { + // use the headers inner text as the id + $(this).attr("id", $(this).text().replaceAll(" ", "_").replaceAll("#", "").replaceAll("__", "_")); + }); + + let result = el.innerHTML; + el.remove(); + return result; + } + + private async inlineImages(html: string): Promise + { + let el = document.createElement('html'); + el.innerHTML = html; + + let query = jQuery(el); + let images = query.find("img").toArray(); + + for (let i = 0; i < images.length; i++) + { + let img = images[i]; + if ($(img).attr("src")?.startsWith("app://local/")) + { + let path = $(img).attr("src")?.replace("app://local/", "").replaceAll("%20", " ").split("?")[0]; + + if (path) + { + var base64 = ""; + try + { + base64 = await Utils.getTextBase64(path); + } + catch (e) + { + console.error(e); + console.warn("Failed to inline image: " + path); + new Notice("Failed to inline image: " + path, 5000); + continue; + } + + $(img).attr("src", "data:image/png;base64," + base64); + } + } + } + + let result = el.innerHTML; + el.remove(); + return result; + } + + private async outlineImages(html: string, view: TextFileView): Promise + { + let el = document.createElement('html'); + el.innerHTML = html; + + let query = jQuery(el); + + this.imagesToDownload = []; + + let img2Download: { original_path: string, destination_path_rel: string }[] = []; + let vaultPath = Utils.getVaultPath(); + + query.find("img").each(function () + { + if (!$(this).attr("src")?.startsWith("app://local/")) return; + + let originalPath = $(this).attr("src")?.replaceAll("\\", "/").replaceAll("%20", " ").replace("app://local/", ""); + + // console.log("originalPath: " + originalPath); + + if (!originalPath) + { + new Notice("Failed to outline image: " + originalPath + ". Couldn't find image src", 5000); + return; + } + + if (originalPath.startsWith("data:image/png;base64,") || originalPath.startsWith("data:image/jpeg;base64,")) return; + + let relPath = originalPath.split(Utils.getDirectoryFromFilePath(vaultPath + "/" + view.file.path.replaceAll("\\", "/")) + "/")[1]; + + // console.log(originalPath, "vs.", Utils.getDirectoryFromFilePath(vaultPath + "/" + view.file.path.replaceAll("\\", "/")) + "/"); + + if (!relPath) + relPath = ("images/" + Utils.getFileNameFromFilePath($(this).attr("src")?.replaceAll("\\", "/").replaceAll("%20", " ") ?? "")) ?? "img.png"; + + // console.log("relPath: " + relPath); + + $(this).attr("src", relPath); + + img2Download.push({ original_path: originalPath, destination_path_rel: relPath }); + + }); + + this.imagesToDownload = img2Download; + + let result = el.innerHTML; + el.remove(); + return result; + } + + //#endregion + + //#region Special Features + + private async injectToggle(html: string): Promise + { + if (!html.contains(this.darkModeToggle.split("\n")[4])) + { + //insert fixed toggle in corner + console.log("Injecting toggle"); + html = this.darkModeToggle.replace("theme-toggle-inline", "theme-toggle") + html; + } + + return html; + } + + private repairOnClick(html: string): string + { + html = html.replaceAll("data-onclick", "onclick"); + return html; + } + + private getHeaderList(html: string): { size: number, title: string, href: string }[] | null + { + var headers = []; + + var el = document.createElement('html'); + el.innerHTML = html; + + var headerElements = el.querySelectorAll("h1, h2, h3, h4, h5, h6"); + + for (var i = 0; i < headerElements.length; i++) + { + var header = headerElements[i]; + var size = parseInt(header.tagName[1]); + var title = (header as HTMLElement).innerText; + var href = (header as HTMLHeadingElement).id; + headers.push({ size, title, href }); + } + + el.remove(); + + return headers; + } + + private generateOutline(headers: { size: number, title: string, href: string }[]): string + { + var outline = + ` +
+ +
+ +
Table of Contents
+
+ +
+ `; + + var builderRoot = document.createElement('html'); + builderRoot.innerHTML = outline; + + var outlineEl = builderRoot.querySelector(".outline-container"); + + if (!outlineEl) return ""; + + var listItemTemplate = + ` + +
+ +
+
+ + +
+ + {title} +
+ +
+ +
+
+ `; + + + var listStack = [outlineEl]; + + // function to get the data-size of the previous list item as a number + function getLastStackSize(): number + { + return parseInt(listStack[listStack.length - 1].getAttribute("data-size") ?? "0"); + } + + + for (var i = 0; i < headers.length; i++) + { + var header = headers[i]; + var listItem = listItemTemplate.replace("{size}", header.size.toString()).replace("{href}", header.href).replace("{title}", header.title); + + while (getLastStackSize() >= header.size && listStack.length > 1) + { + listStack.pop(); + } + + var builditemRoot = document.createElement('div'); + builditemRoot.innerHTML = listItem; + var newOutlineItem = builditemRoot.querySelector(".outline-item"); + + if (!newOutlineItem) continue; + + var childContainer = listStack.last()?.querySelector(".outline-item-children"); + if (getLastStackSize() == 0) childContainer = listStack.last(); + + if (!childContainer) continue; + + childContainer.appendChild(newOutlineItem); + listStack.push(newOutlineItem); + + builditemRoot.remove(); + } + + var result = builderRoot.innerHTML; + builderRoot.remove(); + + return result; + } + + //#endregion +} diff --git a/scripts/leaf-handler.ts b/scripts/leaf-handler.ts new file mode 100644 index 0000000..77ce614 --- /dev/null +++ b/scripts/leaf-handler.ts @@ -0,0 +1,114 @@ +import { MarkdownView, OpenViewState, PaneType, SplitDirection, TFile, WorkspaceLeaf } from "obsidian"; + + +export class LeafHandler +{ + // https://github.com/darlal/obsidian-switcher-plus/blob/27d337039883008bcbf40ca13ea2f9287469dde4/src/Handlers/handler.ts#L388 + // Some functions here are from this source and have been modified to fit the needs of this plugin. + + isMainPanelLeaf(leaf: WorkspaceLeaf): boolean + { + const { workspace } = app; + const root = leaf?.getRoot(); + /*@ts-ignore*/ + return root === workspace.rootSplit || root === workspace.floatingSplit; + } + + getOpenLeaves(excludeMainPanelViewTypes?: string[], includeSidePanelViewTypes?: string[]): WorkspaceLeaf[] + { + const leaves: WorkspaceLeaf[] = []; + + const saveLeaf = (l: WorkspaceLeaf) => + { + const viewType = l.view?.getViewType(); + + if (this.isMainPanelLeaf(l)) + { + if (!excludeMainPanelViewTypes?.includes(viewType)) + { + leaves.push(l); + } + } else if (includeSidePanelViewTypes?.includes(viewType)) + { + leaves.push(l); + } + }; + + app.workspace.iterateAllLeaves(saveLeaf); + return leaves; + } + + openFileInNewLeaf( + file: TFile, + navType: PaneType | boolean, + openState?: OpenViewState, + errorContext?: string, + splitDirection: SplitDirection = 'vertical', + ): WorkspaceLeaf + { + const { workspace } = app; + errorContext = errorContext ?? ''; + const message = `HTML Export: error opening file. ${errorContext}`; + + const getLeaf = () => + { + return navType === 'split' + ? workspace.getLeaf(navType, splitDirection) + : workspace.getLeaf(navType); + }; + + var leaf = getLeaf(); + + try + { + leaf.openFile(file, openState).catch((reason) => + { + console.log(message, reason); + }); + + } + catch (error) + { + console.log(message, error); + } + + return leaf; + } + + getLeafByFile(file: TFile): WorkspaceLeaf | null + { + const leaves = this.getOpenLeaves(); + for (let leaf of leaves) + { + if (leaf.view instanceof MarkdownView) + { + if (leaf.view.file.path === file.path) + { + return leaf; + } + } + } + + return null; + } + + switchToLeafWithFile(file: TFile, openNewIfNotOpen: boolean): {leaf: WorkspaceLeaf | null, alreadyOpen: boolean} + { + const { workspace } = app; + let leaf = this.getLeafByFile(file); + + let alreadyOpen = false; + if (leaf) + { + workspace.setActiveLeaf(leaf); + alreadyOpen = true; + } + else if (openNewIfNotOpen) + { + leaf = this.openFileInNewLeaf(file, true, { active: true }, "Failed to open file to new tab after it was found to not be open yet."); + } + + return {leaf, alreadyOpen}; + } +} + diff --git a/scripts/main.ts b/scripts/main.ts new file mode 100644 index 0000000..b9756b1 --- /dev/null +++ b/scripts/main.ts @@ -0,0 +1,154 @@ +// imports from obsidian API +import { Plugin, TAbstractFile, TFile} from 'obsidian'; + +// modules that are part of the plugin +import { ExportSettings } from './settings'; +import { Utils } from './utils'; +import { LeafHandler } from './leaf-handler'; +import { HTMLGenerator } from './html-gen'; + +export default class HTMLExportPlugin extends Plugin +{ + leafHandler: LeafHandler = new LeafHandler(); + htmlGenerator: HTMLGenerator = new HTMLGenerator(); + + addTogglePostprocessor() + { + this.registerMarkdownCodeBlockProcessor("theme-toggle", (source, el, ctx) => + { + let parent = el.createEl('div'); + parent.innerHTML = this.htmlGenerator.darkModeToggle; + }); + + //also replace `theme-toggle` and ```theme-toggle``` for better inline toggles, or in places you couldn't use a normal code block + this.registerMarkdownPostProcessor((element, context) => + { + let codeBlocks = element.querySelectorAll('code, span.cm-inline-code'); + codeBlocks.forEach((codeBlock) => + { + // console.log(codeBlock); + if (codeBlock instanceof HTMLElement && codeBlock.innerText == "theme-toggle") + { + codeBlock.outerHTML = this.htmlGenerator.darkModeToggle; + } + }); + }); + } + + async onload() + { + console.log('loading obsidian-webpage-export plugin'); + + // init settings + new ExportSettings(this); + ExportSettings.loadSettings(); + + // Register the Export As HTML button in the file menu + this.registerEvent( + this.app.workspace.on("file-menu", (menu, file) => + { + menu.addItem((item) => + { + item + .setTitle("Export to HTML") + .setIcon("document") + .onClick(async () => + { + if (Utils.getFileNameFromFilePath(file.path).contains(".")) + this.exportFile(file); + + else + this.exportFolder(file.path); + }); + }); + }) + ); + + // add export vault icon to ribbon + this.addRibbonIcon("folder-up", "Export Vault to HTML", async () => + { + this.exportFolder(""); + }); + + // add toggle postprocessor + this.addTogglePostprocessor(); + + // init html generator + this.htmlGenerator.initialize(); + } + + onunload() + { + console.log('unloading obsidian-webpage-export plugin'); + } + + async exportFile(file: TAbstractFile, fullPath: string = "", showSettings: boolean = true) + { + // Open the settings modal and wait until it's closed + let onlyCopy = false; + if (showSettings) + { + let result = await new ExportSettings(this).open(); + if (result.canceled) return; + onlyCopy = result.onlyCopy; + } + + let fileTab = this.leafHandler.openFileInNewLeaf(file as TFile, true); + + var html = await this.htmlGenerator.GetCurrentFileHTML(); + if (!html) return; + + let toDownload = await this.htmlGenerator.getSeperateFilesToDownload(); + + // Close the file tab after HTML is generated + fileTab.detach(); + + + // if onlyCopy is true, then we don't want to download the file, we just want to copy it to the clipboard + if (onlyCopy) + { + navigator.clipboard.writeText(html); + return; + } + + // Download files + var htmlDownload = { filename: file.name.replace(".md", ".html"), data: html, type: "text/html" }; + toDownload.push(htmlDownload); + + var htmlPath: string | null = fullPath; + if (htmlPath == "") + htmlPath = await Utils.showSaveDialog(Utils.idealDefaultPath(), file.name.replace(".md", ".html"), false); + + if (!htmlPath) return; + + let filename = Utils.getFileNameFromFilePath(htmlPath); + let folderPath = Utils.getDirectoryFromFilePath(htmlPath); + + toDownload[toDownload.length - 1].filename = filename; + + await Utils.downloadFiles(toDownload, folderPath); + } + + async exportFolder(folderPath: string) + { + // folder path is the path relative to the vault that we are exporting + + // Open the settings modal and wait until it's closed + var exportCanceled = !await new ExportSettings(this).open(); + if (exportCanceled) return; + + let htmlPath = await Utils.showSelectFolderDialog(Utils.idealDefaultPath()); + + var files = this.app.vault.getFiles(); + + for (var i = 0; i < files.length; i++) + { + var file = files[i]; + if (file.path.startsWith(folderPath) && file.extension == "md") + { + var fullPath = htmlPath + "/" + file.path.replace(".md", ".html"); + await this.exportFile(file, fullPath, false); + } + } + } +} diff --git a/scripts/settings.ts b/scripts/settings.ts new file mode 100644 index 0000000..e19b68d --- /dev/null +++ b/scripts/settings.ts @@ -0,0 +1,221 @@ +import { App, Modal, Plugin, Setting } from 'obsidian'; +import { Utils } from './utils'; + +export interface ExportSettingsData +{ + inlineCSS: boolean; + inlineJS: boolean; + inlineImages: boolean; + + uzeZip: boolean; + addDarkModeToggle: boolean; + includeOutline: boolean; + customLineWidth: number; + lastExportPath: string; + + customJS: string; + customCSS: string; + + includePluginCSS: string; +} + +const DEFAULT_SETTINGS: ExportSettingsData = +{ + inlineCSS: true, + inlineJS: true, + inlineImages: true, + + uzeZip: false, + addDarkModeToggle: true, + includeOutline: true, + customLineWidth: 0, + lastExportPath: '', + + customJS: '', + customCSS: '', + + includePluginCSS: '' +} + +export class ExportSettings extends Modal +{ + static settings: ExportSettingsData = DEFAULT_SETTINGS; + static plugin: Plugin; + static isClosed: boolean = true; + static success: boolean = false; + static onlyCopy: boolean = false; + + constructor(plugin: Plugin) + { + super(plugin.app); + ExportSettings.plugin = plugin; + } + + static async loadSettings() + { + ExportSettings.settings = Object.assign({}, DEFAULT_SETTINGS, await ExportSettings.plugin.loadData()); + } + + static async saveSettings() + { + await ExportSettings.plugin.saveData(ExportSettings.settings); + } + + /** + * @brief Opens the modal and async blocks until the modal is closed. + * @returns True if the EXPORT button was pressed, false is the export was canceled. + * @override + */ + async open(): Promise<{canceled: boolean, onlyCopy: boolean}> + { + super.open(); + ExportSettings.isClosed = false; + + const { contentEl } = this; + + contentEl.empty(); + + let header = contentEl.createEl('h2', { text: 'HTML Webpage Settings' }); + header.style.display = 'inline-block'; + + let supporLink = contentEl.createEl('a', { href: 'https://www.buymeacoffee.com/nathangeorge' }); + let supportButton = supporLink.createEl('img'); + supportButton.setAttribute('src', 'https://cdn.buymeacoffee.com/buttons/v2/default-violet.png'); + supportButton.setAttribute('style', ' height: 30px; margin-right: 0; margin-left: 17px; transform: translateY(25%);'); + supportButton.setAttribute('alt', 'Buy Me A Coffee'); + + contentEl.createEl('h3', { text: 'Inlining Options:' }); + + new Setting(contentEl) + .setName('Inline CSS') + .setDesc('Inline the CSS into the HTML file.') + .addToggle((toggle) => toggle + .setValue(ExportSettings.settings.inlineCSS) + .onChange(async (value) => + { + ExportSettings.settings.inlineCSS = value; + await ExportSettings.saveSettings(); + })); + + new Setting(contentEl) + .setName('Inline JS') + .setDesc('Inline the JS into the HTML file.') + .addToggle((toggle) => toggle + .setValue(ExportSettings.settings.inlineJS) + .onChange(async (value) => + { + ExportSettings.settings.inlineJS = value; + await ExportSettings.saveSettings(); + })); + + new Setting(contentEl) + .setName('Inline Images') + .setDesc('Inline the images into the HTML file.') + .addToggle((toggle) => toggle + .setValue(ExportSettings.settings.inlineImages) + .onChange(async (value) => + { + ExportSettings.settings.inlineImages = value; + await ExportSettings.saveSettings(); + })); + + + contentEl.createEl('h3', { text: 'Special Features:' }); + + new Setting(contentEl) + .setName('Add Dark Mode Toggle') + .setDesc('Adds a fixed theme toggle to the top of any page that doesn\'t already have a toggle embedded with `theme-toggle`.') + .addToggle((toggle) => toggle + .setValue(ExportSettings.settings.addDarkModeToggle) + .onChange(async (value) => + { + ExportSettings.settings.addDarkModeToggle = value; + await ExportSettings.saveSettings(); + })); + + new Setting(contentEl) + .setName('Include Document Outline') + .setDesc('Will include an interactive document outline tree on the right side of the document.') + .addToggle((toggle) => toggle + .setValue(ExportSettings.settings.includeOutline) + .onChange(async (value) => + { + ExportSettings.settings.includeOutline = value; + await ExportSettings.saveSettings(); + } + )); + + new Setting(contentEl) + .setName('Custom Line Width') + .setDesc('Will set the line width of the document to the specified value. If set to 0, will use whatever the current line width is.') + .addText((text) => text + .setValue(ExportSettings.settings.customLineWidth.toString()) + .onChange(async (value) => + { + ExportSettings.settings.customLineWidth = parseInt(value); + await ExportSettings.saveSettings(); + } + )); + + contentEl.createEl('h3', { text: 'Export Options:' }); + + // new Setting(contentEl) + // .setName('Export to ZIP') + // .setDesc('Will export a .zip file rather than putting all the files loose in the chosen folder.') + // .addToggle((toggle) => toggle + // .setValue(ExportSettings.settings.uzeZip) + // .onChange(async (value) => + // { + // ExportSettings.settings.uzeZip = value; + // await ExportSettings.saveSettings(); + // })); + + new Setting(contentEl) + .setName('Include Plugin CSS') + .setDesc('Will include the CSS from the plugins listed below. Please write out the plugin\'s ID / folder name exactly each on a new line.') + .addTextArea((text) => text + .setValue(ExportSettings.settings.includePluginCSS) + .onChange(async (value) => + { + ExportSettings.settings.includePluginCSS = value; + await ExportSettings.saveSettings(); + } + )); + + new Setting(contentEl) + .setName('Start Export') + .addButton((button) => button + .setButtonText('Export') + .onClick(async () => + { + this.close(); + ExportSettings.success = true; + } + )); + + new Setting(contentEl) + .setName('Export and Copy HTML') + .addButton((button) => button + .setButtonText('Copy HTML') + .onClick(async () => + { + this.close(); + ExportSettings.success = true; + ExportSettings.onlyCopy = true; + } + )); + + await Utils.waitUntil(() => ExportSettings.isClosed, 60 * 60 * 1000, 200); + + return { canceled: !ExportSettings.success, onlyCopy: ExportSettings.onlyCopy }; + } + + onClose() + { + const { contentEl } = this; + contentEl.empty(); + ExportSettings.isClosed = true; + ExportSettings.success = false; + ExportSettings.onlyCopy = false; + } +} diff --git a/scripts/utils.ts b/scripts/utils.ts new file mode 100644 index 0000000..1f67969 --- /dev/null +++ b/scripts/utils.ts @@ -0,0 +1,340 @@ +import { open, readFile, writeFile, existsSync, mkdirSync } from 'fs'; +import { FileSystemAdapter, MarkdownView, TextFileView, TFile } from 'obsidian'; +import { ExportSettings } from './settings'; +var JSZip = require("jszip"); + +/* @ts-ignore */ +const dialog: Electron.Dialog = require('electron').remote.dialog; + +export class Utils +{ + static async delay (ms: number) + { + return new Promise( resolve => setTimeout(resolve, ms) ); + } + + + static async getText(path: string): Promise + { + return new Promise((resolve, reject) => { + open(path, 'r', (err, fd) => { + if (err) { + reject(err); + } else { + readFile(fd, { encoding: 'utf8' }, (err, data) => { + if (err) { + reject(err); + } else { + resolve(data); + } + }); + } + }); + }); + } + + static async getTextBase64(path: string): Promise + { + return new Promise((resolve, reject) => { + open(path, 'r', (err, fd) => { + if (err) { + reject(err); + } else { + readFile(fd, { encoding: 'base64' }, (err, data) => { + if (err) { + reject(err); + } else { + resolve(data); + } + }); + } + }); + }); + } + + static changeViewMode(view: MarkdownView, modeName: "preview" | "source") + { + /*@ts-ignore*/ + const mode = view.modes[modeName]; + /*@ts-ignore*/ + mode && view.setMode(mode); + }; + + static createUnicodeArray(content: string) : Uint8Array + { + var charCode, byteArray = []; + + // BE BOM + byteArray.push(254, 255); + + // LE BOM + // byteArray.push(255, 254); + + for (var i = 0; i < content.length; ++i) { + + charCode = content.charCodeAt(i); + + // BE Bytes + byteArray.push((charCode & 0xFF00) >>> 8); + byteArray.push(charCode & 0xFF); + + // LE Bytes + // byteArray.push(charCode & 0xff); + // byteArray.push(charCode / 256 >>> 0); + } + + return new Uint8Array(byteArray); + } + + static async showSaveDialog(defaultPath: string, defaultFileName: string, showAllFilesOption: boolean = true): Promise + { + let type = (defaultFileName.split(".").pop() ?? "txt"); + + var filters = [{ + name: type.toUpperCase() + " Files", + extensions: [type] + }]; + + if (showAllFilesOption) + { + filters.push({ + name: "All Files", + extensions: ["*"] + }); + } + + let picker = await dialog.showSaveDialog({ + defaultPath: (defaultPath + "/" + defaultFileName).replaceAll("\\", "/").replaceAll("//", "/"), + filters: filters, + properties: ["showOverwriteConfirmation"] + }) + + if (picker.canceled) return null; + + let path = picker.filePath ?? ""; + + if (path != "") + { + ExportSettings.settings.lastExportPath = path; + ExportSettings.saveSettings(); + } + + return path; + } + + static async showSelectFolderDialog(defaultPath: string): Promise + { + let picker = await dialog.showOpenDialog({ + defaultPath: defaultPath, + properties: ["openDirectory"] + }); + + if (picker.canceled) return null; + + let path = picker.filePaths[0] ?? ""; + + if (path != "") + { + ExportSettings.settings.lastExportPath = path; + ExportSettings.saveSettings(); + } + + return path; + } + + static idealDefaultPath() : string + { + return ExportSettings.settings.lastExportPath == "" ? (Utils.getVaultPath() ?? "") : ExportSettings.settings.lastExportPath; + } + + static async downloadFile(data: string, filename: string, path: string = "") + { + if (path == "") + { + path = await Utils.showSaveDialog(Utils.idealDefaultPath(), filename) ?? ""; + + if (path == "") return; + } + + var array = Utils.createUnicodeArray(data); + + writeFile(path, array, (err) => { + if (err) throw err; + console.log('The file has been saved!'); + }); + } + + static async downloadFilesAsZip(files: {filename: string, data: string, type: string, relativePath?: string}[], zipFileName: string) + { + var blobs = files.map(file => new Blob([file.data], {type: file.type})); + var zip = new JSZip(); + for (var i = 0; i < files.length; i++) + { + let path = ((files[i].relativePath ?? "") + "/" + files[i].filename).replaceAll("//", "/"); + zip.file(path, blobs[i]); + } + + var zipBlob = await zip.generateAsync({type: "uint8array"}); + + var path = await Utils.showSaveDialog(Utils.idealDefaultPath(), zipFileName, false) ?? ""; + + if (path == "") return; + + writeFile(path, zipBlob, (err) => { + if (err) throw err; + console.log('The file has been saved!'); + }); + } + + static async downloadFiles(files: {filename: string, data: string, type?: string, relativePath?: string, unicode?: boolean}[], folderPath: string) + { + for (var i = 0; i < files.length; i++) + { + var array = (files[i].unicode ?? true) ? Utils.createUnicodeArray(files[i].data) : Buffer.from(files[i].data, 'base64'); + + let path = (folderPath + "/" + (files[i].relativePath ?? "") + "/" + files[i].filename).replaceAll("\\", "/").replaceAll("//", "/").replaceAll("//", "/"); + + let dir = Utils.getDirectoryFromFilePath(path); + if (!existsSync(dir)) + { + mkdirSync(dir, { recursive: true }); + } + + writeFile(path, array, (err) => { + if (err) throw err; + console.log('The file has been saved!'); + }); + } + } + + static getDirectoryFromFilePath(path: string): string + { + var forwardIndex = path.lastIndexOf("/"); + var backwardIndex = path.lastIndexOf("\\"); + + var index = forwardIndex > backwardIndex ? forwardIndex : backwardIndex; + + if (index == -1) return ""; + + return path.substring(0, index); + } + + static getFileNameFromFilePath(path: string): string + { + var forwardIndex = path.lastIndexOf("/"); + var backwardIndex = path.lastIndexOf("\\"); + + var index = forwardIndex > backwardIndex ? forwardIndex : backwardIndex; + + if (index == -1) return path; + + return path.substring(index + 1); + } + + static getVaultPath(): string | null + { + let adapter = app.vault.adapter; + if (adapter instanceof FileSystemAdapter) { + return adapter.getBasePath().replaceAll("\\", "/"); + } + + return null; + } + + //async function that awaits until a condition is met + static async waitUntil(condition: () => boolean, timeout: number = 1000, interval: number = 100): Promise + { + return new Promise((resolve, reject) => { + let timer = 0; + let intervalId = setInterval(() => { + if (condition()) { + clearInterval(intervalId); + resolve(); + } else { + timer += interval; + if (timer >= timeout) { + clearInterval(intervalId); + reject(); + } + } + }, interval); + }); + } + + static async getThemeContent(themeName: string): Promise + { + let themePath = this.getVaultPath() + "/.obsidian/themes/" + themeName + "/theme.css"; + if (!existsSync(themePath)) return ""; + let themeContent = await Utils.getText(themePath); + return themeContent; + } + + static getCurrentTheme(): string + { + /*@ts-ignore*/ // config does exist + return app.vault.config?.cssTheme ?? "Default"; + } + + static getEnabledSnippets(): string[] + { + /*@ts-ignore*/ + return app.vault.config?.enabledCssSnippets ?? []; + } + + static async getStyleSnippetsContent(): Promise + { + let snippetContents : string[] = []; + let enabledSnippets = this.getEnabledSnippets(); + + for (var i = 0; i < enabledSnippets.length; i++) + { + snippetContents.push(await Utils.getText(Utils.getVaultPath() + "/.obsidian/snippets/" + enabledSnippets[i] + ".css")); + } + + return snippetContents; + } + + static async viewEnableFullRender(view: MarkdownView) + { + Utils.changeViewMode(view, "preview"); + await this.delay(200); + /*@ts-ignore*/ + view.previewMode.renderer.showAll = true; + /*@ts-ignore*/ + await view.previewMode.renderer.unfoldAllHeadings(); + await Utils.delay(300); + /*@ts-ignore*/ + await view.previewMode.renderer.rerender(); + } + + static async getActiveView(): Promise + { + let view = app.workspace.getActiveViewOfType(TextFileView); + if (!view) + { + console.log("Failed to find active view"); + return null; + } + + return view; + } + + static getFirstFileByName(name: string): TFile | undefined + { + return app.vault.getFiles().find(file => + { + if(!name) return false; + return file.basename == name; + }); + } + + static setLineWidth(width: number) : void + { + if (width != 0) + { + let sizers = document.getElementsByClassName("markdown-preview-sizer markdown-preview-section"); + if (sizers.length > 0) + sizers[0].setAttribute("style", "max-width: " + width + "px"); + } + } +} diff --git a/settings.ts b/settings.ts deleted file mode 100644 index 79ae1e7..0000000 --- a/settings.ts +++ /dev/null @@ -1,218 +0,0 @@ -import { App, Modal, Plugin, Setting} from 'obsidian'; -import { Utils } from 'main'; - -export interface ExportSettingsData -{ - inlineCSS: boolean; - inlineJS: boolean; - inlineImages: boolean; - - uzeZip: boolean; - addDarkModeToggle: boolean; - includeOutline: boolean; - customLineWidth: number; - lastExportPath: string; - - customJS: string; - customCSS: string; - - includePluginCSS: string; -} - -const DEFAULT_SETTINGS: ExportSettingsData = -{ - inlineCSS: true, - inlineJS: true, - inlineImages: true, - - uzeZip: false, - addDarkModeToggle: true, - includeOutline: true, - customLineWidth: 0, - lastExportPath: '', - - customJS: '', - customCSS: '', - - includePluginCSS: '' -} - -export class ExportSettings extends Modal -{ - static settings: ExportSettingsData = DEFAULT_SETTINGS; - static plugin: Plugin; - static isClosed: boolean = true; - static success: boolean = false; - - constructor(plugin: Plugin) - { - super(plugin.app); - ExportSettings.plugin = plugin; - } - - static async loadSettings() - { - ExportSettings.settings = Object.assign({}, DEFAULT_SETTINGS, await ExportSettings.plugin.loadData()); - } - - static async saveSettings() - { - await ExportSettings.plugin.saveData(ExportSettings.settings); - } - - /** - * @brief Opens the modal and async blocks until the modal is closed. - * @returns True if the EXPORT button was pressed, false is the export was canceled. - * @override - */ - async open(): Promise - { - super.open(); - ExportSettings.isClosed = false; - - const {contentEl} = this; - - contentEl.empty(); - - let header = contentEl.createEl('h2', {text: 'HTML Webpage Settings'}); - header.style.display = 'inline-block'; - - let supporLink = contentEl.createEl('a', {href: 'https://www.buymeacoffee.com/nathangeorge'}); - let supportButton = supporLink.createEl('img'); - supportButton.setAttribute('src', 'https://cdn.buymeacoffee.com/buttons/v2/default-violet.png'); - supportButton.setAttribute('style', ' height: 30px; margin-right: 0; margin-left: 17px; transform: translateY(25%);'); - supportButton.setAttribute('alt', 'Buy Me A Coffee'); - - contentEl.createEl('h3', {text: 'Inlining Options:'}); - - new Setting(contentEl) - .setName('Inline CSS') - .setDesc('Inline the CSS into the HTML file.') - .addToggle((toggle) => toggle - .setValue(ExportSettings.settings.inlineCSS) - .onChange(async (value) => - { - ExportSettings.settings.inlineCSS = value; - await ExportSettings.saveSettings(); - })); - - new Setting(contentEl) - .setName('Inline JS') - .setDesc('Inline the JS into the HTML file.') - .addToggle((toggle) => toggle - .setValue(ExportSettings.settings.inlineJS) - .onChange(async (value) => - { - ExportSettings.settings.inlineJS = value; - await ExportSettings.saveSettings(); - })); - - new Setting(contentEl) - .setName('Inline Images') - .setDesc('Inline the images into the HTML file.') - .addToggle((toggle) => toggle - .setValue(ExportSettings.settings.inlineImages) - .onChange(async (value) => - { - ExportSettings.settings.inlineImages = value; - await ExportSettings.saveSettings(); - })); - - - contentEl.createEl('h3', {text: 'Special Features:'}); - - new Setting(contentEl) - .setName('Add Dark Mode Toggle') - .setDesc('Adds a fixed theme toggle to the top of any page that doesn\'t already have a toggle embedded with `theme-toggle`.') - .addToggle((toggle) => toggle - .setValue(ExportSettings.settings.addDarkModeToggle) - .onChange(async (value) => - { - ExportSettings.settings.addDarkModeToggle = value; - await ExportSettings.saveSettings(); - })); - - new Setting(contentEl) - .setName('Include Document Outline') - .setDesc('Will include an interactive document outline tree on the right side of the document.') - .addToggle((toggle) => toggle - .setValue(ExportSettings.settings.includeOutline) - .onChange(async (value) => - { - ExportSettings.settings.includeOutline = value; - await ExportSettings.saveSettings(); - } - )); - - new Setting(contentEl) - .setName('Custom Line Width') - .setDesc('Will set the line width of the document to the specified value. If set to 0, will use whatever the current line width is.') - .addText((text) => text - .setValue(ExportSettings.settings.customLineWidth.toString()) - .onChange(async (value) => - { - ExportSettings.settings.customLineWidth = parseInt(value); - await ExportSettings.saveSettings(); - } - )); - - contentEl.createEl('h3', {text: 'Export Options:'}); - - // new Setting(contentEl) - // .setName('Export to ZIP') - // .setDesc('Will export a .zip file rather than putting all the files loose in the chosen folder.') - // .addToggle((toggle) => toggle - // .setValue(ExportSettings.settings.uzeZip) - // .onChange(async (value) => - // { - // ExportSettings.settings.uzeZip = value; - // await ExportSettings.saveSettings(); - // })); - - new Setting(contentEl) - .setName('Include Plugin CSS') - .setDesc('Will include the CSS from the plugins listed below. Please write out the plugin\'s ID / folder name exactly each on a new line.') - .addTextArea((text) => text - .setValue(ExportSettings.settings.includePluginCSS) - .onChange(async (value) => - { - ExportSettings.settings.includePluginCSS = value; - await ExportSettings.saveSettings(); - } - )); - - new Setting(contentEl) - .setName('Start Export') - .addButton((button) => button - .setButtonText('Export') - .onClick(async () => - { - this.close(); - ExportSettings.success = true; - } - )); - - new Setting(contentEl) - .setName('Export and Copy HTML') - .addButton((button) => button - .setButtonText('Copy HTML') - .onClick(async () => - { - this.close(); - ExportSettings.success = true; - } - )); - - await Utils.waitUntil(() => ExportSettings.isClosed, 60 * 60 * 1000, 200); - - return ExportSettings.success; - } - - onClose() - { - const {contentEl} = this; - contentEl.empty(); - ExportSettings.isClosed = true; - ExportSettings.success = false; - } -} \ No newline at end of file