Files
WorkNote/.obsidian/plugins/obsidian-webpage-export-master/scripts/html-generation/asset-handler.ts
2025-04-10 14:07:13 +08:00

358 lines
14 KiB
TypeScript

import graphViewJS from "assets/graph-view.txt.js";
import graphWASMJS from "assets/graph-wasm.txt.js";
import renderWorkerJS from "assets/graph-render-worker.txt.js";
import graphWASM from "assets/graph-wasm.wasm";
import websiteJS from "assets/website.txt.js";
import webpageStyles from "assets/plugin-styles.txt.css";
import deferredJS from "assets/deferred.txt.js";
import deferredCSS from "assets/deferred.txt.css";
import themeLoadJS from "assets/theme-load.txt.js";
import tinyColorJS from "assets/tinycolor.txt.js";
import pixiJS from "assets/pixi.txt.js";
import minisearchJS from "assets/minisearch.txt.js";
import { Path } from "scripts/utils/path.js";
import { Asset, AssetType, InlinePolicy, LoadMethod, Mutability } from "./assets/asset.js";
import { ObsidianStyles } from "./assets/obsidian-styles.js";
import { OtherPluginStyles } from "./assets/other-plugin-styles.js";
import { ThemeStyles } from "./assets/theme-styles.js";
import { SnippetStyles } from "./assets/snippet-styles.js";
import { MathjaxStyles } from "./assets/mathjax-styles.js";
import { CustomHeadContent } from "./assets/custom-head-content.js";
import { Settings, SettingsPage } from "scripts/settings/settings.js";
import { GlobalVariableStyles } from "./assets/global-variable-styles.js";
import { Favicon } from "./assets/favicon.js";
import { FetchBuffer } from "./assets/local-fetch-buffer.js";
import { ExportLog } from "./render-log.js";
import { SupportedPluginStyles } from "./assets/supported-plugin-styles.js";
import { fileTypeFromBuffer } from "file-type";
import { MarkdownWebpageRendererAPIOptions } from "scripts/api-options.js";
const mime = require('mime');
export class AssetHandler
{
public static vaultPluginsPath: Path;
public static staticAssets: Asset[] = [];
public static dynamicAssets: Asset[] = [];
public static allAssets: Asset[] = [];
public static temporaryAssets: Asset[] = [];
// this path is used to generate the relative path to the images folder, likewise for the other paths
private static libraryFolder: Path;
private static mediaFolder: Path;
private static jsFolder: Path;
private static cssFolder: Path;
private static fontFolder: Path;
private static htmlFolder: Path;
public static exportOptions: MarkdownWebpageRendererAPIOptions = new MarkdownWebpageRendererAPIOptions();
public static get libraryPath(): Path
{
if (!this.libraryFolder) this.initialize();
return AssetHandler.libraryFolder.copy.makeWebStyle(this.exportOptions.webStylePaths);
}
public static get mediaPath(): Path
{
if (!this.mediaFolder) this.initialize();
return AssetHandler.mediaFolder.copy.makeWebStyle(this.exportOptions.webStylePaths);
}
public static get jsPath(): Path
{
if (!this.jsFolder) this.initialize();
return AssetHandler.jsFolder.copy.makeWebStyle(this.exportOptions.webStylePaths);
}
public static get cssPath(): Path
{
if (!this.cssFolder) this.initialize();
return AssetHandler.cssFolder.copy.makeWebStyle(this.exportOptions.webStylePaths);
}
public static get fontPath(): Path
{
if (!this.fontFolder) this.initialize();
return AssetHandler.fontFolder.copy.makeWebStyle(this.exportOptions.webStylePaths);
}
public static get htmlPath(): Path
{
if (!this.htmlFolder) this.initialize();
return AssetHandler.htmlFolder.copy.makeWebStyle(this.exportOptions.webStylePaths);
}
// styles
public static obsidianStyles: ObsidianStyles = new ObsidianStyles();
public static otherPluginStyles: OtherPluginStyles = new OtherPluginStyles();
public static themeStyles: ThemeStyles = new ThemeStyles();
public static snippetStyles: SnippetStyles = new SnippetStyles();
public static mathjaxStyles: MathjaxStyles = new MathjaxStyles();
public static globalDataStyles: GlobalVariableStyles = new GlobalVariableStyles();
public static supportedPluginStyles: SupportedPluginStyles = new SupportedPluginStyles();
public static websiteStyles: Asset = new Asset("main-styles.css", webpageStyles, AssetType.Style, InlinePolicy.AutoHead, true, Mutability.Static, LoadMethod.Async, 4);
public static deferredCSS: Asset = new Asset("deferred.css", deferredCSS, AssetType.Style, InlinePolicy.InlineHead, true, Mutability.Static, LoadMethod.Defer);
// scripts
public static websiteJS: Asset = new Asset("webpage.js", websiteJS, AssetType.Script, InlinePolicy.AutoHead, true, Mutability.Static, LoadMethod.Async);
public static graphViewJS: Asset = new Asset("graph-view.js", graphViewJS, AssetType.Script, InlinePolicy.AutoHead, true, Mutability.Static);
public static graphWASMJS: Asset = new Asset("graph-wasm.js", graphWASMJS, AssetType.Script, InlinePolicy.AutoHead, true, Mutability.Static);
public static graphWASM: Asset = new Asset("graph-wasm.wasm", Buffer.from(graphWASM), AssetType.Script, InlinePolicy.Download, false, Mutability.Static);
public static renderWorkerJS: Asset = new Asset("graph-render-worker.js", renderWorkerJS, AssetType.Script, InlinePolicy.AutoHead, true, Mutability.Static);
public static deferredJS: Asset = new Asset("deferred.js", deferredJS, AssetType.Script, InlinePolicy.InlineHead, true, Mutability.Static, LoadMethod.Defer);
public static themeLoadJS: Asset = new Asset("theme-load.js", themeLoadJS, AssetType.Script, InlinePolicy.Inline, true, Mutability.Static, LoadMethod.Defer);
public static tinyColorJS: Asset = new Asset("tinycolor.js", tinyColorJS, AssetType.Script, InlinePolicy.AutoHead, true, Mutability.Static);
public static pixiJS: Asset = new Asset("pixi.js", pixiJS, AssetType.Script, InlinePolicy.AutoHead, true, Mutability.Static, LoadMethod.Async, 100, "https://cdnjs.cloudflare.com/ajax/libs/pixi.js/7.4.0/pixi.min.js");
public static minisearchJS: Asset = new Asset("minisearch.js", minisearchJS, AssetType.Script, InlinePolicy.AutoHead, true, Mutability.Static, LoadMethod.Async, 100, "https://cdn.jsdelivr.net/npm/minisearch@6.3.0/dist/umd/index.min.js");
// other
public static favicon: Favicon = new Favicon();
public static externalLinkIcon: Asset;
public static customHeadContent: CustomHeadContent = new CustomHeadContent();
public static mainJsModTime: number = 0;
public static async initialize()
{
this.libraryFolder = new Path("lib").makeUnixStyle();
this.mediaFolder = this.libraryFolder.joinString("media").makeUnixStyle();
this.jsFolder = this.libraryFolder.joinString("scripts").makeUnixStyle();
this.cssFolder = this.libraryFolder.joinString("styles").makeUnixStyle();
this.fontFolder = this.libraryFolder.joinString("fonts").makeUnixStyle();
this.htmlFolder = this.libraryFolder.joinString("html").makeUnixStyle();
this.vaultPluginsPath = Path.vaultPath.joinString(app.vault.configDir, "plugins/").makeAbsolute();
// by default all static assets have a modified time the same as main.js
this.mainJsModTime = this.vaultPluginsPath.joinString("webpage-html-export/main.js").stat?.mtimeMs ?? 0;
this.staticAssets.forEach(asset => asset.modifiedTime = this.mainJsModTime);
this.allAssets.sort((a, b) => a.loadPriority - b.loadPriority);
let loadPromises = []
for (let asset of this.allAssets)
{
loadPromises.push(asset.load(this.exportOptions));
}
await Promise.all(loadPromises);
let graphViewJSPath = this.graphViewJS.getAssetPath();
this.graphViewJS.getHTML = () => `<script type="module" async id="graph-view-script" src="${graphViewJSPath}"></script>`;
}
public static async reloadAssets()
{
// remove all temporary assets from allAssets
this.allAssets = this.allAssets.filter(asset => asset.mutability != Mutability.Temporary);
this.temporaryAssets = [];
let i = 0;
let loadPromises = []
for (let asset of this.dynamicAssets)
{
let loadPromise = asset.load(this.exportOptions);
loadPromise.then(() =>
{
i++;
ExportLog.progress(i, this.dynamicAssets.length, "Initialize Export", "Loading asset: " + asset.filename, "var(--color-yellow)");
});
loadPromises.push(loadPromise);
}
await Promise.all(loadPromises);
}
public static getAssetsOfType(type: AssetType): Asset[]
{
let assets = this.allAssets.filter(asset => asset.type == type);
assets = assets.concat(this.allAssets.map(asset => asset.childAssets).flat().filter(asset => asset.type == type));
return assets;
}
public static getAssetsOfInlinePolicy(inlinePolicy: InlinePolicy): Asset[]
{
let assets = this.allAssets.filter(asset => asset.inlinePolicy == inlinePolicy);
assets = assets.concat(this.allAssets.map(asset => asset.childAssets).flat().filter(asset => asset.inlinePolicy == inlinePolicy));
return assets;
}
private static filterDownloads(downloads: Asset[], options: MarkdownWebpageRendererAPIOptions): Asset[]
{
if (!options.addGraphView || !options.addSidebars)
{
downloads = downloads.filter(asset => ![this.graphViewJS, this.graphWASMJS, this.graphWASM, this.renderWorkerJS, this.tinyColorJS, this.pixiJS].includes(asset));
}
if (!options.addSearch || !options.addSidebars)
{
downloads = downloads.filter(asset => ![this.minisearchJS].includes(asset));
}
if (!options.includeCSS)
{
downloads = downloads.filter(asset => asset.type != AssetType.Style);
}
if (!options.includeJS)
{
downloads = downloads.filter(asset => asset.type != AssetType.Script);
}
// remove duplicates
downloads = downloads.filter((asset, index, self) => self.findIndex((t) => t.relativePath.asString == asset.relativePath.asString) === index);
// remove assets with no content
downloads = downloads.filter(asset => asset.content && asset.content.length > 0);
return downloads;
}
public static getDownloads(options: MarkdownWebpageRendererAPIOptions): Asset[]
{
let downloads = this.getAssetsOfInlinePolicy(InlinePolicy.Download)
.concat(this.getAssetsOfInlinePolicy(InlinePolicy.DownloadHead));
if (!options.inlineMedia)
{
downloads = downloads.concat(this.getAssetsOfInlinePolicy(InlinePolicy.Auto));
downloads = downloads.concat(this.getAssetsOfInlinePolicy(InlinePolicy.AutoHead));
}
downloads = this.filterDownloads(downloads, options);
downloads.sort((a, b) => b.loadPriority - a.loadPriority);
return downloads;
}
public static getHeadReferences(options: MarkdownWebpageRendererAPIOptions): string
{
let head = "";
let referenceAssets = this.getAssetsOfInlinePolicy(InlinePolicy.DownloadHead)
.concat(this.getAssetsOfInlinePolicy(InlinePolicy.AutoHead))
.concat(this.getAssetsOfInlinePolicy(InlinePolicy.InlineHead));
referenceAssets = this.filterDownloads(referenceAssets, options);
referenceAssets.sort((a, b) => b.loadPriority - a.loadPriority);
for (let asset of referenceAssets)
{
head += asset.getHTML(options);
}
return head;
}
/*Takes a style sheet string and creates assets from every font or image url embedded in it*/
public static async getStyleChildAssets(asset: Asset, makeBase64External: boolean = false): Promise<string>
{
if (typeof asset.content != "string") throw new Error("Asset content is not a string");
let content = asset.content.replaceAll("app://obsidian.md/", "");
let urls = Array.from(content.matchAll(/url\("([^"]+)"\)|url\('([^']+)'\)/g));
// remove duplicates
urls = urls.filter((url, index, self) => self.findIndex((t) => t[0] === url[0]) === index);
// use this mutability for child assets
let promises = [];
for (let urlObj of urls)
{
let url = urlObj[1] || urlObj[2];
url = url.trim();
// we don't need to download online assets if we are not making the page offline compatible
if (!this.exportOptions.offlineResources && url.startsWith("http")) continue;
if (url == "") continue;
if (url.startsWith("data:"))
{
if (!this.exportOptions.inlineMedia && makeBase64External)
{
// decode the base64 data and create an Asset from it
// then replace the url with the relative path to the asset
function hash(str:string, seed = 0) // taken from https://stackoverflow.com/questions/7616461/generate-a-hash-from-string-in-javascript
{
let h1 = 0xdeadbeef ^ seed, h2 = 0x41c6ce57 ^ seed;
for(let i = 0, ch; i < str.length; i++) {
ch = str.charCodeAt(i);
h1 = Math.imul(h1 ^ ch, 2654435761);
h2 = Math.imul(h2 ^ ch, 1597334677);
}
h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507);
h1 ^= Math.imul(h2 ^ (h2 >>> 13), 3266489909);
h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507);
h2 ^= Math.imul(h1 ^ (h1 >>> 13), 3266489909);
return 4294967296 * (2097151 & h2) + (h1 >>> 0);
}
let splitData = url.split(",")
let data = splitData.slice(1).join(",");
let extension = Asset.mimeToExtention(splitData[0].split(":")[1].split(";")[0]);
let buffer = Buffer.from(data, "base64");
let dataHash = hash(data);
let filename = `${dataHash}.${extension}`;
if (extension == '')
{
let type = await fileTypeFromBuffer(buffer);
if (type) extension = type.ext;
filename = `${dataHash}.${extension}`;
}
let type = Asset.extentionToType(extension);
let childAsset = new Asset(filename, buffer, type, InlinePolicy.Download, false, Mutability.Child);
asset.childAssets.push(childAsset);
let loadPromise = childAsset.load(this.exportOptions);
promises.push(loadPromise);
loadPromise.then(() =>
{
if (childAsset.content == undefined || childAsset.content == null || childAsset.content.length == 0)
{
return;
}
let newPath = childAsset.getAssetPath(asset.getAssetPath());
content = content.replaceAll(url, newPath.asString);
});
}
continue;
}
let path = new Path(url);
let type = Asset.extentionToType(path.extension);
let childAsset = new FetchBuffer(path.fullName, url, type, InlinePolicy.Download, false, Mutability.Child);
asset.childAssets.push(childAsset);
let loadPromise = childAsset.load(this.exportOptions);
promises.push(loadPromise);
loadPromise.then(() =>
{
if (childAsset.content == undefined || childAsset.content == null || childAsset.content.length == 0)
{
return;
}
if (this.exportOptions.inlineMedia)
{
let base64 = childAsset.content.toString("base64");
content = content.replaceAll(url, `data:${mime.getType(url)};base64,${base64}`);
}
else
{
childAsset.relativeDirectory.makeWebStyle(this.exportOptions.webStylePaths);
if (this.exportOptions.webStylePaths) childAsset.filename = Path.toWebStyle(childAsset.filename);
let newPath = childAsset.getAssetPath(asset.getAssetPath());
content = content.replaceAll(url, newPath.asString);
}
});
}
await Promise.all(promises);
return content;
}
}