diff --git a/src/debouncedProcessors.ts b/src/debouncedProcessors.ts index a6d244e..b68731d 100644 --- a/src/debouncedProcessors.ts +++ b/src/debouncedProcessors.ts @@ -1,7 +1,7 @@ -import {debounce, Debouncer, MarkdownPostProcessorContext, Menu, Notice, requestUrl} from "obsidian"; -import {v4 as uuidv4} from "uuid"; +import { debounce, Debouncer, MarkdownPostProcessorContext, Menu, Notice, TFile } from "obsidian"; +import { v4 as uuidv4 } from "uuid"; import PlantumlPlugin from "./main"; -import {Processor} from "./processor"; +import { Processor } from "./processor"; export class DebouncedProcessors implements Processor { @@ -51,6 +51,7 @@ export class DebouncedProcessors implements Processor { source = this.plugin.settings.header + "\r\n" + source; await processor(source, el, ctx); el.addEventListener('contextmenu', (event) => { + const menu = new Menu(this.plugin.app) .addItem(item => { item @@ -60,6 +61,7 @@ export class DebouncedProcessors implements Processor { await navigator.clipboard.writeText(originalSource); }) }) + .addItem(item => { item .setTitle('Copy diagram') @@ -68,37 +70,19 @@ export class DebouncedProcessors implements Processor { console.log(el); const img = el.querySelector('img'); if (img) { - const image = new Image(); - image.crossOrigin = 'anonymous'; - image.src = img.src; - image.addEventListener('load', () => { - const canvas = document.createElement('canvas'); - canvas.width = image.width; - canvas.height = image.height; - const ctx = canvas.getContext('2d'); - ctx.fillStyle = '#fff'; - ctx.fillRect(0, 0, canvas.width, canvas.height); - ctx.drawImage(image, 0, 0); - try { - canvas.toBlob(async (blob) => { - try { - await navigator.clipboard.write([ - new ClipboardItem({ - "image/png": blob - }) - ]); - new Notice('Diagram copied to clipboard'); - } catch (error) { - new Notice('An error occurred while copying image to clipboard'); - console.error(error); - } - }); - } catch (error) { - new Notice('An error occurred while copying image to clipboard'); - console.error(error); - } - }); + this.renderToBlob( + img, + 'An error occurred while copying image to clipboard', + async (blob) => { + await navigator.clipboard.write([ + new ClipboardItem({ + "image/png": blob + }) + ]); + new Notice('Diagram copied to clipboard'); + }); } + const svg = el.querySelector('svg'); if (svg) { await navigator.clipboard.writeText(svg.outerHTML); @@ -110,10 +94,140 @@ export class DebouncedProcessors implements Processor { new Notice('Diagram copied to clipboard'); } }); + }) + .addItem(item => { + item + .setTitle('Export diagram') + .setIcon('image-file') + .onClick(async () => { + const img = el.querySelector('img'); + + if (img) { + this.renderToBlob(img, 'An error occurred while exporting the diagram', async (blob) => { + const filename = await this.getFilePath(source, ctx, 'png'); + const buffer = await blob.arrayBuffer(); + const file = this.getFile(filename); + if (file) { + await this.plugin.app.vault.modifyBinary(file, buffer); + } else { + await this.plugin.app.vault.createBinary(filename, buffer); + } + + new Notice(`Diagram exported to '${filename}'`); + }); + } + + const svg = el.querySelector('svg'); + if (svg) { + await this.saveTextFile(source, ctx, 'svg', svg.outerHTML); + } + + const code = el.querySelector('code'); + if (code) { + await this.saveTextFile(source, ctx, 'txt', code.innerText); + } + }) }); menu.showAtMouseEvent(event); }) } } -} + renderToBlob = (img: HTMLImageElement, errorMessage: string, handleBlob: (blob: Blob) => Promise) => { + const image = new Image(); + image.crossOrigin = 'anonymous'; + image.src = img.src; + image.addEventListener('load', () => { + const canvas = document.createElement('canvas'); + canvas.width = image.width; + canvas.height = image.height; + const ctx = canvas.getContext('2d'); + ctx.fillStyle = '#fff'; + ctx.fillRect(0, 0, canvas.width, canvas.height); + ctx.drawImage(image, 0, 0); + try { + canvas.toBlob(async (blob) => { + try { + await handleBlob(blob); + } catch (error) { + new Notice(errorMessage); + console.error(error); + } + }); + } catch (error) { + new Notice(errorMessage); + console.error(error); + } + }); + } + + getFilename = (source: string, ctx: MarkdownPostProcessorContext) => { + // try extract the title of the diagram + const startuml = source.match(/@startuml (.+)/i); + if (startuml?.length >= 2) { + return `${startuml[1].trim()}`; + } + + const now = (new Date()).toISOString().replace(/[:T]+/g, '-'); + const filename = this.plugin.app.vault.getAbstractFileByPath(ctx.sourcePath).name; + return `${filename.substring(0, filename.lastIndexOf('.'))}-${now.substring(0, now.lastIndexOf('.'))}`; + } + + getFolder = async (ctx: MarkdownPostProcessorContext) => { + let exportPath = this.plugin.settings.exportPath; + if (!exportPath.startsWith('/')) { + // relative to the document + const documentPath = this.plugin.app.vault.getAbstractFileByPath(ctx.sourcePath).parent; + exportPath = `${documentPath.path}/${exportPath}`; + } + + const exists = await this.plugin.app.vault.adapter.exists(exportPath); + if (!exists) { + this.plugin.app.vault.createFolder(exportPath); + } + + return exportPath; + } + + getFilePath = async (source: string, ctx: MarkdownPostProcessorContext, type: string) => { + + const filename = this.getFilename(source, ctx); + const path = await this.getFolder(ctx); + + return `${path}${filename}.${type}`; + } + + getFile = (fileName: string) => { + + let fName = fileName; + if (fName.startsWith('/')) { + fName = fName.substring(1); + } + + const folderOrFile = this.plugin.app.vault.getAbstractFileByPath(fName); + + if (folderOrFile instanceof TFile) { + return folderOrFile; + } + + return undefined; + } + + saveTextFile = async (source: string, ctx: MarkdownPostProcessorContext, type: string, data: string) => { + try { + const filename = await this.getFilePath(source, ctx, type); + const file = this.getFile(filename); + + if (file) { + await this.plugin.app.vault.modify(file, data); + } else { + await this.plugin.app.vault.create(filename, data); + } + + new Notice(`Diagram exported to '${filename}'`); + } catch (error) { + new Notice('An error occurred while while exporting the diagram'); + console.error(error); + } + } +} \ No newline at end of file diff --git a/src/settings.ts b/src/settings.ts index 57e4015..d195f9e 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -1,4 +1,4 @@ -import {Notice, Platform, PluginSettingTab, Setting} from "obsidian"; +import { Notice, Platform, PluginSettingTab, Setting } from "obsidian"; import PlantumlPlugin from "./main"; export interface PlantUMLSettings { @@ -10,6 +10,7 @@ export interface PlantUMLSettings { dotPath: string; defaultProcessor: string; cache: number; + exportPath: string; } export const DEFAULT_SETTINGS: PlantUMLSettings = { @@ -21,6 +22,7 @@ export const DEFAULT_SETTINGS: PlantUMLSettings = { dotPath: 'dot', defaultProcessor: "png", cache: 60, + exportPath: '' } export class PlantUMLSettingsTab extends PluginSettingTab { @@ -92,6 +94,18 @@ export class PlantUMLSettingsTab extends PluginSettingTab { } ) ); + + new Setting(containerEl) + .setName("Diagram export path") + .setDesc("Path where exported diagrams will be saved relative to the vault root. Leave blank to save along side the note.") + .addText(text => text.setPlaceholder(DEFAULT_SETTINGS.exportPath) + .setValue(this.plugin.settings.exportPath) + .onChange(async (value) => { + this.plugin.settings.exportPath = value; + await this.plugin.saveSettings(); + } + ) + ); } new Setting(containerEl)