publish support

This commit is contained in:
zsviczian
2023-11-11 07:21:14 +01:00
parent 7449df6ac6
commit fc4fd685ba
10 changed files with 206 additions and 49 deletions

View File

@@ -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;
}

View File

@@ -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);

View File

@@ -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`;

View File

@@ -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());
}
}

View File

@@ -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<string>, 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<TFile[]> => {
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<TFile>();
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<string>(), 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<TFile,boolean>();
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();
};
}
}

View File

@@ -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 <code>excalidraw-autoexport</code> frontmatter key. Valid values for this key are " +
"<code>none</code>,<code>all</code>,<code>svg</code>, <code>png</code>, <code>svg.md</code>. For backwards compatibility <code>both</code> 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 <<your drawing>>.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.",
"<code>none</code>,<code>both</code>,<code>svg</code>, and <code>png</code>.",
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",

View File

@@ -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({

View File

@@ -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) => {

1
src/types.d.ts vendored
View File

@@ -32,6 +32,7 @@ declare global {
declare module "obsidian" {
interface App {
internalPlugins: any;
isMobile(): boolean;
getObsidianUrl(file:TFile): string;
}

View File

@@ -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<SVGSVGElement> => {
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",
},
});
}