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 =
- `
-
-
-
-
-
- `;
-
- var builderRoot = document.createElement( 'html' );
- builderRoot.innerHTML = outline;
-
- var outlineEl = builderRoot.querySelector(".outline-container");
-
- if (!outlineEl) return "";
-
- var listItemTemplate =
- `
-
-
- `;
-
-
- 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 =
+ `
+
+
+
+
+
+ `;
+
+ var builderRoot = document.createElement('html');
+ builderRoot.innerHTML = outline;
+
+ var outlineEl = builderRoot.querySelector(".outline-container");
+
+ if (!outlineEl) return "";
+
+ var listItemTemplate =
+ `
+
+
+ `;
+
+
+ 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