mirror of
https://github.com/zsviczian/obsidian-excalidraw-plugin.git
synced 2025-08-06 05:46:28 +00:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
36ead43102 | ||
|
|
f61d000326 | ||
|
|
9bb254dc48 |
@@ -11,6 +11,7 @@ Please upgrade to Obsidian v0.12.19 or higher to get the latest release.
|
||||
|[](https://youtu.be/QOL1KF7-kdc)|[](https://youtu.be/aSgcbfspvfo)|[](https://youtu.be/MaJ5jJwBRWs)|
|
||||
|[](https://youtu.be/MXzeCOEExNo)|[](https://youtu.be/R0IAg0s-wQE)|[](https://youtu.be/ibdS7ykwpW4)|
|
||||
|[](https://youtu.be/VRZVujfVab0)|[](https://youtu.be/D1iBYo1_jjc)|[](https://www.youtube.com/watch?v=_c_0zpBJ4Xc&)|
|
||||
|[](https://youtu.be/r08wk-58DPk)|[](https://youtu.be/tsecSfnTMow)||
|
||||
|
||||
# Key features
|
||||
- The plugin aims to integrate Excalidraw seamlessly into Obsidian including Command Palette actions, File Explorer features, Option Menu commands, and the Ribbon Button.
|
||||
@@ -58,6 +59,14 @@ Please upgrade to Obsidian v0.12.19 or higher to get the latest release.
|
||||
- `excalidraw-link-prefix: "📍"` preview prefix for internal links
|
||||
- `excalidraw-url-prefix: "🌐"` preview prefix for external links
|
||||
- `excalidraw-link-brackets: true|false` whether or not to display brackets around links in preview
|
||||
- Embed complete markdown files into your drawings
|
||||
- Drag from the desired file from the Obsidian file explorer and hold down CTRL/CMD while dropping the file onto the canvas.
|
||||
- Use the command palette action: `Insert markdown file from vault`
|
||||
- Use custom woff, woff2 or TTF font to display the document, you can set the default font to use under Excalidraw Settings.
|
||||
- You can control appearance of the embedded markdown file on a file by file bases by adding the following front matter keys to your markdown document:
|
||||
- `excalidraw-font: Virgil|Cascadia|font_file_name.extension`
|
||||
- `excalidraw-font-color: css-color-name|#HEXcolor|any-other-html-standard-format`, you can find css color names [here](https://www.w3schools.com/colors/colors_names.asp).
|
||||
- Switch to markdown view or use CTRL/CMD+ALT/OPT click on the image to edit properties of the embed: [[filename#^blockref|WIDTHxMAXHEIGHT]]
|
||||
- Includes full [QuickAdd](https://github.com/chhoumann/quickadd), [Templater](https://silentvoid13.github.io/Templater/) and [Dataview](https://blacksmithgu.github.io/obsidian-dataview/docs/api/intro/) support through ExcalidrawAutomate. Check out the [detailed help + examples](https://zsviczian.github.io/obsidian-excalidraw-plugin/). I also have a [YouTube ExcalidrawAutomate Playlist](https://www.youtube.com/playlist?list=PL6mqgtMZ4NP1IR4nXxSlMA4PA5E-qpyHZ) with lots of examples.
|
||||
- REQUIRES AN OBSIDIAN SYNC SUBSCRIPTION: Full drawing file history and synchronization between devices
|
||||
- Multilanguage support: if you'd like to help out by translating the plugin, please get in contact with me.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"id": "obsidian-excalidraw-plugin",
|
||||
"name": "Excalidraw",
|
||||
"version": "1.4.13",
|
||||
"version": "1.4.14",
|
||||
"minAppVersion": "0.12.16",
|
||||
"description": "An Obsidian plugin to edit and view Excalidraw drawings",
|
||||
"author": "Zsolt Viczian",
|
||||
|
||||
@@ -1,16 +1,14 @@
|
||||
import { FileId } from "@zsviczian/excalidraw/types/element/types";
|
||||
import { BinaryFileData, DataURL } from "@zsviczian/excalidraw/types/types";
|
||||
import { link } from "fs";
|
||||
import { App, MarkdownRenderer, Notice, TFile } from "obsidian";
|
||||
import { CASCADIA_FONT, fileid, FRONTMATTER_KEY_CSS, FRONTMATTER_KEY_FONT, FRONTMATTER_KEY_FONTCOLOR, IMAGE_TYPES, nanoid, VIRGIL_FONT } from "./constants";
|
||||
import { CASCADIA_FONT, fileid, FRONTMATTER_KEY_FONT, FRONTMATTER_KEY_FONTCOLOR, FRONTMATTER_KEY_MD_STYLE, IMAGE_TYPES, nanoid, VIRGIL_FONT } from "./constants";
|
||||
import { createSVG } from "./ExcalidrawAutomate";
|
||||
import { ExcalidrawData, getTransclusion } from "./ExcalidrawData";
|
||||
import ExcalidrawView, { ExportSettings } from "./ExcalidrawView";
|
||||
import { ExportSettings } from "./ExcalidrawView";
|
||||
import { t } from "./lang/helpers";
|
||||
import de from "./lang/locale/de";
|
||||
import { tex2dataURL } from "./LaTeX";
|
||||
import ExcalidrawPlugin from "./main";
|
||||
import {debug, errorlog, getImageSize, getLinkParts, LinkParts, svgToBase64 } from "./Utils";
|
||||
import {errorlog, getImageSize, getLinkParts, LinkParts, svgToBase64 } from "./Utils";
|
||||
|
||||
export declare type MimeType = "image/svg+xml" | "image/png" | "image/jpeg" | "image/gif" | "application/octet-stream";
|
||||
export type FileData = BinaryFileData & {
|
||||
@@ -282,12 +280,14 @@ const getSVGData = async (app: App, file: TFile): Promise<DataURL> => {
|
||||
}
|
||||
|
||||
const convertMarkdownToSVG = async (plugin: ExcalidrawPlugin, file: TFile, linkParts: LinkParts): Promise<DataURL> => {
|
||||
|
||||
//const text = await plugin.app.vault.cachedRead(file);
|
||||
//1.
|
||||
//get the markdown text
|
||||
const [text,line] = await getTransclusion(linkParts,plugin.app,file);
|
||||
const fileCache = plugin.app.metadataCache.getFileCache(file);
|
||||
|
||||
|
||||
//2.
|
||||
//get styles
|
||||
const fileCache = plugin.app.metadataCache.getFileCache(file);
|
||||
let fontName:string;
|
||||
let fontDef:string;
|
||||
let font = plugin.settings.mdFont;
|
||||
@@ -313,31 +313,93 @@ const convertMarkdownToSVG = async (plugin: ExcalidrawPlugin, file: TFile, linkP
|
||||
}
|
||||
|
||||
const fontColor = fileCache?.frontmatter ? fileCache.frontmatter[FRONTMATTER_KEY_FONTCOLOR] : plugin.settings.mdFontColor;
|
||||
|
||||
//construct SVG
|
||||
const div = createDiv();
|
||||
div.setAttribute("xmlns","http://www.w3.org/1999/xhtml");
|
||||
div.style.fontFamily = fontName;
|
||||
if(fontColor) div.style.color = fontColor;
|
||||
div.style.fontSize = "initial";
|
||||
await MarkdownRenderer.renderMarkdown(text,div,file.path,plugin);
|
||||
div.querySelectorAll(":scope > *[class^='frontmatter']").forEach((el)=>div.removeChild(el));
|
||||
//brute force to swap <a> to <u> because links anyway don't work when the foreignObject is
|
||||
//encapsulated in an img element. <a> does not render with an underline, <u> will.
|
||||
const xml = (new XMLSerializer().serializeToString(div)).replaceAll("<a ","<u ").replaceAll("</a>","</u>");
|
||||
let svgStyle = ' width="'+linkParts.width+'px" height="100%"';
|
||||
|
||||
let style = fileCache?.frontmatter ? (fileCache.frontmatter[FRONTMATTER_KEY_MD_STYLE]??"") : "";
|
||||
let frontmatterCSSisAfile = false;
|
||||
if(style && style!="") {
|
||||
const f = plugin.app.metadataCache.getFirstLinkpathDest(style,file.path);
|
||||
if(f) {
|
||||
style = await plugin.app.vault.read(f);
|
||||
frontmatterCSSisAfile = true;
|
||||
}
|
||||
}
|
||||
if(!frontmatterCSSisAfile && plugin.settings.mdCSS && plugin.settings.mdCSS!="") {
|
||||
const f = plugin.app.metadataCache.getFirstLinkpathDest(plugin.settings.mdCSS,file.path);
|
||||
if(f) {
|
||||
style += "\n"+await plugin.app.vault.read(f);
|
||||
}
|
||||
}
|
||||
|
||||
//3.
|
||||
//SVG helper functions
|
||||
//the SVG will first have ~infinite height. After sizing this will be reduced
|
||||
let svgStyle = ' width="'+linkParts.width+'px" height="100000"';
|
||||
let foreignObjectStyle = ' width="'+linkParts.width+'px" height="100%"';
|
||||
|
||||
const svg = () => '<svg xmlns="http://www.w3.org/2000/svg"'+svgStyle+'>'
|
||||
const svg = (xml:string,style?:string) =>
|
||||
'<svg xmlns="http://www.w3.org/2000/svg"'+svgStyle+'>'
|
||||
+ (style?'<style>'+style+'</style>':'')
|
||||
+ '<foreignObject x="0" y="0"'+foreignObjectStyle+'>'
|
||||
+ xml
|
||||
+ '</foreignObject><defs><style>'
|
||||
+ fontDef
|
||||
+ '</style></defs></svg>';
|
||||
|
||||
|
||||
//4.
|
||||
//create document div - this will be the contents of the foreign object
|
||||
const mdDIV = createDiv();
|
||||
mdDIV.setAttribute("xmlns","http://www.w3.org/1999/xhtml");
|
||||
mdDIV.setAttribute("class","excalidraw-md-host");
|
||||
mdDIV.setAttribute("style",style);
|
||||
mdDIV.style.fontFamily = fontName;
|
||||
mdDIV.style.overflow = "auto";
|
||||
mdDIV.style.display = "block";
|
||||
if(fontColor) mdDIV.style.color = fontColor;
|
||||
|
||||
await MarkdownRenderer.renderMarkdown(text,mdDIV,file.path,plugin);
|
||||
mdDIV.querySelectorAll(":scope > *[class^='frontmatter']").forEach((el)=>mdDIV.removeChild(el));
|
||||
|
||||
//this is a brute force approach to replace anchors with spans for better formatting
|
||||
//mdDIV.innerHTML = mdDIV.innerHTML.replaceAll("<a ","<u ").replaceAll("</a>","</u>");
|
||||
|
||||
|
||||
//5.1
|
||||
//get SVG size.
|
||||
//First I need to create a fully self contained copy of the document to convert
|
||||
//blank styles into inline styles using computedStyle
|
||||
const iframeHost = document.body.createDiv();
|
||||
iframeHost.style.display = "none";
|
||||
const iframe = iframeHost.createEl("iframe");
|
||||
const iframeDoc = iframe.contentWindow.document;
|
||||
if(style) {
|
||||
const styleEl = iframeDoc.createElement("style");
|
||||
styleEl.type = "text/css";
|
||||
styleEl.innerHTML = style;
|
||||
iframeDoc.head.appendChild(styleEl);
|
||||
}
|
||||
const stylingDIV = iframeDoc.importNode(mdDIV,true)
|
||||
iframeDoc.body.appendChild(stylingDIV);
|
||||
|
||||
iframeDoc.body.querySelectorAll("*").forEach((el:HTMLElement)=>{
|
||||
const elementStyle = el.style;
|
||||
const computedStyle = window.getComputedStyle(el);
|
||||
let style = "";
|
||||
for (const prop in elementStyle) {
|
||||
if (elementStyle.hasOwnProperty(prop)) {
|
||||
style += prop + ": " + computedStyle[prop] + ";";
|
||||
}
|
||||
}
|
||||
el.setAttribute("style",style);
|
||||
});
|
||||
|
||||
const xmlINiframe = (new XMLSerializer().serializeToString(stylingDIV))
|
||||
document.body.removeChild(iframeHost);
|
||||
|
||||
//5.2
|
||||
//get SVG size
|
||||
const parser = new DOMParser();
|
||||
const doc = parser.parseFromString(svg(),"image/svg+xml");
|
||||
const doc = parser.parseFromString(svg(xmlINiframe),"image/svg+xml");
|
||||
const svgEl = doc.firstElementChild;
|
||||
const host = createDiv();
|
||||
host.appendChild(svgEl);
|
||||
@@ -349,7 +411,40 @@ const convertMarkdownToSVG = async (plugin: ExcalidrawPlugin, file: TFile, linkP
|
||||
//finalize SVG
|
||||
svgStyle = ' width="'+linkParts.width+'px" height="'+svgHeight+'px"';
|
||||
foreignObjectStyle = ' width="'+linkParts.width+'px" height="'+svgHeight+'px"';
|
||||
return svgToBase64(svg()) as DataURL;
|
||||
const xml = (new XMLSerializer().serializeToString(mdDIV))
|
||||
const finalSVG = svg(xml,style);
|
||||
plugin.ea.mostRecentMarkdownSVG = parser.parseFromString(finalSVG,"image/svg+xml").firstElementChild as SVGSVGElement;
|
||||
return svgToBase64(finalSVG) as DataURL;
|
||||
}
|
||||
|
||||
const styleSandbox = async (style:string,fontName:string,fontColor:string, text:string,path:string,plugin:ExcalidrawPlugin) => {
|
||||
const host = document.body.createDiv();
|
||||
host.style.display = "none";
|
||||
const iframe = host.createEl("iframe");
|
||||
const doc = iframe.contentWindow.document;
|
||||
if(style) {
|
||||
const styleEl = doc.createElement("style");
|
||||
styleEl.type = "text/css";
|
||||
styleEl.innerHTML = style;
|
||||
doc.head.appendChild(styleEl);
|
||||
}
|
||||
const div = createDiv("div");
|
||||
|
||||
doc.body.querySelectorAll("*").forEach((el:HTMLElement)=>{
|
||||
const elementStyle = el.style;
|
||||
const computedStyle = window.getComputedStyle(el);
|
||||
let style = "";
|
||||
for (const prop in elementStyle) {
|
||||
if (elementStyle.hasOwnProperty(prop)) {
|
||||
style += prop + ": " + computedStyle[prop] + ";";
|
||||
}
|
||||
}
|
||||
el.setAttribute("style",style);
|
||||
});
|
||||
|
||||
const xml = (new XMLSerializer().serializeToString(div))
|
||||
document.body.removeChild(host);
|
||||
return xml;
|
||||
}
|
||||
|
||||
const getDataURL = async (file: ArrayBuffer,mimeType: string): Promise<DataURL> => {
|
||||
|
||||
@@ -7,7 +7,6 @@ import {
|
||||
FileId,
|
||||
} from "@zsviczian/excalidraw/types/element/types";
|
||||
import {
|
||||
MarkdownRenderer,
|
||||
normalizePath,
|
||||
TFile
|
||||
} from "obsidian"
|
||||
@@ -169,7 +168,7 @@ export interface ExcalidrawAutomate {
|
||||
view: ExcalidrawView, //the excalidraw view receiving the drop
|
||||
pointerPosition: {x:number, y:number} //the pointer position on canvas at the time of drop
|
||||
}):boolean;
|
||||
renderMarkdown(text:string,el:HTMLElement):Promise<void>;
|
||||
mostRecentMarkdownSVG:SVGSVGElement; //Markdown renderer will drop a copy of the most recent SVG here for debugging purposes
|
||||
}
|
||||
|
||||
declare let window: any;
|
||||
@@ -750,9 +749,7 @@ export async function initExcalidrawAutomate(plugin: ExcalidrawPlugin):Promise<E
|
||||
return await this.targetView.addElements(elements,repositionToCursor,save,this.imagesDict);
|
||||
},
|
||||
onDropHook:null,
|
||||
async renderMarkdown(text:string,el:HTMLElement):Promise<void> {
|
||||
await MarkdownRenderer.renderMarkdown(text,el,'',this.plugin);
|
||||
}
|
||||
mostRecentMarkdownSVG:null,
|
||||
};
|
||||
await initFonts();
|
||||
return window.ExcalidrawAutomate;
|
||||
|
||||
@@ -15,7 +15,7 @@ export const FRONTMATTER_KEY_CUSTOM_LINK_BRACKETS = "excalidraw-link-brackets";
|
||||
export const FRONTMATTER_KEY_DEFAULT_MODE = "excalidraw-default-mode";
|
||||
export const FRONTMATTER_KEY_FONT = "excalidraw-font";
|
||||
export const FRONTMATTER_KEY_FONTCOLOR = "excalidraw-font-color";
|
||||
export const FRONTMATTER_KEY_CSS = "excalidraw-css-file";
|
||||
export const FRONTMATTER_KEY_MD_STYLE = "excalidraw-css";
|
||||
export const VIEW_TYPE_EXCALIDRAW = "excalidraw";
|
||||
export const ICON_NAME = "excalidraw-icon";
|
||||
export const MAX_COLORS = 5;
|
||||
|
||||
@@ -143,6 +143,12 @@ export default {
|
||||
MD_DEFAULT_COLOR_NAME: "The default font color to use for embedded markdown files.",
|
||||
MD_DEFAULT_COLOR_DESC: 'Set this to allowed css color names e.g. "steelblue" (https://www.w3schools.com/colors/colors_names.asp), or a valid hexadecimal color e.g. "#e67700". ' +
|
||||
'You can override this setting by adding the following frontmatter-key to the embedded markdown file: "excalidraw-font-color: color_name_or_rgbhex"',
|
||||
MD_CSS_NAME: "CSS file",
|
||||
MD_CSS_DESC: "Filename of the CSS to apply to markdown embeds. Provide the filename with extension (e.g. 'md-embed.css'). Nota bene, the filename may also be a plain " +
|
||||
"markdown file as well, just make sure the content is written using valid css syntax (e.g. 'md-embed-css.md') will work just as well. " +
|
||||
"The generated HTML that is embedded into the image is the same as normal rendered documents in Obsidian. " +
|
||||
"Setting the font-family in the css is currently not supported; it should be set separately using the setting above. " +
|
||||
'You can override this css setting by adding the following frontmatter-key to the embedded markdown file: "excalidraw-css: css_file_in_valut".',
|
||||
EMBED_HEAD: "Embed & Export",
|
||||
EMBED_PREVIEW_SVG_NAME: "Display SVG in markdown preview",
|
||||
EMBED_PREVIEW_SVG_DESC: "The default is to display drawings as SVG images in the markdown preview. Turning this feature off, the markdown preview will display the drawing as an embedded PNG image.",
|
||||
|
||||
@@ -460,6 +460,8 @@ export default class ExcalidrawPlugin extends Plugin {
|
||||
if(!this.settings.matchThemeTrigger) return;
|
||||
//@ts-ignore
|
||||
if(m[0]?.oldValue === m[0]?.target?.getAttribute("class")) return;
|
||||
//@ts-ignore
|
||||
if(m[0]?.oldValue?.includes("theme-dark") === m[0]?.target?.classList?.contains("theme-dark")) return;
|
||||
const theme = isObsidianThemeDark() ? "dark":"light";
|
||||
const leaves = this.app.workspace.getLeavesOfType(VIEW_TYPE_EXCALIDRAW);
|
||||
leaves.forEach((leaf:WorkspaceLeaf)=> {
|
||||
|
||||
@@ -56,6 +56,7 @@ export interface ExcalidrawSettings {
|
||||
mdSVGmaxHeight: number,
|
||||
mdFont: string,
|
||||
mdFontColor: string,
|
||||
mdCSS: string,
|
||||
}
|
||||
|
||||
export const DEFAULT_SETTINGS: ExcalidrawSettings = {
|
||||
@@ -109,6 +110,7 @@ export const DEFAULT_SETTINGS: ExcalidrawSettings = {
|
||||
mdSVGmaxHeight: 800,
|
||||
mdFont: "Virgil",
|
||||
mdFontColor: "Black",
|
||||
mdCSS: ""
|
||||
}
|
||||
|
||||
export class ExcalidrawSettingTab extends PluginSettingTab {
|
||||
@@ -478,6 +480,18 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
|
||||
this.applySettingsUpdate(true);
|
||||
}));
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName(t("MD_CSS_NAME"))
|
||||
.setDesc(t("MD_CSS_DESC"))
|
||||
.addText(text => text
|
||||
.setPlaceholder("filename of css file in vault")
|
||||
.setValue(this.plugin.settings.mdCSS)
|
||||
.onChange((value) => {
|
||||
this.requestReloadDrawings=true;
|
||||
this.plugin.settings.mdCSS = value;
|
||||
this.applySettingsUpdate(true);
|
||||
}));
|
||||
|
||||
this.containerEl.createEl('h1', {text: t("EMBED_HEAD")});
|
||||
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"1.4.13": "0.12.16",
|
||||
"1.4.14": "0.12.16",
|
||||
"1.4.2": "0.11.13"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user