From 1d7960521ea64c61e9f8c520551f22dd891557e6 Mon Sep 17 00:00:00 2001 From: Diomede T Date: Tue, 15 Feb 2022 23:07:25 +0100 Subject: [PATCH 1/2] Fix the problem with the Live Preview editor --- src/engine/filters/date.ts | 82 ++++----- src/engine/filters/days_after.ts | 22 +-- src/main.ts | 25 +-- src/settings.ts | 283 +++++++++++++++--------------- src/suggest/codemirror-suggest.ts | 258 +++++++++++++-------------- src/suggest/template-suggest.ts | 251 ++++++++++++++++---------- 6 files changed, 486 insertions(+), 435 deletions(-) diff --git a/src/engine/filters/date.ts b/src/engine/filters/date.ts index a5f1312..467cb2f 100644 --- a/src/engine/filters/date.ts +++ b/src/engine/filters/date.ts @@ -1,41 +1,41 @@ -import { addDays, format, subDays } from 'date-fns'; - -import { BaseFilter, BaseFilterProps } from './base_filter'; - -export default class DateFiler extends BaseFilter { - originalFilter: Function; - - constructor(props: BaseFilterProps) { - super({ ...props, filterName: 'date' }); - this.originalFilter = this.engine.filters.get('date'); - } - - handler = (givenValue: number | string, dateFormat?: string): string | number => { - const dateToFormat = (typeof givenValue === 'number') - ? givenValue - : this.parseDate(givenValue) - const formatToUse = dateFormat || this.plugin.settings.dateFormat; - - // TODO: improve or remove me: keep backwards compatibility with the current 0.1.5 version - if (formatToUse.includes('%')) { - // TODO: send a notification when using this function - return this.originalFilter(givenValue, dateFormat); - } - - try { - return format(dateToFormat, formatToUse) - } catch (e) { - if (!(e instanceof RangeError)) throw e; - } - - return givenValue; - } - - parseDate = (stringToParse: string): Date => { - if(['now', 'today'].includes(stringToParse)) return new Date; - if(stringToParse === 'yesterday') return subDays(Date.now(), 1); - if(stringToParse === 'tomorrow') return addDays(Date.now(), 1); - // try to parse the given string - return new Date(Date.parse(stringToParse)); - } -} +import { addDays, format, subDays } from 'date-fns'; + +import { BaseFilter, BaseFilterProps } from './base_filter'; + +export default class DateFiler extends BaseFilter { + originalFilter: Function; + + constructor(props: BaseFilterProps) { + super({ ...props, filterName: 'date' }); + this.originalFilter = this.engine.filters.get('date'); + } + + handler = (givenValue: number | string, dateFormat?: string): string | number => { + const dateToFormat = (typeof givenValue === 'number') + ? givenValue + : this.parseDate(givenValue) + const formatToUse = dateFormat || this.plugin.settings.dateFormat; + + // TODO: improve or remove me: keep backwards compatibility with the current 0.1.5 version + if (formatToUse.includes('%')) { + // TODO: send a notification when using this function + return this.originalFilter(givenValue, dateFormat); + } + + try { + return format(dateToFormat, formatToUse) + } catch (e) { + if (!(e instanceof RangeError)) throw e; + } + + return givenValue; + } + + parseDate = (stringToParse: string): Date => { + if(['now', 'today'].includes(stringToParse)) return new Date; + if(stringToParse === 'yesterday') return subDays(Date.now(), 1); + if(stringToParse === 'tomorrow') return addDays(Date.now(), 1); + // try to parse the given string + return new Date(Date.parse(stringToParse)); + } +} diff --git a/src/engine/filters/days_after.ts b/src/engine/filters/days_after.ts index 56e3321..e9cd3ef 100644 --- a/src/engine/filters/days_after.ts +++ b/src/engine/filters/days_after.ts @@ -1,11 +1,11 @@ -import { addDays } from 'date-fns'; - -import { BaseFilter, BaseFilterProps } from './base_filter'; - -export default class DaysAfter extends BaseFilter { - constructor(props: BaseFilterProps) { - super({ ...props, filterName: 'days_after' }); - } - - handler = (daysToSub: number): Date => addDays(Date.now(), daysToSub); -} +import { addDays } from 'date-fns'; + +import { BaseFilter, BaseFilterProps } from './base_filter'; + +export default class DaysAfter extends BaseFilter { + constructor(props: BaseFilterProps) { + super({ ...props, filterName: 'days_after' }); + } + + handler = (daysToAdd: number): Date => addDays(Date.now(), daysToAdd); +} diff --git a/src/main.ts b/src/main.ts index f909dbd..5e1552a 100644 --- a/src/main.ts +++ b/src/main.ts @@ -5,40 +5,21 @@ import { LiquidTemplatesSettings, DEFAULT_SETTINGS, LiquidTemplatesSettingsTab } import TemplateSuggest from "./suggest/template-suggest"; export default class LiquidTemplates extends Plugin { - private autosuggest: TemplateSuggest; public settings: LiquidTemplatesSettings; async onload(): Promise { console.log('loading liquid templates plugin'); await this.loadSettings(); - this.setupAutosuggest(); + this.addSettingTab(new LiquidTemplatesSettingsTab(this.app, this)); - this.addSettingTab(new LiquidTemplatesSettingsTab(this.app, this)) + this.registerEditorSuggest(new TemplateSuggest(this.app, this)); } onunload(): void { console.log('unloading liquid templates plugin'); - // remove the autosuggest handler when unloading the plugin - this.app.workspace.iterateCodeMirrors((cm: CodeMirror.Editor) => { - cm.off("change", this.autosuggestHandler); - }) } - setupAutosuggest(): void { - this.autosuggest = new TemplateSuggest(this.app, this); - - this.registerCodeMirror((cm: CodeMirror.Editor) => { - cm.on("change", this.autosuggestHandler); - }); - } - - autosuggestHandler = ( - cmEditor: CodeMirror.Editor, - changeObj: CodeMirror.EditorChange - ): boolean => { - return this.autosuggest?.update(cmEditor, changeObj); - }; async loadSettings(): Promise { this.settings = this.settingsWithDefault(await this.loadData()); @@ -46,7 +27,7 @@ export default class LiquidTemplates extends Plugin { async saveSettings(): Promise { // the templates folder can change - this.setupAutosuggest(); + // this.setupAutosuggest(); await this.saveData(this.settings); } diff --git a/src/settings.ts b/src/settings.ts index 5a0a2d5..257be08 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -1,137 +1,146 @@ -import { App, PluginSettingTab, Setting } from "obsidian"; -import LiquidTemplates from "./main"; -import { format } from 'date-fns'; - -export interface LiquidTemplatesSettings { - templatesFolder: string; - excludeFolders: string; - dateFormat: string; - timeFormat: string; - autocompleteTrigger: string; -} - -export const DEFAULT_SETTINGS: LiquidTemplatesSettings = { - templatesFolder: 'templates', - excludeFolders: "", - dateFormat: 'yyyy-MM-dd', - timeFormat: 'k:m', - autocompleteTrigger: ';;' -}; - -export class LiquidTemplatesSettingsTab extends PluginSettingTab { - plugin: LiquidTemplates; - - constructor(app: App, plugin: LiquidTemplates) { - super(app, plugin); - this.plugin = plugin; - } - - display(): void { - const { containerEl } = this; - - containerEl.empty(); - - containerEl.createEl('h2', { - text: 'Liquid Templates', - }); - - new Setting(containerEl) - .setName('Templates folder') - .setDesc('Folder where to find your templates, it will be used also as root folder for the liquid tags, like the include ones') - .addText((text) => - text - .setPlaceholder(DEFAULT_SETTINGS.templatesFolder) - .setValue(this.plugin.settings.templatesFolder || DEFAULT_SETTINGS.templatesFolder) - .onChange(async (value) => { - this.plugin.settings.templatesFolder = value; - await this.plugin.saveSettings(); - }) - ); - - new Setting(containerEl) - .setName('Exclude folders') - .setDesc('Name of the folders you want to exclude from the autosuggest menu, relative to the "Templates folder" above. Comma separated values. (useful if you have a "common" or "partial" folder where you store all the partial templates)') - .addText((text) => - text - .setValue(this.plugin.settings.excludeFolders || DEFAULT_SETTINGS.excludeFolders) - .onChange(async (value) => { - this.plugin.settings.excludeFolders = value; - await this.plugin.saveSettings(); - }) - ); - - const dateFormatDesc = document.createDocumentFragment(); - dateFormatDesc.append( - 'Output format for render dates.', - dateFormatDesc.createEl('br'), - 'For more syntax, refer to ', - dateFormatDesc.createEl("a", { - href: "https://date-fns.org/v2.21.3/docs/format", - text: "format reference", - }), - dateFormatDesc.createEl('br'), - 'current format: ', - dateFormatDesc.createEl('b', { - cls: 'u-pop', - text: format(new Date, this.plugin.settings.dateFormat) - }) - ); - - new Setting(containerEl) - .setName('Date format') - .setDesc(dateFormatDesc) - .addText((text) => - text - .setPlaceholder(DEFAULT_SETTINGS.dateFormat) - .setValue(this.plugin.settings.dateFormat || DEFAULT_SETTINGS.dateFormat) - .onChange(async (value) => { - this.plugin.settings.dateFormat = value; - await this.plugin.saveSettings(); - }) - ); - - - const timeFormatDesc = document.createDocumentFragment(); - timeFormatDesc.append( - 'Output format for render time.', - timeFormatDesc.createEl('br'), - 'For more syntax, refer to ', - timeFormatDesc.createEl("a", { - href: "https://date-fns.org/v2.21.3/docs/format", - text: "format reference", - }), - timeFormatDesc.createEl('br'), - 'current format: ', - timeFormatDesc.createEl('b', { - cls: 'u-pop', - text: format(new Date, this.plugin.settings.timeFormat) - }) - ); - - new Setting(containerEl) - .setName('Time format') - .setDesc(timeFormatDesc) - .addText((text) => - text - .setPlaceholder(DEFAULT_SETTINGS.timeFormat) - .setValue(this.plugin.settings.timeFormat || DEFAULT_SETTINGS.timeFormat) - .onChange(async (value) => { - this.plugin.settings.timeFormat = value; - await this.plugin.saveSettings(); - }) - ); - - new Setting(containerEl) - .setName('Autocomplete trigger') - .setDesc('Character/s that trigger the autocomplete menu') - .addText((text) => - text - .setPlaceholder(DEFAULT_SETTINGS.autocompleteTrigger) - .setValue(this.plugin.settings.autocompleteTrigger || DEFAULT_SETTINGS.autocompleteTrigger) - .onChange(async (value) => { - this.plugin.settings.autocompleteTrigger = value.trim(); - await this.plugin.saveSettings(); - }) - ); - } -} +import { App, PluginSettingTab, Setting } from "obsidian"; +import LiquidTemplates from "./main"; +import { format } from 'date-fns'; + +export interface LiquidTemplatesSettings { + templatesFolder: string; + excludeFolders: string; + dateFormat: string; + timeFormat: string; + autocompleteTrigger: string; +} + +export const DEFAULT_SETTINGS: LiquidTemplatesSettings = { + templatesFolder: 'templates', + excludeFolders: "", + dateFormat: 'yyyy-MM-dd', + timeFormat: 'k:m', + autocompleteTrigger: ';;' +}; + +export class LiquidTemplatesSettingsTab extends PluginSettingTab { + plugin: LiquidTemplates; + + constructor(app: App, plugin: LiquidTemplates) { + super(app, plugin); + this.plugin = plugin; + } + + display(): void { + const { containerEl } = this; + + containerEl.empty(); + + containerEl.createEl('h2', { + text: 'Liquid Templates', + }); + + new Setting(containerEl) + .setName('Templates folder') + .setDesc('Folder where to find your templates, it will be used also as root folder for the liquid tags, like the include ones') + .addText((text) => + text + .setPlaceholder(DEFAULT_SETTINGS.templatesFolder) + .setValue(this.plugin.settings.templatesFolder || DEFAULT_SETTINGS.templatesFolder) + .onChange(async (value) => { + this.plugin.settings.templatesFolder = value; + await this.plugin.saveSettings(); + }) + ); + + new Setting(containerEl) + .setName('Exclude folders') + .setDesc('Name of the folders you want to exclude from the autosuggest menu, relative to the "Templates folder" above. Comma separated values. (useful if you have a "common" or "partial" folder where you store all the partial templates)') + .addText((text) => + text + .setValue(this.plugin.settings.excludeFolders || DEFAULT_SETTINGS.excludeFolders) + .onChange(async (value) => { + this.plugin.settings.excludeFolders = value; + await this.plugin.saveSettings(); + }) + ); + + const dateFormatDesc = document.createDocumentFragment(); + dateFormatDesc.append( + 'Output format for render dates.', + dateFormatDesc.createEl('br'), + 'For more syntax, refer to ', + dateFormatDesc.createEl("a", { + href: "https://date-fns.org/v2.21.3/docs/format", + text: "format reference", + }), + dateFormatDesc.createEl('br'), + 'current format: ', + dateFormatDesc.createEl('b', { + cls: 'u-pop', + text: format(new Date, this.plugin.settings.dateFormat) + }) + ); + + new Setting(containerEl) + .setName('Date format') + .setDesc(dateFormatDesc) + .addText((text) => + text + .setPlaceholder(DEFAULT_SETTINGS.dateFormat) + .setValue(this.plugin.settings.dateFormat || DEFAULT_SETTINGS.dateFormat) + .onChange(async (value) => { + this.plugin.settings.dateFormat = value; + await this.plugin.saveSettings(); + }) + ); + + + const timeFormatDesc = document.createDocumentFragment(); + timeFormatDesc.append( + 'Output format for render time.', + timeFormatDesc.createEl('br'), + 'For more syntax, refer to ', + timeFormatDesc.createEl("a", { + href: "https://date-fns.org/v2.21.3/docs/format", + text: "format reference", + }), + timeFormatDesc.createEl('br'), + 'current format: ', + timeFormatDesc.createEl('b', { + cls: 'u-pop', + text: format(new Date, this.plugin.settings.timeFormat) + }) + ); + + new Setting(containerEl) + .setName('Time format') + .setDesc(timeFormatDesc) + .addText((text) => + text + .setPlaceholder(DEFAULT_SETTINGS.timeFormat) + .setValue(this.plugin.settings.timeFormat || DEFAULT_SETTINGS.timeFormat) + .onChange(async (value) => { + this.plugin.settings.timeFormat = value; + await this.plugin.saveSettings(); + }) + ); + + const triggerDesc = document.createDocumentFragment(); + triggerDesc.append( + 'Character/s that trigger the autocomplete menu.', + triggerDesc.createEl('br'), + triggerDesc.createEl('b', { + cls: 'u-pop', + text: 'NOTE: if you change this setting you need to reload Obsidian in order to take action.' + }) + ); + new Setting(containerEl) + .setName('Autocomplete trigger') + .setDesc(triggerDesc) + .addText((text) => + text + .setPlaceholder(DEFAULT_SETTINGS.autocompleteTrigger) + .setValue(this.plugin.settings.autocompleteTrigger || DEFAULT_SETTINGS.autocompleteTrigger) + .onChange(async (value) => { + this.plugin.settings.autocompleteTrigger = value.trim(); + await this.plugin.saveSettings(); + }) + ); + } +} diff --git a/src/suggest/codemirror-suggest.ts b/src/suggest/codemirror-suggest.ts index 118cbbe..36e9a71 100644 --- a/src/suggest/codemirror-suggest.ts +++ b/src/suggest/codemirror-suggest.ts @@ -1,129 +1,129 @@ -import { App, ISuggestOwner, Scope } from "obsidian"; - -import Suggest from "./suggest"; - -function checkForInputPhrase( - cmEditor: CodeMirror.Editor, - pos: CodeMirror.Position, - phrase: string -): boolean { - const from = { - line: pos.line, - ch: pos.ch - phrase.length, - }; - return cmEditor.getRange(from, pos) === phrase; -} - -function isCursorBeforePos( - pos: CodeMirror.Position, - cursor: CodeMirror.Position -): boolean { - if (pos.line === cursor.line) { - return cursor.ch < pos.ch; - } - return cursor.line < pos.line; -} - -export default abstract class CodeMirrorSuggest implements ISuggestOwner { - protected app: App; - protected cmEditor: CodeMirror.Editor; - private scope: Scope; - - private suggestEl: HTMLElement; - private instructionsEl: HTMLElement; - private suggest: Suggest; - - private startPos: CodeMirror.Position; - private triggerPhrase: string; - - constructor(app: App, triggerPhrase: string) { - this.triggerPhrase = triggerPhrase; - this.app = app; - this.scope = new Scope(); - - this.suggestEl = createDiv("suggestion-container"); - const suggestion = this.suggestEl.createDiv("suggestion"); - this.instructionsEl = this.suggestEl.createDiv("prompt-instructions"); - this.suggest = new Suggest(this, suggestion, this.scope); - - this.scope.register([], "Escape", this.close.bind(this)); - } - - public setInstructions( - createInstructionsFn: (containerEl: HTMLElement) => void - ): void { - this.instructionsEl.empty(); - createInstructionsFn(this.instructionsEl); - } - - public update( - cmEditor: CodeMirror.Editor, - changeObj: CodeMirror.EditorChange - ): boolean { - if (this.cmEditor !== cmEditor) { - this.suggestEl?.detach(); - } - this.cmEditor = cmEditor; - const cursorPos = cmEditor.getCursor(); - - // autosuggest is open - if (this.suggestEl.parentNode) { - if (isCursorBeforePos(this.startPos, cursorPos)) { - this.close(); - return false; - } - this.attachAtCursor(); - } else { - if ( - changeObj.text.length === 1 && // ignore multi-cursors - checkForInputPhrase(this.cmEditor, cursorPos, this.triggerPhrase) && - !document.querySelector(".suggestion-container") // don't trigger multiple autosuggests - ) { - this.startPos = cursorPos; - this.open(); - this.attachAtCursor(); - } - } - - return false; - } - - protected getStartPos(): CodeMirror.Position { - return { - line: this.startPos.line, - ch: this.startPos.ch - this.triggerPhrase.length, - }; - } - - protected getInputStr(): string { - // return string from / to cursor - const cursor = this.cmEditor.getCursor(); - const line = this.cmEditor.getLine(cursor.line); - return line.substring(this.startPos.ch, cursor.ch); - } - - private attachAtCursor() { - const inputStr = this.getInputStr(); - const suggestions = this.getSuggestions(inputStr); - this.suggest.setSuggestions(suggestions); - - this.cmEditor.addWidget(this.cmEditor.getCursor(), this.suggestEl, true); - } - - open(): void { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (this.app).keymap.pushScope(this.scope); - } - - close(): void { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (this.app).keymap.popScope(this.scope); - this.startPos = null; - this.suggest.setSuggestions([]); - this.suggestEl.detach(); - } - - abstract getSuggestions(inputStr: string): T[]; - abstract renderSuggestion(item: T, el: HTMLElement): void; - abstract selectSuggestion(item: T, evt: MouseEvent | KeyboardEvent): void; -} +import { App, ISuggestOwner, Scope } from "obsidian"; + +import Suggest from "./suggest"; + +function checkForInputPhrase( + cmEditor: CodeMirror.Editor, + pos: CodeMirror.Position, + phrase: string +): boolean { + const from = { + line: pos.line, + ch: pos.ch - phrase.length, + }; + return cmEditor.getRange(from, pos) === phrase; +} + +function isCursorBeforePos( + pos: CodeMirror.Position, + cursor: CodeMirror.Position +): boolean { + if (pos.line === cursor.line) { + return cursor.ch < pos.ch; + } + return cursor.line < pos.line; +} + +export default abstract class CodeMirrorSuggest implements ISuggestOwner { + protected app: App; + protected cmEditor: CodeMirror.Editor; + private scope: Scope; + + private suggestEl: HTMLElement; + private instructionsEl: HTMLElement; + private suggest: Suggest; + + private startPos: CodeMirror.Position; + private triggerPhrase: string; + + constructor(app: App, triggerPhrase: string) { + this.triggerPhrase = triggerPhrase; + this.app = app; + this.scope = new Scope(); + + this.suggestEl = createDiv("suggestion-container"); + const suggestion = this.suggestEl.createDiv("suggestion"); + this.instructionsEl = this.suggestEl.createDiv("prompt-instructions"); + this.suggest = new Suggest(this, suggestion, this.scope); + + this.scope.register([], "Escape", this.close.bind(this)); + } + + public setInstructions( + createInstructionsFn: (containerEl: HTMLElement) => void + ): void { + this.instructionsEl.empty(); + createInstructionsFn(this.instructionsEl); + } + + public update( + cmEditor: CodeMirror.Editor, + changeObj: CodeMirror.EditorChange + ): boolean { + if (this.cmEditor !== cmEditor) { + this.suggestEl?.detach(); + } + this.cmEditor = cmEditor; + const cursorPos = cmEditor.getCursor(); + + // autosuggest is open + if (this.suggestEl.parentNode) { + if (isCursorBeforePos(this.startPos, cursorPos)) { + this.close(); + return false; + } + this.attachAtCursor(); + } else { + if ( + changeObj.text.length === 1 && // ignore multi-cursors + checkForInputPhrase(this.cmEditor, cursorPos, this.triggerPhrase) && + !document.querySelector(".suggestion-container") // don't trigger multiple autosuggests + ) { + this.startPos = cursorPos; + this.open(); + this.attachAtCursor(); + } + } + + return false; + } + + protected getStartPos(): CodeMirror.Position { + return { + line: this.startPos.line, + ch: this.startPos.ch - this.triggerPhrase.length, + }; + } + + protected getInputStr(): string { + // return string from / to cursor + const cursor = this.cmEditor.getCursor(); + const line = this.cmEditor.getLine(cursor.line); + return line.substring(this.startPos.ch, cursor.ch); + } + + private attachAtCursor() { + const inputStr = this.getInputStr(); + const suggestions = this.getSuggestions(inputStr); + this.suggest.setSuggestions(suggestions); + + this.cmEditor.addWidget(this.cmEditor.getCursor(), this.suggestEl, true); + } + + open(): void { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (this.app).keymap.pushScope(this.scope); + } + + close(): void { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (this.app).keymap.popScope(this.scope); + this.startPos = null; + this.suggest.setSuggestions([]); + this.suggestEl.detach(); + } + + abstract getSuggestions(inputStr: string): T[]; + abstract renderSuggestion(item: T, el: HTMLElement): void; + abstract selectSuggestion(item: T, evt: MouseEvent | KeyboardEvent): void; +} diff --git a/src/suggest/template-suggest.ts b/src/suggest/template-suggest.ts index 423638d..40f4c2a 100644 --- a/src/suggest/template-suggest.ts +++ b/src/suggest/template-suggest.ts @@ -1,95 +1,156 @@ -import { App, TFile } from "obsidian"; -import { Liquid } from "liquidjs"; -import { format } from "date-fns"; - -import CodeMirrorSuggest from "./codemirror-suggest"; -import type PoweredTemplates from "../main"; -import { getTFilesFromFolder } from "../utils"; -import { initEngine } from "src/engine"; - -interface ITemplateCompletion { - label: string; - file?: TFile; -} - -// TODO: create a class for the suggestion that implements the ITemplateCompletion and have some -// utility method to render the template -// TODO: generate a proper context and use the defined settings for the date and time format -export default class TemplateSuggest extends CodeMirrorSuggest { - plugin: PoweredTemplates; - engine: Liquid; - - constructor(app: App, plugin: PoweredTemplates) { - super(app, plugin.settings.autocompleteTrigger); - - this.plugin = plugin; - this.engine = initEngine(app, plugin); - } - - open(): void { - super.open(); - } - - - getSuggestions(inputStr: string): ITemplateCompletion[] { - // handle no matches - const suggestions = this.getTemplateSuggestions(inputStr); - if (suggestions.length) { - return suggestions; - } else { - return [{ label: inputStr }]; - } - } - - getTemplateSuggestions(inputStr: string): ITemplateCompletion[] { - // find the list of files - // TODO: filter before returning all the files - const { - templatesFolder, - excludeFolders - } = this.plugin.settings; - const templates = getTFilesFromFolder(this.app, templatesFolder, excludeFolders.split(',')); - return templates - .map(file => ({ label: file.basename, file: file })) - .filter((items) => items.label.toLowerCase().startsWith(inputStr)); - } - - renderSuggestion(suggestion: ITemplateCompletion, el: HTMLElement): void { - el.setText(suggestion.label); - } - - async generateContext() { - const { - dateFormat, - timeFormat - } = this.plugin.settings; - - return { - // TODO: improve me - date: format(Date.now(), dateFormat), - time: format(Date.now(), timeFormat), - title: await this.app.workspace.getActiveFile().basename, - default_date_format: dateFormat, - default_time_format: timeFormat - } - } - - async selectSuggestion( - suggestion: ITemplateCompletion, - _event: KeyboardEvent | MouseEvent - ): Promise { - const head = this.getStartPos(); - const anchor = this.cmEditor.getCursor(); - - if (suggestion.file) { - const templateString = await this.app.vault.read(suggestion.file) - - const rendered = await this.engine.parseAndRender( - templateString, - await this.generateContext() - ); - this.cmEditor.replaceRange(rendered.trim(), head, anchor); - } - this.close() - } -} +import { + App, + TFile, + Editor, + EditorPosition, + EditorSuggest, + EditorSuggestContext, + EditorSuggestTriggerInfo, + MarkdownView, +} from "obsidian"; +import { Liquid } from "liquidjs"; +import { format } from "date-fns"; + +import type PoweredTemplates from "../main"; +import { getTFilesFromFolder } from "../utils"; +import { initEngine } from "src/engine"; + +interface ITemplateCompletion { + label: string; + file?: TFile; + inline?: string; +} + +// TODO: create a class for the suggestion that implements the ITemplateCompletion and have some +// utility method to render the template +// TODO: generate a proper context and use the defined settings for the date and time format +export default class TemplateSuggest extends EditorSuggest { + plugin: PoweredTemplates; + engine: Liquid; + app: App; + + protected cmEditor: Editor; + + private startPos: CodeMirror.Position; + private triggerPhrase: string; + + constructor(app: App, plugin: PoweredTemplates) { + super(app); + this.app = app; + + this.plugin = plugin; + this.engine = initEngine(app, plugin); + this.triggerPhrase = this.plugin.settings.autocompleteTrigger; + } + + open(): void { + super.open(); + } + + + getSuggestions(context: EditorSuggestContext): ITemplateCompletion[] { + const suggestions = this.getTemplateSuggestions(context.query); + if (suggestions.length) { + return suggestions; + } else { + return [{ label: context.query, inline: `{{${context.query}}}` }]; + } + } + + getTemplateSuggestions(inputStr: string): ITemplateCompletion[] { + // find the list of files + // TODO: filter before returning all the files + const { + templatesFolder, + excludeFolders + } = this.plugin.settings; + const templates = getTFilesFromFolder(this.app, templatesFolder, excludeFolders.split(',')); + return templates + .map(file => ({ label: file.basename, file: file })) + .filter((items) => items.label.toLowerCase().startsWith(inputStr)); + } + + renderSuggestion(suggestion: ITemplateCompletion, el: HTMLElement): void { + el.setText(suggestion.label); + } + + async generateContext() { + const { + dateFormat, + timeFormat + } = this.plugin.settings; + + return { + // TODO: improve me + date: format(Date.now(), dateFormat), + time: format(Date.now(), timeFormat), + title: await this.app.workspace.getActiveFile().basename, + default_date_format: dateFormat, + default_time_format: timeFormat + } + } + + async selectSuggestion( + suggestion: ITemplateCompletion, + _event: KeyboardEvent | MouseEvent + ): Promise { + const { workspace } = this.app; + const activeView = workspace.getActiveViewOfType(MarkdownView); + + if (!activeView) return; + + const editor = activeView.editor; + const head = this.startPos; + const anchor = editor.getCursor(); + + const rendered = await this.renderTemplateSuggestion(suggestion); + editor.replaceRange(rendered.trim(), head, anchor); + this.close() + } + + async renderTemplateSuggestion(suggestion: ITemplateCompletion): Promise { + let templateString = suggestion.inline + if (suggestion.file) templateString = await this.app.vault.read(suggestion.file!); + if (!templateString) return ''; + + return this.engine.parseAndRender( + templateString, + await this.generateContext() + ); + } + + onTrigger(cursor: EditorPosition, editor: Editor, file: TFile): EditorSuggestTriggerInfo { + const lineContents = editor.getLine(cursor.line); + + const match = lineContents + .substring(0, cursor.ch) + .match(new RegExp(`(?:^|\s|\W)(${this.triggerPhrase}[^${this.triggerPhrase}]*$)`)); + + if (match === null) return null; + + const triggerInfo = this.getTriggerInfo(match, cursor); + + this.startPos = triggerInfo.start; + this.cmEditor = editor; + + return triggerInfo; + } + + protected getTriggerInfo( + match: RegExpMatchArray, + cursor: EditorPosition + ): EditorSuggestTriggerInfo { + return { + start: this.getStartPos(match, cursor.line), + end: cursor, + query: match[1].substring(this.triggerPhrase.length), + }; + } + + protected getStartPos(match: RegExpMatchArray, line: number): EditorPosition { + return { + line: line, + ch: match.index + match[0].length - match[1].length, + }; + } +} From 0d90772b184a5da9d53af0343921e501b558141f Mon Sep 17 00:00:00 2001 From: Diomede T Date: Tue, 15 Feb 2022 23:08:51 +0100 Subject: [PATCH 2/2] bump the version and minimum app version --- manifest.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/manifest.json b/manifest.json index cfb757d..28e3305 100644 --- a/manifest.json +++ b/manifest.json @@ -1,8 +1,8 @@ { "id": "liquid-templates", "name": "Liquid Templates", - "version": "0.2.0", - "minAppVersion": "0.12.3", + "version": "0.3.0", + "minAppVersion": "0.13.10", "description": "Empower your template with LiquidJS tags", "author": "diomedet", "authorUrl": "https://diomedet.com/",