From fc4fd685ba454df7fabc57a29786c8ce05ddf974 Mon Sep 17 00:00:00 2001 From: zsviczian Date: Sat, 11 Nov 2023 07:21:14 +0100 Subject: [PATCH] publish support --- src/EmbeddedFileLoader.ts | 4 + src/ExcalidrawAutomate.ts | 5 +- src/ExcalidrawData.ts | 2 +- src/ExcalidrawView.ts | 21 ++--- src/dialogs/PublishOutOfDateFiles.ts | 125 +++++++++++++++++++++++++++ src/lang/locale/en.ts | 7 +- src/main.ts | 18 +++- src/settings.ts | 43 ++++----- src/types.d.ts | 1 + src/utils/Utils.ts | 29 ++++++- 10 files changed, 206 insertions(+), 49 deletions(-) create mode 100644 src/dialogs/PublishOutOfDateFiles.ts diff --git a/src/EmbeddedFileLoader.ts b/src/EmbeddedFileLoader.ts index d568cbe..26f0226 100644 --- a/src/EmbeddedFileLoader.ts +++ b/src/EmbeddedFileLoader.ts @@ -105,8 +105,12 @@ const replaceSVGColors = (svg: SVGSVGElement | string, colorMap: ColorMap | null for (const [oldColor, newColor] of Object.entries(colorMap)) { const fillRegex = new RegExp(`fill="${oldColor}"`, 'gi'); svg = svg.replaceAll(fillRegex, `fill="${newColor}"`); + const fillStyleRegex = new RegExp(`fill:${oldColor}`, 'gi'); + svg = svg.replaceAll(fillStyleRegex, `fill:${newColor}`); const strokeRegex = new RegExp(`stroke="${oldColor}"`, 'gi'); svg = svg.replaceAll(strokeRegex, `stroke="${newColor}"`); + const strokeStyleRegex = new RegExp(`stroke:${oldColor}`, 'gi'); + svg = svg.replaceAll(strokeStyleRegex, `stroke:${newColor}`); } return svg; } diff --git a/src/ExcalidrawAutomate.ts b/src/ExcalidrawAutomate.ts index b0fd3ac..2e66132 100644 --- a/src/ExcalidrawAutomate.ts +++ b/src/ExcalidrawAutomate.ts @@ -2599,12 +2599,12 @@ export async function createPNG( ); } -const updateElementLinksToObsidianLinks = ({elements, hostFile}:{ +export const updateElementLinksToObsidianLinks = ({elements, hostFile}:{ elements: ExcalidrawElement[]; hostFile: TFile; }): ExcalidrawElement[] => { return elements.map((el)=>{ - if(el.type!=="embeddable" && el.link && el.link.startsWith("[")) { + if(el.link && el.link.startsWith("[")) { const partsArray = REGEX_LINK.getResList(el.link)[0]; if(!partsArray?.value) return el; let linkText = REGEX_LINK.getLink(partsArray); @@ -2692,6 +2692,7 @@ export async function createSVG( withTheme, }, padding, + null, ); if (withTheme && theme === "dark") addFilterToForeignObjects(svg); diff --git a/src/ExcalidrawData.ts b/src/ExcalidrawData.ts index 85a41ad..7281c6b 100644 --- a/src/ExcalidrawData.ts +++ b/src/ExcalidrawData.ts @@ -1149,7 +1149,7 @@ export class ExcalidrawData { } else { //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/829 const path = ef.file - ? ef.linkParts.original.replace(PATHREG,app.metadataCache.fileToLinktext(ef.file,this.file.path)) + ? ef.linkParts.original.replace(PATHREG,this.app.metadataCache.fileToLinktext(ef.file,this.file.path)) : ef.linkParts.original; const colorMap = ef.colorMap ? " " + JSON.stringify(ef.colorMap) : ""; outString += `${key}: [[${path}]]${colorMap}\n`; diff --git a/src/ExcalidrawView.ts b/src/ExcalidrawView.ts index 9808d98..b8675d1 100644 --- a/src/ExcalidrawView.ts +++ b/src/ExcalidrawView.ts @@ -373,9 +373,9 @@ export default class ExcalidrawView extends TextFileView { if (!this.getScene || !this.file) { return; } - if (app.isMobile) { + if (this.app.isMobile) { const prompt = new Prompt( - app, + this.app, "Please provide filename", this.file.basename, "filename, leave blank to cancel action", @@ -388,11 +388,11 @@ export default class ExcalidrawView extends TextFileView { const folderpath = splitFolderAndFilename(this.file.path).folderpath; await checkAndCreateFolder(folderpath); //create folder if it does not exist const fname = getNewUniqueFilepath( - app.vault, + this.app.vault, filename, folderpath, ); - app.vault.create( + this.app.vault.create( fname, JSON.stringify(this.getScene(), null, "\t"), ); @@ -430,6 +430,7 @@ export default class ExcalidrawView extends TextFileView { }, exportSettings, ed ? ed.padding : getExportPadding(this.plugin, this.file), + this.file, ); } @@ -442,7 +443,7 @@ export default class ExcalidrawView extends TextFileView { } const exportImage = async (filepath:string, theme?:string) => { - const file = app.vault.getAbstractFileByPath(normalizePath(filepath)); + const file = this.app.vault.getAbstractFileByPath(normalizePath(filepath)); const svg = await this.svg(scene,theme, embedScene); if (!svg) { @@ -453,9 +454,9 @@ export default class ExcalidrawView extends TextFileView { embedFontsInSVG(svg, this.plugin), ); if (file && file instanceof TFile) { - await app.vault.modify(file, svgString); + await this.app.vault.modify(file, svgString); } else { - await app.vault.create(filepath, svgString); + await this.app.vault.create(filepath, svgString); } } @@ -519,16 +520,16 @@ export default class ExcalidrawView extends TextFileView { } const exportImage = async (filepath:string, theme?:string) => { - const file = app.vault.getAbstractFileByPath(normalizePath(filepath)); + const file = this.app.vault.getAbstractFileByPath(normalizePath(filepath)); const png = await this.png(scene, theme, embedScene); if (!png) { return; } if (file && file instanceof TFile) { - await app.vault.modifyBinary(file, await png.arrayBuffer()); + await this.app.vault.modifyBinary(file, await png.arrayBuffer()); } else { - await app.vault.createBinary(filepath, await png.arrayBuffer()); + await this.app.vault.createBinary(filepath, await png.arrayBuffer()); } } diff --git a/src/dialogs/PublishOutOfDateFiles.ts b/src/dialogs/PublishOutOfDateFiles.ts new file mode 100644 index 0000000..0a23ebd --- /dev/null +++ b/src/dialogs/PublishOutOfDateFiles.ts @@ -0,0 +1,125 @@ +import { Modal, Setting, TFile } from "obsidian"; +import ExcalidrawPlugin from "src/main"; +import { getIMGFilename } from "src/utils/FileUtils"; +import { addIframe } from "src/utils/Utils"; + +const haveLinkedFilesChanged = (depth: number, mtime: number, path: string, sourceList: Set, plugin: ExcalidrawPlugin):boolean => { + if(depth++ > 5) return false; + sourceList.add(path); + const links = plugin.app.metadataCache.resolvedLinks[path]; + if(!links) return false; + for(const link of Object.keys(links)) { + if(sourceList.has(link)) continue; + const file = plugin.app.vault.getAbstractFileByPath(link); + if(!file || !(file instanceof TFile)) continue; + console.log(path, {mtimeLinked: file.stat.mtime, mtimeSource: mtime, path: file.path}); + if(file.stat.mtime > mtime) return true; + if(plugin.isExcalidrawFile(file)) { + if(haveLinkedFilesChanged(depth, mtime, file.path, sourceList, plugin)) return true; + } + } + return false; +} + +const listOfOutOfSyncSVGExports = async(plugin: ExcalidrawPlugin, recursive: boolean):Promise => { + const app = plugin.app; + + const publish = app.internalPlugins.plugins["publish"].instance; + if(!publish) return; + const list = await app.internalPlugins.plugins["publish"].instance.apiList(); + if(!list || !list.files) return; + const outOfSyncFiles = new Set(); + list.files.filter((f:any)=>f.path.endsWith(".svg")).forEach((f:any)=>{ + const maybeExcalidraFilePath = getIMGFilename(f.path,"md"); + const svgFile = app.vault.getAbstractFileByPath(f.path); + const excalidrawFile = app.vault.getAbstractFileByPath(maybeExcalidraFilePath); + if(!excalidrawFile || !svgFile || !(excalidrawFile instanceof TFile) || !(svgFile instanceof TFile)) return; + console.log(excalidrawFile, {mtimeEx: excalidrawFile.stat.mtime, mtimeSVG: svgFile.stat.mtime}); + if(excalidrawFile.stat.mtime <= svgFile.stat.mtime) { + if(!recursive) return; + if(!haveLinkedFilesChanged(0, excalidrawFile.stat.mtime, excalidrawFile.path, new Set(), plugin)) return; + } + outOfSyncFiles.add(excalidrawFile); + }); + return Array.from(outOfSyncFiles); +} + +export class PublishOutOfDateFilesDialog extends Modal { + constructor( + private plugin: ExcalidrawPlugin, + ) { + super(plugin.app); + } + + async onClose() {} + + onOpen() { + this.containerEl.classList.add("excalidraw-release"); + this.titleEl.setText(`Out of Date SVG Files`); + this.createForm(false); + } + + async createForm(recursive: boolean) { + const detailsEl = this.contentEl.createEl("details"); + detailsEl.createEl("summary", { + text: "Video about Obsidian Publish support", + }); + addIframe(detailsEl, "OX5_UYjXEvc"); + + const p = this.contentEl.createEl("p",{text: "Collecting data..."}); + const files = await listOfOutOfSyncSVGExports(this.plugin, recursive); + + if(!files || files.length === 0) { + p.innerText = "No out of date files found."; + const div = this.contentEl.createDiv({cls: "excalidraw-prompt-buttons-div"}); + const bClose = div.createEl("button", { text: "Close", cls: "excalidraw-prompt-button"}); + bClose.onclick = () => { + this.close(); + }; + if(!recursive) { + const bRecursive = div.createEl("button", { text: "Check Recursive", cls: "excalidraw-prompt-button"}); + bRecursive.onclick = () => { + this.contentEl.empty(); + this.createForm(true); + }; + } + return; + } + + const filesMap = new Map(); + p.innerText = "Select files to open."; + files.forEach((f:TFile) => { + filesMap.set(f,true); + new Setting(this.contentEl) + .setName(f.path) + .addToggle(toggle => toggle + .setValue(true) + .onChange(value => { + filesMap.set(f,value); + }) + ) + }); + + const div = this.contentEl.createDiv({cls: "excalidraw-prompt-buttons-div"}); + const bClose = div.createEl("button", { text: "Close", cls: "excalidraw-prompt-button"}); + bClose.onclick = () => { + this.close(); + }; + if(!recursive) { + const bRecursive = div.createEl("button", { text: "Check Recursive", cls: "excalidraw-prompt-button"}); + bRecursive.onclick = () => { + this.contentEl.empty(); + this.createForm(true); + }; + } + const bOpen = div.createEl("button", { text: "Open Selected", cls: "excalidraw-prompt-button" }); + bOpen.onclick = () => { + filesMap.forEach((value:boolean,key:TFile) => { + if(value) { + this.plugin.openDrawing(key,"new-tab",true); + } + }); + this.close(); + }; + } +} diff --git a/src/lang/locale/en.ts b/src/lang/locale/en.ts index 0179a1a..82976a6 100644 --- a/src/lang/locale/en.ts +++ b/src/lang/locale/en.ts @@ -9,6 +9,7 @@ import { labelALT, labelCTRL, labelMETA, labelSHIFT } from "src/utils/Modifierke // English export default { // main.ts + PUBLISH_SVG_CHECK: "Obsidian Publish: Find SVGs that are out of date", INSTALL_SCRIPT: "Install the script", UPDATE_SCRIPT: "Update available - Click to install", CHECKING_SCRIPT: @@ -434,11 +435,7 @@ FILENAME_HEAD: "Filename", "Embed the .svg file into your documents instead of Excalidraw making you embeds platform independent. " + "While the auto-export switch is on, this file will get updated every time you edit the Excalidraw drawing with the matching name. " + "You can override this setting on a file level by adding the excalidraw-autoexport frontmatter key. Valid values for this key are " + - "none,all,svg, png, svg.md. For backwards compatibility both also works and will export both SVG and PNG files.", - EXPORT_SVG_MD_NAME: "Auto-export SVG as a markdown file", - EXPORT_SVG_MD_DESC: - "Similar to autoexport SVG. This is a hack to Automatically create an SVG export of your drawings. Filename will be <>.svg.md" + - "Embed the .svg.md file into your documents instead of Excalidraw and your drawings will show up in Obsidian publish with working links and embedded videos.", + "none,both,svg, and png.", EXPORT_PNG_NAME: "Auto-export PNG", EXPORT_PNG_DESC: "Same as the auto-export SVG, but for *.PNG", EXPORT_BOTH_DARK_AND_LIGHT_NAME: "Export both dark- and light-themed image", diff --git a/src/main.ts b/src/main.ts index a5fe3cd..9be8099 100644 --- a/src/main.ts +++ b/src/main.ts @@ -116,6 +116,7 @@ import { imageCache } from "./utils/ImageCache"; import { StylesManager } from "./utils/StylesManager"; import { MATHJAX_SOURCE_LZCOMPRESSED } from "./constMathJaxSource"; import { getEA } from "src"; +import { PublishOutOfDateFilesDialog } from "./dialogs/PublishOutOfDateFiles"; declare const EXCALIDRAW_PACKAGES:string; declare const react:any; @@ -837,6 +838,21 @@ export default class ExcalidrawPlugin extends Plugin { ), ); + this.addCommand({ + id: "excalidraw-publish-svg-check", + name: t("PUBLISH_SVG_CHECK"), + checkCallback: (checking: boolean) => { + const publish = app.internalPlugins.plugins["publish"].instance; + if (!publish) { + return false; + } + if (checking) { + return true; + } + (new PublishOutOfDateFilesDialog(this)).open(); + } + }) + this.addCommand({ id: "excalidraw-disable-autosave", name: t("TEMPORARY_DISABLE_AUTOSAVE"), @@ -1966,7 +1982,7 @@ export default class ExcalidrawPlugin extends Plugin { } //close excalidraw view where this file is open - const leaves = app.workspace.getLeavesOfType(VIEW_TYPE_EXCALIDRAW); + const leaves = this.app.workspace.getLeavesOfType(VIEW_TYPE_EXCALIDRAW); for (let i = 0; i < leaves.length; i++) { if ((leaves[i].view as ExcalidrawView).file.path == file.path) { await leaves[i].setViewState({ diff --git a/src/settings.ts b/src/settings.ts index 73efaf4..896ec17 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -21,6 +21,7 @@ import { } from "./utils/FileUtils"; import { PENS } from "./utils/Pens"; import { + addIframe, fragWithHTML, setLeftHandedMode, } from "./utils/Utils"; @@ -346,20 +347,6 @@ export class ExcalidrawSettingTab extends PluginSettingTab { async display() { let detailsEl: HTMLElement; - const addIframe = (link:string, startAt?: number) => { - const wrapper = detailsEl.createDiv({cls: "excalidraw-videoWrapper settings"}) - wrapper.createEl("iframe", { - attr: { - allowfullscreen: true, - allow: "encrypted-media;picture-in-picture", - frameborder: "0", - title: "YouTube video player", - src: "https://www.youtube.com/embed/" + link + (startAt ? "?start=" + startAt : ""), - sandbox: "allow-forms allow-presentation allow-same-origin allow-scripts allow-modals", - }, - }); - - } await this.plugin.loadSettings(); //in case sync loaded changed settings in the background this.requestEmbedUpdate = false; this.requestReloadDrawings = false; @@ -450,7 +437,7 @@ export class ExcalidrawSettingTab extends PluginSettingTab { this.applySettingsUpdate(); }), ); - addIframe("jgUpYznHP9A",216); + addIframe(detailsEl, "jgUpYznHP9A",216); new Setting(detailsEl) .setName(t("SCRIPT_FOLDER_NAME")) @@ -678,7 +665,7 @@ export class ExcalidrawSettingTab extends PluginSettingTab { this.applySettingsUpdate(); }), ); - addIframe("H8Njp7ZXYag",999); + addIframe(detailsEl, "H8Njp7ZXYag",999); detailsEl = displayDetailsEl.createEl("details"); detailsEl.createEl("summary", { @@ -701,7 +688,7 @@ export class ExcalidrawSettingTab extends PluginSettingTab { this.applySettingsUpdate(); }), ); - addIframe("fypDth_-8q0"); + addIframe(detailsEl, "fypDth_-8q0"); new Setting(detailsEl) .setName(t("IFRAME_MATCH_THEME_NAME")) @@ -714,7 +701,7 @@ export class ExcalidrawSettingTab extends PluginSettingTab { this.applySettingsUpdate(true); }), ); - addIframe("ICpoyMv6KSs"); + addIframe(detailsEl, "ICpoyMv6KSs"); new Setting(detailsEl) .setName(t("MATCH_THEME_NAME")) @@ -787,7 +774,7 @@ export class ExcalidrawSettingTab extends PluginSettingTab { this.applySettingsUpdate(); }), ); - addIframe("rBarRfcSxNo",107); + addIframe(detailsEl, "rBarRfcSxNo",107); new Setting(detailsEl) .setName(t("DEFAULT_WHEELZOOM_NAME")) @@ -1209,8 +1196,8 @@ export class ExcalidrawSettingTab extends PluginSettingTab { this.applySettingsUpdate(); }) ); - addIframe("yZQoJg2RCKI"); - addIframe("opLd1SqaH_I",8); + addIframe(detailsEl, "yZQoJg2RCKI"); + addIframe(detailsEl, "opLd1SqaH_I",8); let dropdown: DropdownComponent; @@ -1308,7 +1295,7 @@ export class ExcalidrawSettingTab extends PluginSettingTab { text: t("EXPORT_SUBHEAD"), cls: "excalidraw-setting-h3", }); - addIframe("wTtaXmRJ7wg",171); + addIframe(detailsEl, "wTtaXmRJ7wg",171); detailsEl = exportDetailsEl.createEl("details"); detailsEl.createEl("summary", { text: t("EMBED_SIZING"), @@ -1504,7 +1491,7 @@ export class ExcalidrawSettingTab extends PluginSettingTab { cls: "excalidraw-setting-h3", }); - addIframe("nB4cOfn0xAs"); + addIframe(detailsEl, "nB4cOfn0xAs"); new Setting(detailsEl) .setName(t("PDF_TO_IMAGE_SCALE_NAME")) .setDesc(fragWithHTML(t("PDF_TO_IMAGE_SCALE_DESC"))) @@ -1669,7 +1656,7 @@ export class ExcalidrawSettingTab extends PluginSettingTab { text: t("CUSTOM_PEN_HEAD"), cls: "excalidraw-setting-h3", }); - addIframe("OjNhjaH2KjI",69); + addIframe(detailsEl, "OjNhjaH2KjI",69); new Setting(detailsEl) .setName(t("CUSTOM_PEN_NAME")) .setDesc(t("CUSTOM_PEN_DESC")) @@ -1699,7 +1686,7 @@ export class ExcalidrawSettingTab extends PluginSettingTab { text: t("CUSTOM_FONT_HEAD"), cls: "excalidraw-setting-h3", }); - addIframe("eKFmrSQhFA4"); + addIframe(detailsEl, "eKFmrSQhFA4"); new Setting(detailsEl) .setName(t("ENABLE_FOURTH_FONT_NAME")) .setDesc(fragWithHTML(t("ENABLE_FOURTH_FONT_DESC"))) @@ -1765,7 +1752,7 @@ export class ExcalidrawSettingTab extends PluginSettingTab { }) })*/ - addIframe("r08wk-58DPk"); + addIframe(detailsEl, "r08wk-58DPk"); new Setting(detailsEl) .setName(t("LATEX_DEFAULT_NAME")) .setDesc(fragWithHTML(t("LATEX_DEFAULT_DESC"))) @@ -1837,7 +1824,7 @@ export class ExcalidrawSettingTab extends PluginSettingTab { detailsEl.createDiv( { text: t("TASKBONE_DESC"), cls: "setting-item-description" }); let taskboneAPIKeyText: TextComponent; - addIframe("7gu4ETx7zro"); + addIframe(detailsEl, "7gu4ETx7zro"); new Setting(detailsEl) .setName(t("TASKBONE_ENABLE_NAME")) .setDesc(fragWithHTML(t("TASKBONE_ENABLE_DESC"))) @@ -2090,7 +2077,7 @@ export class ExcalidrawSettingTab extends PluginSettingTab { cls: "excalidraw-setting-h1", }); - addIframe("H8Njp7ZXYag",52); + addIframe(detailsEl, "H8Njp7ZXYag",52); Object.keys(this.plugin.settings.scriptEngineSettings) .filter((s) => scripts.contains(s)) .forEach((scriptName: string) => { diff --git a/src/types.d.ts b/src/types.d.ts index 3d22a86..5944567 100644 --- a/src/types.d.ts +++ b/src/types.d.ts @@ -32,6 +32,7 @@ declare global { declare module "obsidian" { interface App { + internalPlugins: any; isMobile(): boolean; getObsidianUrl(file:TFile): string; } diff --git a/src/utils/Utils.ts b/src/utils/Utils.ts index c5b1909..384483f 100644 --- a/src/utils/Utils.ts +++ b/src/utils/Utils.ts @@ -31,6 +31,7 @@ import ExcalidrawScene from "src/svgToExcalidraw/elements/ExcalidrawScene"; import { FILENAMEPARTS } from "./UtilTypes"; import { Mutable } from "@zsviczian/excalidraw/types/utility-types"; import { cleanBlockRef, cleanSectionHeading } from "./ObsidianUtils"; +import { updateElementLinksToObsidianLinks } from "src/ExcalidrawAutomate"; declare const PLUGIN_VERSION:string; @@ -259,6 +260,7 @@ export const getSVG = async ( scene: any, exportSettings: ExportSettings, padding: number, + srcFile: TFile|null, //if set, will replace markdown links with obsidian links ): Promise => { let elements:ExcalidrawElement[] = scene.elements; if(elements.some(el => el.type === "embeddable")) { @@ -269,8 +271,13 @@ export const getSVG = async ( } try { - return await exportToSvg({ - elements, + const svg = await exportToSvg({ + elements: srcFile + ? updateElementLinksToObsidianLinks({ + elements, + hostFile: srcFile, + }) + : elements, appState: { exportBackground: exportSettings.withBackground, exportWithDarkMode: exportSettings.withTheme @@ -281,6 +288,10 @@ export const getSVG = async ( files: scene.files, exportPadding: padding, }); + if(svg) { + svg.addClass("excalidraw-svg"); + } + return svg; } catch (error) { return null; } @@ -784,3 +795,17 @@ export const convertSVGStringToElement = (svg: string): SVGSVGElement => { } export const escapeRegExp = (str:string) => str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string + +export const addIframe = (containerEl: HTMLElement, link:string, startAt?: number) => { + const wrapper = containerEl.createDiv({cls: "excalidraw-videoWrapper settings"}) + wrapper.createEl("iframe", { + attr: { + allowfullscreen: true, + allow: "encrypted-media;picture-in-picture", + frameborder: "0", + title: "YouTube video player", + src: "https://www.youtube.com/embed/" + link + (startAt ? "?start=" + startAt : ""), + sandbox: "allow-forms allow-presentation allow-same-origin allow-scripts allow-modals", + }, + }); +} \ No newline at end of file