mirror of
https://github.com/oen/liquid-template.git
synced 2026-07-22 05:40:24 +00:00
Merge pull request #5 from oeN/fix/use-new-suggest-api
Support the Live Preview editor
This commit is contained in:
commit
6d35adb0a3
7 changed files with 488 additions and 437 deletions
|
|
@ -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/",
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
25
src/main.ts
25
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<void> {
|
||||
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<void> {
|
||||
this.settings = this.settingsWithDefault(await this.loadData());
|
||||
|
|
@ -46,7 +27,7 @@ export default class LiquidTemplates extends Plugin {
|
|||
|
||||
async saveSettings(): Promise<void> {
|
||||
// the templates folder can change
|
||||
this.setupAutosuggest();
|
||||
// this.setupAutosuggest();
|
||||
await this.saveData(this.settings);
|
||||
}
|
||||
|
||||
|
|
|
|||
283
src/settings.ts
283
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();
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<T> implements ISuggestOwner<T> {
|
||||
protected app: App;
|
||||
protected cmEditor: CodeMirror.Editor;
|
||||
private scope: Scope;
|
||||
|
||||
private suggestEl: HTMLElement;
|
||||
private instructionsEl: HTMLElement;
|
||||
private suggest: Suggest<T>;
|
||||
|
||||
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
|
||||
(<any>this.app).keymap.pushScope(this.scope);
|
||||
}
|
||||
|
||||
close(): void {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(<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<T> implements ISuggestOwner<T> {
|
||||
protected app: App;
|
||||
protected cmEditor: CodeMirror.Editor;
|
||||
private scope: Scope;
|
||||
|
||||
private suggestEl: HTMLElement;
|
||||
private instructionsEl: HTMLElement;
|
||||
private suggest: Suggest<T>;
|
||||
|
||||
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
|
||||
(<any>this.app).keymap.pushScope(this.scope);
|
||||
}
|
||||
|
||||
close(): void {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(<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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<ITemplateCompletion> {
|
||||
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<void> {
|
||||
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<ITemplateCompletion> {
|
||||
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<void> {
|
||||
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<string> {
|
||||
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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue