Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding Ability to export diagrams - Issue #55 #56

Merged
merged 1 commit into from
Mar 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
182 changes: 148 additions & 34 deletions src/debouncedProcessors.ts
Original file line number Diff line number Diff line change
@@ -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 {

Expand Down Expand Up @@ -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
Expand All @@ -60,6 +61,7 @@ export class DebouncedProcessors implements Processor {
await navigator.clipboard.writeText(originalSource);
})
})

.addItem(item => {
item
.setTitle('Copy diagram')
Expand All @@ -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);
Expand All @@ -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<void>) => {
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);
}
}
}
16 changes: 15 additions & 1 deletion src/settings.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -10,6 +10,7 @@ export interface PlantUMLSettings {
dotPath: string;
defaultProcessor: string;
cache: number;
exportPath: string;
}

export const DEFAULT_SETTINGS: PlantUMLSettings = {
Expand All @@ -21,6 +22,7 @@ export const DEFAULT_SETTINGS: PlantUMLSettings = {
dotPath: 'dot',
defaultProcessor: "png",
cache: 60,
exportPath: ''
}

export class PlantUMLSettingsTab extends PluginSettingTab {
Expand Down Expand Up @@ -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)
Expand Down
Loading