Skip to content
Open
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
7 changes: 7 additions & 0 deletions .prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"semi": true,
"singleQuote": true,
"trailingComma": "es5",
"printWidth": 100,
"tabWidth": 4
}
4 changes: 2 additions & 2 deletions manifest.json
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
{
"id": "obsidian-graphviz",
"name": "Obsidian Graphviz",
"version": "1.0.5",
"version": "1.0.6",
"minAppVersion": "0.11.5",
"description": "Render Graphviz Diagrams",
"author": "Feng Peng",
"author": "Feng Peng, Seth Miers",
"authorUrl": "https://QAMichaelPeng.github.io",
"isDesktopOnly": true
}
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"eslint": "7.20.0",
"eslint-plugin-json": "2.1.2",
"obsidian": "^0.15.9",
"prettier": "^3.6.2",
"tmp": "0.2.1",
"tslib": "2.3.1",
"typescript": "4.4.4"
Expand Down
124 changes: 75 additions & 49 deletions src/main.ts
Original file line number Diff line number Diff line change
@@ -1,56 +1,82 @@
import { Plugin } from 'obsidian';
import { Plugin, MarkdownPostProcessorContext } from 'obsidian';
import { DEFAULT_SETTINGS, GraphvizSettings, GraphvizSettingsTab } from './setting';
import { Processors } from './processors';
import { Suggesters } from './suggesters';

// Remember to rename these classes and interfaces!
export default class GraphvizPlugin extends Plugin {
settings: GraphvizSettings;

registeredProcessors: (() => void)[] = [];

export default class GraphvizPlugin extends Plugin {
settings: GraphvizSettings;

async onload() {
console.debug('Load graphviz plugin');
await this.loadSettings();
this.addSettingTab(new GraphvizSettingsTab(this));
const processors = new Processors(this);
const suggesters = new Suggesters(this);
const d3Sources = ['https://d3js.org/d3.v5.min.js',
'https://unpkg.com/@hpcc-js/wasm@0.3.11/dist/index.min.js',
'https://unpkg.com/d3-graphviz@3.0.5/build/d3-graphviz.js'];


this.app.workspace.onLayoutReady(() => {
switch (this.settings.renderer) {
case 'd3_graphviz':
for (const src of d3Sources) {
const script = document.createElement('script');
script.src = src;
(document.head || document.documentElement).appendChild(script);
}
this.registerMarkdownCodeBlockProcessor('dot', processors.d3graphvizProcessor.bind(processors));
break;
default:
this.registerMarkdownCodeBlockProcessor('dot', processors.imageProcessor.bind(processors));
}

this.registerEditorSuggest(new Suggesters(this.app, this));
});
}



onunload() {
console.debug('Unload graphviz plugin');
}

async loadSettings(): Promise<void> {
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
return Promise.resolve();
}


async saveSettings() {
await this.saveData(this.settings);
}
async onload() {
console.debug('Load graphviz plugin');
await this.loadSettings();
this.addSettingTab(new GraphvizSettingsTab(this));

const d3Sources = [
'https://d3js.org/d3.v5.min.js',
'https://unpkg.com/@hpcc-js/wasm@0.3.11/dist/index.min.js',
'https://unpkg.com/d3-graphviz@3.0.5/build/d3-graphviz.js',
];

this.app.workspace.onLayoutReady(() => {
for (const src of d3Sources) {
const script = document.createElement('script');
script.src = src;
(document.head || document.documentElement).appendChild(script);
}

this.reloadProcessors();

this.registerEditorSuggest(new Suggesters(this.app, this));
});
}

reloadProcessors() {
// unregister old processors
this.registeredProcessors.forEach((unreg) => unreg());
this.registeredProcessors = [];

const processors = new Processors(this);
const targetLang = this.settings.codeblockLanguage;

// Wrap the processor to check both lang and label
const wrappedProcessor = async (source: string, el: HTMLElement, ctx: MarkdownPostProcessorContext) => {
const section = ctx.getSectionInfo(el);
if (!section) return;

const firstLine = section.text.split('\n')[section.lineStart];
const match = firstLine.match(/^```(?<lang>[^\s^{]+)?\s*(?:{(?<label>[^}]+)})?/);

if (!match) return;

const lang = match.groups?.lang ?? '';
const label = match.groups?.label ?? '';

if (lang === targetLang || label === targetLang) {
if (this.settings.renderer === 'd3_graphviz') {
await processors.d3graphvizProcessor(source, el, ctx);
} else {
await processors.imageProcessor(source, el, ctx);
}
}
};

// Register a "catch-all" codeblock processor
const unreg = this.registerMarkdownCodeBlockProcessor(targetLang, wrappedProcessor);
this.registeredProcessors.push(unreg);
}

onunload() {
console.debug('Unload graphviz plugin');
}

async loadSettings(): Promise<void> {
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
return Promise.resolve();
}

async saveSettings() {
await this.saveData(this.settings);
}
}
213 changes: 115 additions & 98 deletions src/processors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,116 +7,133 @@ import GraphvizPlugin from './main';
// import {graphviz} from 'd3-graphviz'; => does not work, ideas how to embed d3 into the plugin?

export class Processors {
plugin: GraphvizPlugin;
plugin: GraphvizPlugin;

constructor(plugin: GraphvizPlugin) {
this.plugin = plugin;
}
imageMimeType = new Map<string, string>([
constructor(plugin: GraphvizPlugin) {
this.plugin = plugin;
}

imageMimeType = new Map<string, string>([
['png', 'image/png'],
['svg', 'image/svg+xml']
['svg', 'image/svg+xml'],
]);

private async writeDotFile(sourceFile: string): Promise<Uint8Array> {
return new Promise<Uint8Array>((resolve, reject) => {
const cmdPath = this.plugin.settings.dotPath;
const imageFormat = this.plugin.settings.imageFormat;
const parameters = [ `-T${imageFormat}`, `-Gbgcolor=transparent`, `-Gstylesheet=obs-gviz.css`, sourceFile ];
private async writeDotFile(sourceFile: string): Promise<Uint8Array> {
return new Promise<Uint8Array>((resolve, reject) => {
const cmdPath = this.plugin.settings.dotPath;
const imageFormat = this.plugin.settings.imageFormat;
const parameters = [
`-T${imageFormat}`,
'-Gbgcolor=transparent',
'-Gstylesheet=obs-gviz.css',
sourceFile,
];

console.debug(`Starting dot process ${cmdPath}, ${parameters}`);
const dotProcess = spawn(cmdPath, parameters);
const outData: Array<Uint8Array> = [];
let errData = '';
console.debug(`Starting dot process ${cmdPath}, ${parameters}`);
const dotProcess = spawn(cmdPath, parameters);
const outData: Array<Uint8Array> = [];
let errData = '';

dotProcess.stdout.on('data', function (data) {
outData.push(data);
});
dotProcess.stderr.on('data', function (data) {
errData += data;
});
dotProcess.stdin.end();
dotProcess.on('exit', function (code) {
if (code !== 0) {
reject(`"${cmdPath} ${parameters}" failed, error code: ${code}, stderr: ${errData}`);
} else {
resolve(Buffer.concat(outData));
}
});
dotProcess.on('error', function (err: Error) {
reject(`"${cmdPath} ${parameters}" failed, ${err}`);
});
});
}
dotProcess.stdout.on('data', function (data) {
outData.push(data);
});
dotProcess.stderr.on('data', function (data) {
errData += data;
});
dotProcess.stdin.end();
dotProcess.on('exit', function (code) {
if (code !== 0) {
reject(
`"${cmdPath} ${parameters}" failed, error code: ${code}, stderr: ${errData}`
);
} else {
resolve(Buffer.concat(outData));
}
});
dotProcess.on('error', function (err: Error) {
reject(`"${cmdPath} ${parameters}" failed, ${err}`);
});
});
}

private async convertToImage(source: string): Promise<Uint8Array> {
const self = this;
return new Promise<Uint8Array>((resolve, reject) => {
tmp.file(function (err, tmpPath, fd, _/* cleanupCallback */) {
if (err) reject(err);
private async convertToImage(source: string): Promise<Uint8Array> {
const self = this;
return new Promise<Uint8Array>((resolve, reject) => {
tmp.file(function (err, tmpPath, fd, _ /* cleanupCallback */) {
if (err) reject(err);

fs.write(fd, source, function (err) {
if (err) {
reject(`write to ${tmpPath} error ${err}`);
return;
}
fs.close(fd,
function (err) {
if (err) {
reject(`close ${tmpPath} error ${err}`);
return;
}
return self.writeDotFile(tmpPath).then(data => resolve(data)).catch(message => reject(message));
}
);
fs.write(fd, source, function (err) {
if (err) {
reject(`write to ${tmpPath} error ${err}`);
return;
}
fs.close(fd, function (err) {
if (err) {
reject(`close ${tmpPath} error ${err}`);
return;
}
return self
.writeDotFile(tmpPath)
.then((data) => resolve(data))
.catch((message) => reject(message));
});
});
});
});
});
});
}
}

public async imageProcessor(source: string, el: HTMLElement, _: MarkdownPostProcessorContext): Promise<void> {
const stringBeforeBrace = source.split("{", 1)[0]?.trim() || "";
const wordsBeforeBrace = stringBeforeBrace.split();
public async imageProcessor(
source: string,
el: HTMLElement,
_: MarkdownPostProcessorContext
): Promise<void> {
const stringBeforeBrace = source.split('{', 1)[0]?.trim() || '';
const wordsBeforeBrace = stringBeforeBrace.split();

try {
console.debug('Call image processor');
//make sure url is defined. once the setting gets reset to default, an empty string will be returned by settings
const imageData = await this.convertToImage(source);
const blob = new Blob([ imageData ], {'type': this.imageMimeType.get(this.plugin.settings.imageFormat)});
const url = window.URL || window.webkitURL;
const blobUrl = url.createObjectURL(blob);
const img = document.createElement('img');
img.setAttribute("class", "graphviz " + wordsBeforeBrace.join(" "));
img.setAttribute("src", blobUrl);
el.appendChild(img);
} catch (errMessage) {
console.error('convert to image error', errMessage);
const pre = document.createElement('pre');
const code = document.createElement('code');
pre.appendChild(code);
code.setText(errMessage);
el.appendChild(pre);
try {
console.debug('Call image processor');
//make sure url is defined. once the setting gets reset to default, an empty string will be returned by settings
const imageData = await this.convertToImage(source);
const blob = new Blob([imageData], {
type: this.imageMimeType.get(this.plugin.settings.imageFormat),
});
const url = window.URL || window.webkitURL;
const blobUrl = url.createObjectURL(blob);
const img = document.createElement('img');
img.setAttribute('class', 'graphviz ' + wordsBeforeBrace.join(' '));
img.setAttribute('src', blobUrl);
el.appendChild(img);
} catch (errMessage) {
console.error('convert to image error', errMessage);
const pre = document.createElement('pre');
const code = document.createElement('code');
pre.appendChild(code);
code.setText(errMessage);
el.appendChild(pre);
}
}
}

public async d3graphvizProcessor(source: string, el: HTMLElement, _: MarkdownPostProcessorContext): Promise<void> {
console.debug('Call d3graphvizProcessor');

const stringBeforeBrace = source.split("{", 1)[0]?.trim() || "";
const wordsBeforeBrace = stringBeforeBrace.split();
public async d3graphvizProcessor(
source: string,
el: HTMLElement,
_: MarkdownPostProcessorContext
): Promise<void> {
console.debug('Call d3graphvizProcessor');

const div = document.createElement('div');
const graphId = 'd3graph_' + createHash('md5').update(source).digest('hex').substring(0, 6);
div.setAttr('id', graphId);
div.setAttr('style', 'text-align: center');
div.setAttr('class', 'graphviz ' + wordsBeforeBrace.join(" "));
el.appendChild(div);
const script = document.createElement('script');
// graphviz(graphId).renderDot(source); => does not work, ideas how to use it?
// Besides, sometimes d3 is undefined, so there must be a proper way to integrate d3.
const escapedSource = source.replaceAll('\\', '\\\\').replaceAll('`','\\`');
script.text =
`if( typeof d3 != 'undefined') {
const stringBeforeBrace = source.split('{', 1)[0]?.trim() || '';
const wordsBeforeBrace = stringBeforeBrace.split();

const div = document.createElement('div');
const graphId = 'd3graph_' + createHash('md5').update(source).digest('hex').substring(0, 6);
div.setAttr('id', graphId);
div.setAttr('style', 'text-align: center');
div.setAttr('class', 'graphviz ' + wordsBeforeBrace.join(' '));
el.appendChild(div);
const script = document.createElement('script');
// graphviz(graphId).renderDot(source); => does not work, ideas how to use it?
// Besides, sometimes d3 is undefined, so there must be a proper way to integrate d3.
const escapedSource = source.replaceAll('\\', '\\\\').replaceAll('`', '\\`');
script.text = `if( typeof d3 != 'undefined') {
d3.select("#${graphId}").graphviz()
.onerror(d3error)
.renderDot(\`${escapedSource}\`);
Expand All @@ -125,6 +142,6 @@ export class Processors {
d3.select("#${graphId}").html(\`<div class="d3graphvizError"> d3.graphviz(): \`+err.toString()+\`</div>\`);
console.error('Caught error on ${graphId}: ', err);
}`;
el.appendChild(script);
}
el.appendChild(script);
}
}
Loading