diff --git a/src/assets/main.css b/src/assets/main.css index ba90477..f531733 100644 --- a/src/assets/main.css +++ b/src/assets/main.css @@ -160,6 +160,16 @@ margin: 0 6px; } +.dice-roller-setting-additional-container + > .additional + > .setting-item + > .setting-item-control + > .dice-mod-template-use-subfolders { + margin: 0 px; + font-style: italic; + font-size: small; +} + .dice-roller-setting-additional-container .add-new-formula { margin: 0 1rem; padding: 1rem 1rem 0 1rem; @@ -167,9 +177,9 @@ box-shadow: 0 0 0.25rem var(--background-modifier-box-shadow); } -.dice-roller-setting-additional-container - .add-new-formula - .formula-data +.dice-roller-setting-additional-container + .add-new-formula + .formula-data .setting-item { border: 0; } diff --git a/src/live-preview.ts b/src/live-preview.ts index 55732da..c9da34f 100644 --- a/src/live-preview.ts +++ b/src/live-preview.ts @@ -45,6 +45,7 @@ import { } from "obsidian"; import DiceRollerPlugin from "./main"; import { BasicRoller } from "./roller/roller"; +import { isTemplateFolder } from "./utils/util"; function selectionAndRangeOverlap( selection: EditorSelection, @@ -63,7 +64,7 @@ function selectionAndRangeOverlap( function inlineRender(view: EditorView, plugin: DiceRollerPlugin) { // still doesn't work as expected for tables and callouts - const currentFile = app.workspace.getActiveFile(); + const currentFile = this.app.workspace.getActiveFile(); if (!currentFile) return; const widgets: Range[] = []; @@ -86,8 +87,19 @@ function inlineRender(view: EditorView, plugin: DiceRollerPlugin) { // symbols) overlap if (selectionAndRangeOverlap(selection, start, end + 1)) return; + const original = view.state.doc.sliceString(start, end).trim(); - if (/^dice\-mod:\s*([\s\S]+)\s*?/.test(original)) { + + const isTemplate = + isTemplateFolder( + plugin.data.diceModTemplateFolders, + currentFile + ) + if ( + /^dice\-mod:\s*([\s\S]+)\s*?/.test(original) && + !isTemplate && + plugin.data.replaceDiceModInLivePreview + ) { let [, content] = original.match( /dice\-mod:\s*([\s\S]+)\s*?/ ); @@ -124,13 +136,12 @@ function inlineRender(view: EditorView, plugin: DiceRollerPlugin) { const transaction = view.state.update({ changes: mod }); view.dispatch(transaction); }); - return; } - if (!/^dice(?:\+|\-)?:\s*([\s\S]+)\s*?/.test(original)) return; + if (!/^dice(?:\+|\-|\-mod)?:\s*([\s\S]+)\s*?/.test(original)) return; let [, content] = original.match( - /^dice(?:\+|\-)?:\s*([\s\S]+)\s*?/ + /^dice(?:\+|\-|\-mod)?:\s*([\s\S]+)\s*?/ ); const roller = plugin.getRollerSync(content, currentFile.path); diff --git a/src/main.ts b/src/main.ts index ed1c810..26adc13 100644 --- a/src/main.ts +++ b/src/main.ts @@ -42,6 +42,7 @@ import { inlinePlugin } from "./live-preview"; import API from "./api/api"; import { DEFAULT_ICONS, DiceIcon } from "./view/view.icons"; import copy from "fast-copy"; +import { isTemplateFolder } from "./utils/util"; /* import GenesysView, { GENESYS_VIEW_TYPE } from "./view/genesys"; */ String.prototype.matchAll = String.prototype.matchAll || @@ -176,6 +177,8 @@ interface DiceRollerSettings { icons: DiceIcon[]; showRenderNotice: boolean; + diceModTemplateFolders: Record; + replaceDiceModInLivePreview: boolean; } export const DEFAULT_SETTINGS: DiceRollerSettings = { @@ -209,7 +212,9 @@ export const DEFAULT_SETTINGS: DiceRollerSettings = { round: Round.None, initialDisplay: ExpectedValue.Roll, icons: copy(DEFAULT_ICONS), - showRenderNotice: true + showRenderNotice: true, + diceModTemplateFolders: {}, + replaceDiceModInLivePreview: true }; export default class DiceRollerPlugin extends Plugin { @@ -396,6 +401,8 @@ export default class DiceRollerPlugin extends Plugin { /^dice\-mod:\s*([\s\S]+)\s*?/.test(node.innerText) && info ) { + if (isTemplateFolder(this.data.diceModTemplateFolders, file)) + continue; try { if (!replacementFound) { fileContent = ( diff --git a/src/settings/settings.ts b/src/settings/settings.ts index 297b90c..6bb3882 100644 --- a/src/settings/settings.ts +++ b/src/settings/settings.ts @@ -8,7 +8,8 @@ import { PluginSettingTab, setIcon, Setting, - TextComponent + TextComponent, + TFolder } from "obsidian"; import { Round, ExpectedValue } from "src/types"; import { ICON_DEFINITION } from "src/utils/constants"; @@ -17,6 +18,7 @@ import { DEFAULT_SETTINGS } from "../main"; import { DiceIcon, IconManager, IconShapes } from "src/view/view.icons"; import { generateSlug } from "random-word-slugs"; import { FontSuggestionModal } from "src/suggester/fonts"; +import { FolderSuggestionModal } from "src/suggester/folder"; declare var require: (id: "get-fonts") => { getFonts: () => Promise }; @@ -119,6 +121,11 @@ export default class SettingTab extends PluginSettingTab { cls: "dice-roller-nested-settings" }) ); + this.buildDiceModTemplateFoldersSettings( + this.contentEl.createEl("details", { + cls: "dice-roller-nested-settings" + }) + ); const div = containerEl.createDiv("coffee"); div.createEl("a", { @@ -186,24 +193,6 @@ export default class SettingTab extends PluginSettingTab { await this.plugin.saveSettings(); }); }); - new Setting(containerEl) - .setName("Add Formula When Using Modify Dice") - .setDesc( - createFragment((e) => { - e.createSpan({ - text: "Both the formula and the results will both be added to the note when using " - }); - e.createEl("code", { text: "dice-mod" }); - e.createSpan({ text: "." }); - }) - ) - .addToggle((t) => { - t.setValue(this.plugin.data.displayFormulaForMod); - t.onChange(async (v) => { - this.plugin.data.displayFormulaForMod = v; - await this.plugin.saveSettings(); - }); - }); new Setting(containerEl) .setName("Display Formula in Parentheses After") .setDesc( @@ -231,24 +220,6 @@ export default class SettingTab extends PluginSettingTab { await this.plugin.saveSettings(); }); }); - new Setting(containerEl) - .setName("Escape Markdown When Modifying") - .setDesc( - createFragment((e) => { - e.createSpan({ - text: "Markdown characters will be escaped when using " - }); - e.createEl("code", { text: "dice-mod" }); - e.createSpan({ text: "." }); - }) - ) - .addToggle((t) => { - t.setValue(this.plugin.data.escapeDiceMod); - t.onChange(async (v) => { - this.plugin.data.escapeDiceMod = v; - await this.plugin.saveSettings(); - }); - }); } buildDice(containerEl: HTMLDetailsElement) { containerEl.empty(); @@ -841,9 +812,270 @@ export default class SettingTab extends PluginSettingTab { ); }); } + + buildDiceModTemplateFoldersSettings(containerEl: HTMLDetailsElement) { + containerEl.empty(); + this.#buildSummary(containerEl, "Modify Dice"); + new Setting(containerEl) + .setName( + createFragment((e) => { + e.createSpan({ text: "Apply " }); + e.createEl("code", { text: "dice-mod" }); + e.createSpan({ text: " in live-preview" }); + }) + ) + .setDesc( + createFragment((e) => { + e.createSpan({ text: "If not enabled " }); + e.createEl("code", { text: "dice-mod" }); + e.createSpan({ + text: " will only be applied/replaced in read mode." + }); + }) + ) + .addToggle((t) => { + t.setValue(this.plugin.data.replaceDiceModInLivePreview); + t.onChange(async (v) => { + this.plugin.data.replaceDiceModInLivePreview = v; + await this.plugin.saveSettings(); + }); + }); + new Setting(containerEl) + .setName("Escape Markdown When Modifying") + .setDesc( + createFragment((e) => { + e.createSpan({ + text: "Markdown characters will be escaped when using " + }); + e.createEl("code", { text: "dice-mod" }); + e.createSpan({ text: "." }); + }) + ) + .addToggle((t) => { + t.setValue(this.plugin.data.escapeDiceMod); + t.onChange(async (v) => { + this.plugin.data.escapeDiceMod = v; + await this.plugin.saveSettings(); + }); + }); + new Setting(containerEl) + .setName("Add Formula When Using Modify Dice") + .setDesc( + createFragment((e) => { + e.createSpan({ + text: "Both the formula and the results will both be added to the note when using " + }); + e.createEl("code", { text: "dice-mod" }); + e.createSpan({ text: "." }); + }) + ) + .addToggle((t) => { + t.setValue(this.plugin.data.displayFormulaForMod); + t.onChange(async (v) => { + this.plugin.data.displayFormulaForMod = v; + await this.plugin.saveSettings(); + }); + }); + + const settingEl = containerEl.createDiv( + "dice-roller-setting-additional-container" + ); + const addNew = settingEl.createDiv(); + new Setting(addNew) + .setName("Template Folders") + .setDesc( + createFragment((e) => { + e.createSpan({ text: "Define folders where " }); + e.createEl("code", { text: "dice-mod" }); + e.createSpan({ + text: " is not applied/replaced and can be used in templates." + }); + }) + ) + .addButton((button: ButtonComponent): ButtonComponent => { + let b = button + .setTooltip("Add Folder") + .setButtonText("+") + .onClick(async () => { + const tmp = await this.buildDiceModTemplateFoldersForm( + addNew + ); + + if (tmp) { + this.plugin.data.diceModTemplateFolders[ + tmp.folder + ] = tmp.useSubfolders; + this.buildDiceModTemplateFoldersSettings( + containerEl + ); + await this.plugin.saveSettings(); + } + }); + + return b; + }); + + const additional = settingEl.createDiv("additional"); + + const diceModeTemplateFolders = this.plugin.data.diceModTemplateFolders; + + for (const [folder, useSubfolders] of Object.entries( + diceModeTemplateFolders + )) { + const setting = new Setting(additional).setName(folder); + if (useSubfolders) { + setting.controlEl.createSpan({ + text: "(including subfolders)", + cls: "dice-mod-template-use-subfolders" + }); + } + setting + .addExtraButton((b) => + b + .setIcon("pencil") + .setTooltip("Edit") + .onClick(async () => { + const edited = + await this.buildDiceModTemplateFoldersForm( + addNew, + { + folder: folder, + useSubfolders: useSubfolders + } + ); + + if (edited) { + delete this.plugin.data.diceModTemplateFolders[ + folder + ]; + this.plugin.data.diceModTemplateFolders[ + edited.folder + ] = edited.useSubfolders; + this.buildDiceModTemplateFoldersSettings( + containerEl + ); + await this.plugin.saveSettings(); + } + }) + ) + .addExtraButton((b) => + b + .setIcon("trash") + .setTooltip("Delete") + .onClick(async () => { + delete this.plugin.data.diceModTemplateFolders[ + folder + ]; + await this.plugin.saveSettings(); + this.buildDiceModTemplateFoldersSettings( + containerEl + ); + }) + ); + } + if (!Object.values(diceModeTemplateFolders).length) { + additional.createDiv( + { cls: "no-dice-mod-template-folders" }, + (e) => { + e.createSpan({ text: "Add a template folder to enable " }); + e.createEl("code", { text: "dice-mod" }); + e.createSpan({ text: " in templates!" }); + } + ); + } + } + allFolders = this.app.vault + .getAllLoadedFiles() + .filter((f) => f instanceof TFolder) as TFolder[]; + folders = this.allFolders + .filter( + (f) => + !Object.keys( + this.plugin.data.diceModTemplateFolders ?? {} + ).find(([p]) => f.path === p) + ) + .sort((a, b) => a.path.localeCompare(b.path)); + async buildDiceModTemplateFoldersForm( + el: HTMLElement, + temp: DiceModTemplateFolder = { + folder: null, + useSubfolders: true + } + ): Promise { + return new Promise((resolve) => { + const formulaEl = el.createDiv("add-new-formula"); + const dataEl = formulaEl.createDiv("formula-data"); + + new Setting(dataEl) + .setName("Template Folder") + .addText(async (t) => { + const set = async () => { + temp.folder = t.getValue(); + this.folders = this.allFolders + .filter( + (f) => + !Object.keys( + this.plugin.data + .diceModTemplateFolders ?? {} + ).find(([p]) => f.path === p) + ) + .sort((a, b) => a.path.localeCompare(b.path)); + }; + const folderModal = new FolderSuggestionModal( + this.app, + t, + this.folders + ); + folderModal.onClose = () => { + set(); + }; + t.inputEl.onblur = async () => { + set(); + }; + }); + new Setting(dataEl) + .setName("Also use subfolders") + .addToggle((t) => { + t.setValue(temp.useSubfolders).onChange( + (v) => (temp.useSubfolders = v) + ); + }); + + const buttonEl = formulaEl.createDiv("formula-buttons"); + new Setting(buttonEl) + .addButton((b) => + b + .setCta() + .setButtonText("Save") + .onClick(async () => { + formulaEl.detach(); + if (temp.folder && temp.folder != "") { + resolve(temp); + } else { + new Notice("Invalid Template folder!"); + resolve(null); + } + }) + ) + .addExtraButton((b) => + b + .setIcon("cross") + .setTooltip("Cancel") + .onClick(() => { + formulaEl.detach(); + resolve(null); + }) + ); + }); + } } interface DiceFormula { alias: string; formula: string; } + +interface DiceModTemplateFolder { + folder: string; + useSubfolders: boolean; +} diff --git a/src/suggester/folder.ts b/src/suggester/folder.ts new file mode 100644 index 0000000..c78fb5d --- /dev/null +++ b/src/suggester/folder.ts @@ -0,0 +1,76 @@ +import { + TFolder, + TextComponent, + type CachedMetadata, + App, + type FuzzyMatch +} from "obsidian"; +import { SuggestionModal } from "./suggester"; + +export class FolderSuggestionModal extends SuggestionModal { + text: TextComponent; + cache: CachedMetadata; + constructor(app: App, input: TextComponent, items: TFolder[]) { + super(app, input.inputEl, items); + this.text = input; + + this.inputEl.addEventListener("input", () => this.getFolder()); + } + getFolder() { + const v = this.inputEl.value, + folder = this.app.vault.getAbstractFileByPath(v); + if (folder == this.item) return; + if (!(folder instanceof TFolder)) return; + this.item = folder; + + this.onInputChanged(); + } + getItemText(item: TFolder) { + return item.path; + } + onChooseItem(item: TFolder) { + this.item = item; + this.text.setValue(item.path); + } + selectSuggestion({ item }: FuzzyMatch) { + let link = item.path; + this.item = item; + this.text.setValue(link); + this.onClose(); + + this.close(); + } + renderSuggestion(result: FuzzyMatch, el: HTMLElement) { + let { item, match: matches } = result || {}; + let content = el.createDiv({ + cls: "suggestion-content" + }); + if (!item) { + content.setText(this.emptyStateText); + content.parentElement?.addClass("is-selected"); + return; + } + + let pathLength = item.path.length - item.name.length; + const matchElements = matches.matches.map((m) => { + return createSpan("suggestion-highlight"); + }); + for (let i = pathLength; i < item.path.length; i++) { + let match = matches.matches.find((m) => m[0] === i); + if (match) { + let element = matchElements[matches.matches.indexOf(match)]; + content.appendChild(element); + element.appendText(item.path.substring(match[0], match[1])); + + i += match[1] - match[0] - 1; + continue; + } + + content.appendText(item.path[i]); + } + el.createDiv({ + cls: "suggestion-note", + text: item.path + }); + } +} diff --git a/src/suggester/suggester.ts b/src/suggester/suggester.ts index de17ab8..1b3f8a1 100644 --- a/src/suggester/suggester.ts +++ b/src/suggester/suggester.ts @@ -164,6 +164,7 @@ export abstract class SuggestionModal extends FuzzySuggestModal { empty() { this.suggester.empty(); } + shouldRender = true; onInputChanged(): void { if (this.shouldNotOpen) return; const inputStr = this.modifyInput(this.inputEl.value); @@ -173,7 +174,10 @@ export abstract class SuggestionModal extends FuzzySuggestModal { } else { this.onNoSuggestion(); } - this.open(); + if (this.shouldRender) { + this.open(); + this.shouldRender = false; + } } onFocus(): void { this.shouldNotOpen = false; diff --git a/src/utils/util.ts b/src/utils/util.ts index 34a24ad..830ea19 100644 --- a/src/utils/util.ts +++ b/src/utils/util.ts @@ -1,3 +1,4 @@ +import { TFile } from "obsidian"; import { ResultInterface, ResultMapInterface } from "src/types"; const MATCH = /^\|?([\s\S]+?)\|?$/; @@ -63,3 +64,18 @@ export function _insertIntoMap( /** Insert the new value at the specified index */ map.set(index, value); } + +export function isTemplateFolder(diceModTemplateFolders: Record, currentFile: TFile) { + return Object.entries(diceModTemplateFolders) + .reduce( + (acc, e) => { + let folderName: string = e[0] + let useSubfoldees = e[1] + let ret = useSubfoldees ? + currentFile.parent.path.startsWith(folderName) : + currentFile.parent.path == folderName + return acc || ret + }, + false + ) +}