/*
#exclude
DO NOT ADD DOCUMENT PROPERTIES TO THIS FILE.
CONFIGURE PLUGINS THAT AUTOMATICALLY ADD FRONTMATTER TO EXCLUDE THIS FILE.
THE FILE WILL BREAK IF IT HAS FRONT MATTER PROPERTIES.
*/
// -----------------------------
// -----------------------------
// Icon Library / Bases Search
// -----------------------------
// -----------------------------
const IMAGE_LIBRARY_FOLDER = ea.obsidian.normalizePath("Assets/nosync");
const IMAGE_LIBRARY_FILENAME = "Image Library.base"
const ICONTYPES = [
{name: "Icon", pattern: "icon"},
{name: "Stickfigure", pattern: "stickfigure"},
{name: "Logo", pattern: "logo"}
];
const IMAGE_LIBRARY_PATH = ea.obsidian.normalizePath(IMAGE_LIBRARY_FOLDER + "/" + IMAGE_LIBRARY_FILENAME);
async function initializeImageLibrary() {
await ea.checkAndCreateFolder(IMAGE_LIBRARY_FOLDER);
const syncPlugin = app.internalPlugins.plugins["sync"]?.instance;
if(syncPlugin && !syncPlugin.ignoreFolders.includes(IMAGE_LIBRARY_FOLDER)) {
syncPlugin.setIgnoreFolders(syncPlugin.ignoreFolders.concat(IMAGE_LIBRARY_FOLDER));
}
const imgLibFile = app.vault.getFileByPath(IMAGE_LIBRARY_PATH);
if(!imgLibFile) {
//The bases file is very sensitive to spaces, indents, and formatting
//take care when modifying this
const baseTemplate = `formulas:\n` +
` Icon: image(file.path)\n` +
` keywords: file.name.split(" - ")[1]\n` +
` icon-path: link(if(file.ext == "md", "Assets/" + file.name.split(" - ")[0] + "s/" + file.name + ".svg", file.path))\n` +
`views:\n` +
` - type: cards\n` +
` name: View\n` +
` filters:\n` +
` and:\n` +
` - /^(icon|stickfigure|logo) \\- /i.matches(file.name.lower())\n` +
` - '!file.path.startsWith("Assets/")'\n` +
` - /./i.matches(formula.keywords)\n` +
` order:\n` +
` - formula.keywords\n` +
` sort:\n` +
` - property: formula.keywords\n` +
` direction: ASC\n` +
` cardSize: 130\n` +
` imageFit: contain\n` +
` image: formula.icon-path\n` +
` imageAspectRatio: 0.8\n`;
await app.vault.create(IMAGE_LIBRARY_PATH, baseTemplate);
}
}
initializeImageLibrary();
async function revealIconLibrary() {
const file = app.vault.getFileByPath(IMAGE_LIBRARY_PATH);
if(!file) return;
let leaf;
app.workspace.iterateAllLeaves(l=>{
if(leaf) return;
if(l.view?.getViewType() === "bases" && l.view.getState().file === file.path) leaf = l;
});
if(leaf) {
app.workspace.revealLeaf(leaf);
return file;
}
leaf = app.workspace.getRightLeaf();
await leaf.openFile(file);
app.workspace.revealLeaf(leaf);
return file;
}
if(ea.verifyMinimumPluginVersion("2.13.2")) {
ea.plugin.addCommand({
id: "base-filter-keywords",
name: "Icon Library",
icon: "images",
callback: async () => {
// Check if the active file is a .base file
const file = await revealIconLibrary();
if(!file) return false;
let baseContent = await app.vault.read(file);
// Check if the file has the specific patterns for filtering
if (!baseContent.includes(".matches(formula.keywords)")) return;
// Create a modal using Obsidian's Modal class
const Modal = ea.FloatingModal;
const modal = new Modal(app);
const { contentEl } = modal;
contentEl.createEl("style", {
text: `
input[type="checkbox"]:focus-visible {
outline: 2px solid ${app.getAccentColor()} !important;
outline-offset: 2px !important;
}
`
});
// Set title
contentEl.createEl("h3", {
text: "Icon Library"
});
// ---------------------
// Create keyword filter
// ---------------------
const inputContainer = contentEl.createDiv();
inputContainer.style.margin = "20px 0";
const input = contentEl.createEl("input", {
type: "text",
placeholder: "Enter filter term (leave empty for wildcard, you may use regular expression)",
});
input.style.width = "100%";
input.style.padding = "8px";
// Extract current keyword filter
const keywordFilterRegex = /(- +)\/(.*?)\/i?\.matches\(formula\.keywords\)/;
const keywordMatch = baseContent.match(keywordFilterRegex);
if (keywordMatch && keywordMatch[2] && keywordMatch[2] !== ".") {
input.value = keywordMatch[2];
}
// Set focus on the input
setTimeout(() => input.focus(), 50);
// ------------------
// Create toggle switches for file type filters
// ------------------
const toggleContainer = contentEl.createDiv();
toggleContainer.style.margin = "20px 0";
toggleContainer.style.display = "flex";
toggleContainer.style.gap = "15px";
toggleContainer.style.flexWrap = "wrap";
// Get current filter pattern to determine initial toggle states
const fileNameFilterRegex = /\/\^(.*?) \\- \/i?\.matches\(file\.name\.lower\(\)\)/;
const match = baseContent.match(fileNameFilterRegex);
let currentFilters = [];
if (match && match[1]) {
currentFilters = match[1].replace(/[\(\)]/g, '').split('|');
}
// Create toggle function
const createToggle = (label, value) => {
const toggleWrapper = toggleContainer.createDiv();
toggleWrapper.style.display = "flex";
toggleWrapper.style.alignItems = "center";
const checkbox = toggleWrapper.createEl("input", {
type: "checkbox",
attr: { id: `toggle-${value}` }
});
checkbox.checked = currentFilters.includes(value);
const labelEl = toggleWrapper.createEl("label", {
text: label,
attr: { for: `toggle-${value}` }
});
labelEl.style.marginLeft = "5px";
return checkbox;
};
// Create toggles dynamically based on ICONTYPES array
const typeToggles = {};
ICONTYPES.forEach(iconType => {
typeToggles[iconType.pattern] = createToggle(iconType.name, iconType.pattern);
});
// Function to apply the filter
const applyFilter = async () => {
// Get selected file types
const selectedTypes = [];
ICONTYPES.forEach(iconType => {
if (typeToggles[iconType.pattern].checked) selectedTypes.push(iconType.pattern);
});
// Build file type filter pattern
const fileTypePattern = selectedTypes.length > 0
? `/^(${selectedTypes.join('|')}) \\- /i`
: `/^() \\- /i`; // Empty pattern if none selected
// Get keyword filter
const keywordTerm = input.value.trim() || ".";
// Update both filter patterns in the base file
let updatedContent = baseContent;
// Update file name filter
updatedContent = updatedContent.replace(
/\/\^.*? \\\- \/i?\.matches\(file\.name\.lower\(\)\)/,
`${fileTypePattern}.matches(file.name.lower())`
);
// Update keyword filter
updatedContent = updatedContent.replace(
/(- +)\/.*\/i?\.matches\(formula\.keywords\)/g,
`$1/${keywordTerm}/i.matches(formula.keywords)`
);
// Save the updated file
if (updatedContent !== baseContent) {
await app.vault.modify(file, updatedContent);
baseContent = updatedContent; // Update base content to prevent duplicate updates
}
};
// -------------------
// Add event listeners for input changes to apply filter immediately
// -------------------
contentEl.querySelectorAll("input").forEach(el => {
el.addEventListener("input", applyFilter);
});
// Handle Enter key in the input field
contentEl.querySelectorAll("input").forEach(el => {
el.addEventListener("keydown", (e) => {
if (e.key === "Enter") {
e.preventDefault();
modal.close();
}
});
});
modal.open();
},
});
} else {
new Notice("Icon Library not initialized. Please update to the latest Excalidraw Plugin version", 0);
}
// ------------------------
// ------------------------
// Excalidraw Event Hooks
// ------------------------
// ------------------------
/**
* If set, this callback is triggered when the Excalidraw image is being exported to
* .svg, .png, or .excalidraw.
* You can use this callback to customize the naming and path of the images. This allows
* you to place images into an assets folder.
*
* If the function returns null or undefined, the normal Excalidraw operation will continue
* with the currentImageName and in the same folder as the Excalidraw file
* If a filepath is returned, that will be used. Include the full Vault filepath and filename
* with the file extension.
* !!!! If an image already exists on the path, that will be overwritten. When returning
* your own image path, you must take care of unique filenames (if that is a requirement) !!!!
* The current image name is the name generated by Excalidraw:
* - my-drawing.png
* - my-drawing.svg
* - my-drawing.excalidraw
* - my-drawing.dark.svg
* - my-drawing.light.svg
* - my-drawing.dark.png
* - my-drawing.light.png
*
* @param data - An object containing the following properties:
* @property {string} exportFilepath - Default export filepath for the image.
* @property {string} excalidrawFile - TFile: The Excalidraw file being exported.
* @property {ExcalidrawAutomate} ea - The ExcalidrawAutomate instance associated with the hook.
* @property {string} [oldExcalidrawPath] - If action === "move" The old path of the Excalidraw file, else undefined
* @property {string} action - The action being performed: "export", "move", or "delete". move and delete reference the change to the Excalidraw file.
*
* @returns {string} - The new filepath for the image including full vault path and extension.
*
* action === "move" || action === "delete" is only possible if "keep in sync" is enabled
* in plugin export settings
*
* Example usage:
* ```
* onImageFilePathHook: (data) => {
* const { currentImageName, drawingFilePath, frontmatter } = data;
* // Generate a new filepath based on the drawing file name and other criteria
* const ext = currentImageName.split('.').pop();
* if(frontmatter && frontmatter["my-custom-field"]) {
* }
* return `${drawingFileName} - ${currentImageName || 'image'}.${ext}`;
* }
* ```
*/
ea.onImageExportPathHook = (data) => {
//debugger; //remove comment to debug using Developer Console
let {
excalidrawFile,
exportFilepath,
exportExtension,
oldExcalidrawPath,
action
} = data;
const frontmatter = app.metadataCache.getFileCache(excalidrawFile)?.frontmatter;
//console.log(data, frontmatter);
const excalidrawFilename = action === "move" ?
ea.splitFolderAndFilename(excalidrawFile.name).filename :
excalidrawFile.name
if (excalidrawFilename.match(/^icon - /i)) {
const {
folderpath,
filename,
basename,
extension
} = ea.splitFolderAndFilename(exportFilepath);
exportFilepath = "Assets/icons/" + filename;
return exportFilepath;
}
if (excalidrawFilename.match(/^stickfigure - /i)) {
const {
folderpath,
filename,
basename,
extension
} = ea.splitFolderAndFilename(exportFilepath);
exportFilepath = "Assets/stickfigures/" + filename;
return exportFilepath;
}
if (excalidrawFilename.match(/^logo - /i)) {
const {
folderpath,
filename,
basename,
extension
} = ea.splitFolderAndFilename(exportFilepath);
exportFilepath = "Assets/logos/" + filename;
return exportFilepath;
}
// !!!! frontmatter will be undefined when action === "delete"
// this means if you base your logic on frontmatter properties, then
// plugin settings keep files in sync will break for those files when
// deleting the Excalidraw file. The images will not be deleted, or worst
// your logic might result in deleting other files. This hook gives you
// powerful control, but the hook function logic requires careful testing
// on your part.
//if(frontmatter && frontmatter["is-asset"]) { //custom frontmatter property
// exportFilepath = ea.obsidian.normalizePath("assets/" + exportFilepath);
// return exportFilepath;
//}
return exportFilepath;
};
/**
* Excalidraw supports auto-export of Excalidraw files to .png, .svg, and .excalidraw formats.
*
* Auto-export of Excalidraw files can be controlled at multiple levels.
* 1) In plugin settings where you can set up default auto-export applicable to all your Excalidraw files.
* 2) However, if you do not want to auto-export every file, you can also control auto-export
* at the file level using the 'excalidraw-autoexport' frontmatter property.
* 3) This hook gives you an additional layer of control over the auto-export process.
*
* This hook is triggered when an Excalidraw file is being saved.
*
* interface AutoexportConfig {
* png: boolean; // Whether to auto-export to PNG
* svg: boolean; // Whether to auto-export to SVG
* excalidraw: boolean; // Whether to auto-export to Excalidraw format
* theme: "light" | "dark" | "both"; // The theme to use for the export
* }
*
* @param {Object} data - The data for the hook.
* @param {AutoexportConfig} data.autoexportConfig - The current autoexport configuration.
* @param {TFile} data.excalidrawFile - The Excalidraw file being auto-exported.
* @returns {AutoexportConfig | null} - Return a modified AutoexportConfig to override the export behavior, or null to use the default.
*/
ea.onTriggerAutoexportHook = (data) => {
let {
autoexportConfig,
excalidrawFile
} = data;
//const frontmatter = app.metadataCache.getFileCache(excalidrawFile)?.frontmatter;
//console.log(data, frontmatter);
//logic based on filepath and frontmatter
if (excalidrawFile.name.match(/^(?:icon|stickfigure|logo) - /i)) {
autoexportConfig.theme = "light";
autoexportConfig.svg = true;
autoexportConfig.png = false;
autoexportConfig.excalidraw = false;
return autoexportConfig;
}
return autoexportConfig;
};
/*