From e5817667f802d76ff163f0c93f3483a7dbf61dd0 Mon Sep 17 00:00:00 2001 From: Christian Bager Bach Houmann Date: Fri, 2 Jul 2021 16:46:02 +0200 Subject: [PATCH] v0.3.0 (#44) --- README.md | 8 ++++ docs/FormatSyntax.md | 4 +- docs/QuickAddAPI.md | 5 +++ manifest.json | 4 +- package.json | 4 +- src/IChoiceExecutor.ts | 1 + src/choiceExecutor.ts | 2 +- src/constants.ts | 6 ++- src/engine/MacroChoiceEngine.ts | 3 +- src/engine/SingleMacroEngine.ts | 6 ++- src/formatters/completeFormatter.ts | 1 + src/formatters/formatter.ts | 22 ++++++++--- src/gui/silentFileAndTagSuggester.ts | 55 ++++++++++++++++++++++++---- src/main.ts | 16 ++++++++ src/quickAddApi.ts | 12 +++++- src/utility.ts | 12 +++--- versions.json | 3 +- 17 files changed, 131 insertions(+), 33 deletions(-) diff --git a/README.md b/README.md index d052494..78e19df 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,14 @@ Quickly add new pages or content to your vault. You can also do a [manual installation](docs/ManualInstallation.md). ## What's new? +### 0.3.0 +- Link suggestion in the input prompt now uses your Obsidian link settings by default. +- Add error handling for using ``{{MACRO}}`` to execute a macro that does not exist. +- Input prompt can now also suggest unresolved links. +- Capped input prompt at 50 suggestions for performance in larger vaults. +- You can now offset dates with ``{{DATE+3}}`` or ``{{DATE:+3}}``. `+3` gives you the date in three days, while `+-3` gives you the date three days ago. +- Added a new API feature which allows you to execute choices from within user scripts. These keep the current variables for the execution, so you can 'transfer' variables. + ### 0.2.14 - 0.2.16 - Add 'Insert at the end of section' feature to Captures. - Revamped the Capture & Template format suggesters. They're now more like smart-autocompleters. diff --git a/docs/FormatSyntax.md b/docs/FormatSyntax.md index 08f5cbb..c0a7aa7 100644 --- a/docs/FormatSyntax.md +++ b/docs/FormatSyntax.md @@ -1,8 +1,8 @@ ## `format` syntax | Template | Description | | ------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `{{DATE}}` | Outputs the current date in `YYYY-MM-DD` format. | -| `{{DATE:}}` | Replace `` with a [Moment.js date format](https://momentjs.com/docs/#/displaying/format/). | +| `{{DATE}}` | Outputs the current date in `YYYY-MM-DD` format. You could write `{{DATE+3}}` to offset the date with 3 days. You can use `+-3` to offset with `-3` days. | +| `{{DATE:}}` | Replace `` with a [Moment.js date format](https://momentjs.com/docs/#/displaying/format/). You could write `{{DATE+3}}` to offset the date with 3 days. | | `{{VDATE:, }}` | You'll get prompted to enter a date and it'll be parsed to the given date format. You could write 'today' or 'in two weeks' and it'll give you the date for that. Works like variables, so you can use the date in multiple places. **REQUIRES THE NATURAL LANGUAGE DATES PLUGIN!** | | `{{VALUE}}` or `{{NAME}}` | Interchangeable. Represents the value given in an input prompt. If text is selected in the current editor, it will be used as the value. | | `{{VALUE:` | You can now use variable names in values. They'll get saved and inserted just like values, but the difference is that you can have as many of them as you want. Use comma separation to get a suggester rather than a prompt. | diff --git a/docs/QuickAddAPI.md b/docs/QuickAddAPI.md index bfedda3..36932d1 100644 --- a/docs/QuickAddAPI.md +++ b/docs/QuickAddAPI.md @@ -33,6 +33,11 @@ Returns an array of the selected items. This function is asynchronous. You should ``await`` it. +### ``executeChoice(choiceName: string)`` +Executes choice with the given name. + +This function is asynchronous. You should ``await`` it. + ## Utility module ### ``getClipboard()`` Returns the contents of your clipboard. diff --git a/manifest.json b/manifest.json index 15bf4e6..5cbc341 100644 --- a/manifest.json +++ b/manifest.json @@ -1,8 +1,8 @@ { "id": "quickadd", "name": "QuickAdd", - "version": "0.2.16", - "minAppVersion": "0.12.00", + "version": "0.3.0", + "minAppVersion": "0.12.5", "description": "Quickly add new pages or content to your vault.", "author": "Christian B. B. Houmann", "authorUrl": "https://bagerbach.com", diff --git a/package.json b/package.json index cd3719e..bf21410 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "quickadd", - "version": "0.2.16", + "version": "0.3.0", "description": "Quickly add new pages or content to your vault.", "main": "main.js", "scripts": { @@ -29,7 +29,7 @@ "babel-jest": "27.0.1", "jest": "27.0.1", "jest-environment-node": "27.0.1", - "obsidian": "^0.12.0", + "obsidian": "0.12.5", "rollup": "^2.32.1", "rollup-plugin-strip-code": "0.2.7", "rollup-plugin-svelte": "^7.1.0", diff --git a/src/IChoiceExecutor.ts b/src/IChoiceExecutor.ts index 6603627..2e993bf 100644 --- a/src/IChoiceExecutor.ts +++ b/src/IChoiceExecutor.ts @@ -2,4 +2,5 @@ import type IChoice from "./types/choices/IChoice"; export interface IChoiceExecutor { execute(choice: IChoice): Promise; + variables: Map; } \ No newline at end of file diff --git a/src/choiceExecutor.ts b/src/choiceExecutor.ts index fb0d423..0457b11 100644 --- a/src/choiceExecutor.ts +++ b/src/choiceExecutor.ts @@ -14,7 +14,7 @@ import type IMultiChoice from "./types/choices/IMultiChoice"; import ChoiceSuggester from "./gui/choiceSuggester"; export class ChoiceExecutor implements IChoiceExecutor { - private variables: Map = new Map(); + public variables: Map = new Map(); constructor(private app: App, private plugin: QuickAdd) { } diff --git a/src/constants.ts b/src/constants.ts index 236d364..a09928e 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -16,8 +16,10 @@ export const FILE_NAME_FORMAT_SYNTAX: string[] = [ ] export const FILE_NUMBER_REGEX: RegExp = new RegExp(/([0-9]*)\.md$/); -export const DATE_REGEX: RegExp = new RegExp(/{{DATE(\+[0-9]*)?}}/); -export const DATE_REGEX_FORMATTED: RegExp = new RegExp(/{{DATE:([^}\n\r+]*)(\+[0-9]*)?}}/); +export const NUMBER_REGEX: RegExp = new RegExp(/^-?[0-9]*$/); + +export const DATE_REGEX: RegExp = new RegExp(/{{DATE(\+-?[0-9]+)?}}/); +export const DATE_REGEX_FORMATTED: RegExp = new RegExp(/{{DATE:([^}\n\r+]*)(\+-?[0-9]+)?}}/); export const NAME_VALUE_REGEX: RegExp = new RegExp(/{{NAME}}|{{VALUE}}/); export const VARIABLE_REGEX: RegExp = new RegExp(/{{VALUE:([^\n\r}]*)}}/); export const DATE_VARIABLE_REGEX: RegExp = new RegExp(/{{VDATE:([^\n\r},]*),\s*([^\n\r},]*)}}/); diff --git a/src/engine/MacroChoiceEngine.ts b/src/engine/MacroChoiceEngine.ts index 77f8fcf..0da5286 100644 --- a/src/engine/MacroChoiceEngine.ts +++ b/src/engine/MacroChoiceEngine.ts @@ -21,7 +21,7 @@ import type {IWaitCommand} from "../types/macros/QuickCommands/IWaitCommand"; export class MacroChoiceEngine extends QuickAddChoiceEngine { public choice: IMacroChoice; - public params = {app: this.app, quickAddApi: QuickAddApi.GetApi(this.app), variables: {}}; + public params; protected output: string; protected macros: IMacro[]; protected choiceExecutor: IChoiceExecutor; @@ -33,6 +33,7 @@ export class MacroChoiceEngine extends QuickAddChoiceEngine { this.plugin = plugin; this.macros = macros; this.choiceExecutor = choiceExecutor; + this.params = {app: this.app, quickAddApi: QuickAddApi.GetApi(app, plugin, choiceExecutor), variables: {}}; variables?.forEach(((value, key) => { this.params.variables[key] = value; diff --git a/src/engine/SingleMacroEngine.ts b/src/engine/SingleMacroEngine.ts index 5674cb4..3e6e592 100644 --- a/src/engine/SingleMacroEngine.ts +++ b/src/engine/SingleMacroEngine.ts @@ -4,6 +4,7 @@ import {MacroChoiceEngine} from "./MacroChoiceEngine"; import type QuickAdd from "../main"; import type {IChoiceExecutor} from "../IChoiceExecutor"; import {getUserScriptMemberAccess} from "../utility"; +import {log} from "../logger/logManager"; export class SingleMacroEngine extends MacroChoiceEngine { private memberAccess: string[]; @@ -15,7 +16,10 @@ export class SingleMacroEngine extends MacroChoiceEngine { public async runAndGetOutput(macroName: string): Promise { const {basename, memberAccess} = getUserScriptMemberAccess(macroName); const macro = this.macros.find(macro => macro.name === basename); - if (!macro) return; + if (!macro) { + log.logError(`macro '${macroName}' does not exist.`) + return; + } if (memberAccess && memberAccess.length > 0) { this.memberAccess = memberAccess; diff --git a/src/formatters/completeFormatter.ts b/src/formatters/completeFormatter.ts index 7024363..4a2cc46 100644 --- a/src/formatters/completeFormatter.ts +++ b/src/formatters/completeFormatter.ts @@ -14,6 +14,7 @@ export class CompleteFormatter extends Formatter { constructor(protected app: App, private plugin: QuickAdd, protected choiceExecutor: IChoiceExecutor) { super(); + this.variables = choiceExecutor.variables; } protected async format(input: string): Promise { diff --git a/src/formatters/formatter.ts b/src/formatters/formatter.ts index 9531d00..7775aa5 100644 --- a/src/formatters/formatter.ts +++ b/src/formatters/formatter.ts @@ -4,10 +4,11 @@ import { DATE_VARIABLE_REGEX, LINEBREAK_REGEX, LINK_TO_CURRENT_FILE_REGEX, MACRO_REGEX, - NAME_VALUE_REGEX, TEMPLATE_REGEX, + NAME_VALUE_REGEX, NUMBER_REGEX, TEMPLATE_REGEX, VARIABLE_REGEX } from "../constants"; import {getDate} from "../utility"; +import {match} from "assert"; export abstract class Formatter { protected value: string; @@ -20,17 +21,26 @@ export abstract class Formatter { while (DATE_REGEX.test(output)) { const dateMatch = DATE_REGEX.exec(output); - let offset: string = dateMatch[1] - if (offset) - offset = offset.replace('+', ''); + let offset: number; + if (dateMatch[1]) { + const offsetString = dateMatch[1].replace('+', '').trim(); + const offsetIsInt = NUMBER_REGEX.test(offsetString); + if (offsetIsInt) offset = parseInt(offsetString); + } output = output.replace(DATE_REGEX, getDate({offset: offset})); } while (DATE_REGEX_FORMATTED.test(output)) { const dateMatch = DATE_REGEX_FORMATTED.exec(output); - const format = dateMatch[1].replace('+', ''); - const offset = parseInt(dateMatch[2]); + const format = dateMatch[1] + let offset: number; + + if (dateMatch[2]) { + const offsetString = dateMatch[2].replace('+', '').trim(); + const offsetIsInt = NUMBER_REGEX.test(offsetString); + if (offsetIsInt) offset = parseInt(offsetString); + } output = output.replace(DATE_REGEX_FORMATTED, getDate({format, offset})); } diff --git a/src/gui/silentFileAndTagSuggester.ts b/src/gui/silentFileAndTagSuggester.ts index 23d08b1..0f1fdcd 100644 --- a/src/gui/silentFileAndTagSuggester.ts +++ b/src/gui/silentFileAndTagSuggester.ts @@ -1,5 +1,6 @@ import {TextInputSuggest} from "./suggest"; -import type {App} from "obsidian"; +import type {App, TAbstractFile} from "obsidian"; +import {TFile} from "obsidian"; import {FILE_LINK_REGEX, TAG_REGEX} from "../constants"; enum TagOrFile { @@ -9,12 +10,15 @@ enum TagOrFile { export class SilentFileAndTagSuggester extends TextInputSuggest { private lastInput: string = ""; private lastInputType: TagOrFile; - private fileNames: string[]; + private files: TFile[]; + private unresolvedLinkNames: string[]; private tags: string[]; constructor(public app: App, public inputEl: HTMLInputElement) { super(app, inputEl); - this.fileNames = app.vault.getMarkdownFiles().map(f => f.basename); + this.files = app.vault.getMarkdownFiles(); + this.unresolvedLinkNames = this.getUnresolvedLinkNames(app); + // @ts-ignore this.tags = Object.keys(app.metadataCache.getTags()); } @@ -38,10 +42,13 @@ export class SilentFileAndTagSuggester extends TextInputSuggest { const fileNameInput: string = fileLinkMatch[1]; this.lastInput = fileNameInput; this.lastInputType = TagOrFile.File; - suggestions = this.fileNames.filter(filePath => filePath.toLowerCase().contains(fileNameInput.toLowerCase())); + suggestions = this.files + .filter(file => file.path.toLowerCase().contains(fileNameInput.toLowerCase())) + .map(file => file.path); + suggestions.push(...this.unresolvedLinkNames.filter(name => name.toLowerCase().contains(fileNameInput.toLowerCase()))); } - return suggestions; + return suggestions.slice(0, 50); } renderSuggestion(item: string, el: HTMLElement): void { @@ -55,8 +62,14 @@ export class SilentFileAndTagSuggester extends TextInputSuggest { let insertedEndPosition: number = 0; if (this.lastInputType === TagOrFile.File) { - this.inputEl.value = this.getNewInputValueForFileName(currentInputValue, item, cursorPosition, lastInputLength); - insertedEndPosition = cursorPosition - lastInputLength + item.length + 2; + const linkFile: TAbstractFile = this.app.vault.getAbstractFileByPath(item); + + if (linkFile instanceof TFile) { + insertedEndPosition = this.makeLinkObsidianMethod(linkFile, currentInputValue, cursorPosition, lastInputLength); + } else { + insertedEndPosition = this.makeLinkManually(currentInputValue, item.replace(/.md$/, ''), cursorPosition, lastInputLength); + } + } if (this.lastInputType === TagOrFile.Tag) { @@ -69,6 +82,21 @@ export class SilentFileAndTagSuggester extends TextInputSuggest { this.inputEl.setSelectionRange(insertedEndPosition, insertedEndPosition); } + private makeLinkObsidianMethod(linkFile: TFile, currentInputValue: string, cursorPosition: number, lastInputLength: number) { + const link = this.app.fileManager.generateMarkdownLink(linkFile, ''); + this.inputEl.value = this.getNewInputValueForFileLink(currentInputValue, link, cursorPosition, lastInputLength); + return cursorPosition - lastInputLength + link.length + 2; + } + + private makeLinkManually(currentInputValue: string, item: string, cursorPosition: number, lastInputLength: number) { + this.inputEl.value = this.getNewInputValueForFileName(currentInputValue, item, cursorPosition, lastInputLength); + return cursorPosition - lastInputLength + item.length + 2; + } + + private getNewInputValueForFileLink(currentInputElValue: string, selectedItem: string, cursorPosition: number, lastInputLength: number): string { + return `${currentInputElValue.substr(0, cursorPosition - lastInputLength - 2)}${selectedItem}${currentInputElValue.substr(cursorPosition)}`; + } + private getNewInputValueForFileName(currentInputElValue: string, selectedItem: string, cursorPosition: number, lastInputLength: number): string { return `${currentInputElValue.substr(0, cursorPosition - lastInputLength)}${selectedItem}]]${currentInputElValue.substr(cursorPosition)}`; } @@ -76,4 +104,17 @@ export class SilentFileAndTagSuggester extends TextInputSuggest { private getNewInputValueForTag(currentInputElValue: string, selectedItem: string, cursorPosition: number, lastInputLength: number) { return `${currentInputElValue.substr(0, cursorPosition - lastInputLength - 1)}${selectedItem}${currentInputElValue.substr(cursorPosition)}`; } + + private getUnresolvedLinkNames(app: App): string[] { + const unresolvedLinks: Record> = app.metadataCache.unresolvedLinks; + const unresolvedLinkNames: Set = new Set(); + + for (const sourceFileName in unresolvedLinks) { + for (const unresolvedLink in unresolvedLinks[sourceFileName]) { + unresolvedLinkNames.add(unresolvedLink); + } + } + + return Array.from(unresolvedLinkNames); + } } \ No newline at end of file diff --git a/src/main.ts b/src/main.ts index 465e594..a2983a6 100644 --- a/src/main.ts +++ b/src/main.ts @@ -83,6 +83,22 @@ export default class QuickAdd extends Plugin { } } + public getChoice(choiceName: string): IChoice { + return this.settings.choices.find((choice) => this.getChoiceHelper(choiceName, choice)); + } + + private getChoiceHelper(targetChoiceName: string, currentChoice: IChoice) { + if (currentChoice.type === ChoiceType.Multi) { + let foundChoice: IChoice = (currentChoice as IMultiChoice).choices + .find((choice) => this.getChoiceHelper(targetChoiceName, choice)); + + if (foundChoice) return foundChoice; + } + + if (currentChoice.name === targetChoiceName) + return currentChoice; + } + public removeCommandForChoice(choice: IChoice) { deleteObsidianCommand(this.app, `quickadd:choice:${choice.id}`); } diff --git a/src/quickAddApi.ts b/src/quickAddApi.ts index 28a2df9..7a9ad48 100644 --- a/src/quickAddApi.ts +++ b/src/quickAddApi.ts @@ -3,14 +3,24 @@ import GenericYesNoPrompt from "./gui/GenericYesNoPrompt/GenericYesNoPrompt"; import GenericSuggester from "./gui/GenericSuggester/genericSuggester"; import type {App} from "obsidian"; import GenericCheckboxPrompt from "./gui/GenericCheckboxPrompt/genericCheckboxPrompt"; +import type {IChoiceExecutor} from "./IChoiceExecutor"; +import type QuickAdd from "./main"; +import type IChoice from "./types/choices/IChoice"; +import {log} from "./logger/logManager"; export class QuickAddApi { - public static GetApi(app: App) { + public static GetApi(app: App, plugin: QuickAdd, choiceExecutor: IChoiceExecutor) { return { inputPrompt: (header: string, placeholder?: string, value?: string) => {return this.inputPrompt(app, header, placeholder, value)}, yesNoPrompt: (header: string, text?: string) => {return this.yesNoPrompt(app, header, text)}, suggester: (displayItems: string[] | ((value: string, index?: number, arr?: string[]) => string[]), actualItems: string[]) => {return this.suggester(app, displayItems, actualItems)}, checkboxPrompt: (items: string[], selectedItems?: string[]) => {return this.checkboxPrompt(app, items, selectedItems)}, + executeChoice: async (choiceName: string) => { + const choice: IChoice = plugin.getChoice(choiceName); + if (!choice) log.logError(`choice named '${choiceName}' not found`); + + await choiceExecutor.execute(choice); + }, utility: { getClipboard: async () => {return await navigator.clipboard.readText()}, setClipboard: async (text: string) => {return await navigator.clipboard.writeText(text)} diff --git a/src/utility.ts b/src/utility.ts index 8ad62ea..4d40097 100644 --- a/src/utility.ts +++ b/src/utility.ts @@ -65,17 +65,15 @@ export function getNaturalLanguageDates(app: App) { return app.plugins.plugins["nldates-obsidian"]; } -export function getDate(input?: {format?: string, offset?: number|string}) { +export function getDate(input?: {format?: string, offset?: number}) { let duration; - if (input.offset) { - if(typeof input.offset === "string") - duration = window.moment.duration(input.offset); - else if (typeof input.offset === "number") - duration = window.moment.duration(input.offset, "days"); + if (input.offset !== null && input.offset !== undefined && typeof input.offset === "number") { + duration = window.moment.duration(input.offset, "days"); } - return input.format ? window.moment().add(duration).format(input.format) : window.moment().add(duration).format("YYYY-MM-DD"); + return input.format ? window.moment().add(duration).format(input.format) + : window.moment().add(duration).format("YYYY-MM-DD"); } export function appendToCurrentLine(toAppend: string, app: App) { diff --git a/versions.json b/versions.json index 0c50ccc..ede2280 100644 --- a/versions.json +++ b/versions.json @@ -1,3 +1,4 @@ { - "0.2.16": "0.12.4" + "0.2.16": "0.12.4", + "0.3.0": "0.12.5" }