Permalink
Cannot retrieve contributors at this time
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
542 lines (467 sloc)
14.6 KB
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import { | |
| App, | |
| Modal, | |
| MomentFormatComponent, | |
| Plugin, | |
| PluginSettingTab, | |
| Setting, | |
| TextComponent, | |
| ButtonComponent, | |
| MarkdownView, | |
| ToggleComponent, | |
| } from "obsidian"; | |
| import { | |
| createDailyNote, | |
| getAllDailyNotes, | |
| getDailyNote, | |
| } from "obsidian-daily-notes-interface"; | |
| import chrono from "chrono-node"; | |
| var getLastDayOfMonth = function (y: any, m: any) { | |
| return new Date(y, m, 0).getDate(); | |
| }; | |
| const custom = chrono.casual.clone(); | |
| custom.parsers.push({ | |
| pattern: () => { | |
| return /\bChristmas\b/i; | |
| }, | |
| extract: (context: any, match: RegExpMatchArray) => { | |
| return { | |
| day: 25, | |
| month: 12, | |
| }; | |
| }, | |
| }); | |
| interface NLDResult { | |
| formattedString: string; | |
| date: Date; | |
| moment: any; | |
| } | |
| export default class NaturalLanguageDates extends Plugin { | |
| settings: NLDSettings; | |
| onInit() {} | |
| async onload() { | |
| console.log("Loading natural language date parser plugin"); | |
| await this.loadSettings(); | |
| this.addCommand({ | |
| id: "nlp-dates", | |
| name: "Parse natural language date", | |
| callback: () => this.onTrigger("replace"), | |
| hotkeys: [], | |
| }); | |
| this.addCommand({ | |
| id: "nlp-dates-link", | |
| name: "Parse natural language date (as link)", | |
| callback: () => this.onTrigger("link"), | |
| hotkeys: [], | |
| }); | |
| this.addCommand({ | |
| id: "nlp-date-clean", | |
| name: "Parse natural language date (as plain text)", | |
| callback: () => this.onTrigger("clean"), | |
| hotkeys: [], | |
| }); | |
| this.addCommand({ | |
| id: "nlp-parse-time", | |
| name: "Parse natural language time", | |
| callback: () => this.onTrigger("time"), | |
| hotkeys: [], | |
| }); | |
| this.addCommand({ | |
| id: "nlp-now", | |
| name: "Insert the current date and time", | |
| callback: () => this.getNowCommand(), | |
| hotkeys: [], | |
| }); | |
| this.addCommand({ | |
| id: "nlp-today", | |
| name: "Insert the current date", | |
| callback: () => this.getDateCommand(), | |
| hotkeys: [], | |
| }); | |
| this.addCommand({ | |
| id: "nlp-time", | |
| name: "Insert the current time", | |
| callback: () => this.getTimeCommand(), | |
| hotkeys: [], | |
| }); | |
| this.addCommand({ | |
| id: "nlp-picker", | |
| name: "Date picker", | |
| checkCallback: (checking: boolean) => { | |
| let leaf = this.app.workspace.activeLeaf; | |
| if (leaf) { | |
| if (!checking) { | |
| new ParseMomentModal(this.app, this).open(); | |
| } | |
| return true; | |
| } | |
| return false; | |
| }, | |
| hotkeys: [], | |
| }); | |
| this.addSettingTab(new NLDSettingsTab(this.app, this)); | |
| this.registerObsidianProtocolHandler("nldates", this.actionHandler.bind(this)); | |
| } | |
| onunload() { | |
| console.log("Unloading natural language date parser plugin"); | |
| } | |
| async loadSettings() { | |
| this.settings = Object.assign(DEFAULT_SETTINGS, await this.loadData()); | |
| } | |
| async saveSettings() { | |
| await this.saveData(this.settings) | |
| } | |
| getParsedDate(selectedText: string): Date { | |
| var nextDateMatch = selectedText.match(/next\s([\w]+)/i); | |
| var lastDayOfMatch = selectedText.match( | |
| /(last day of|end of)\s*([^\n\r]*)/i | |
| ); | |
| var midOf = selectedText.match(/mid\s([\w]+)/i); | |
| if (nextDateMatch && nextDateMatch[1] === "week") { | |
| return custom.parseDate(`next ${this.settings.weekStart}`, new Date(), { | |
| forwardDate: true, | |
| }); | |
| } else if (nextDateMatch && nextDateMatch[1] === "month") { | |
| var thisMonth = custom.parseDate("this month", new Date(), { | |
| forwardDate: true, | |
| }); | |
| return custom.parseDate(selectedText, thisMonth, { | |
| forwardDate: true, | |
| }); | |
| } else if (nextDateMatch && nextDateMatch[1] === "year") { | |
| var thisYear = custom.parseDate("this year", new Date(), { | |
| forwardDate: true, | |
| }); | |
| return custom.parseDate(selectedText, thisYear, { | |
| forwardDate: true, | |
| }); | |
| } else if (lastDayOfMatch) { | |
| var tempDate = custom.parse(lastDayOfMatch[2]); | |
| var year = tempDate[0].start.get("year"), | |
| month = tempDate[0].start.get("month"); | |
| var lastDay = getLastDayOfMonth(year, month); | |
| return custom.parseDate(`${year}-${month}-${lastDay}`, new Date(), { | |
| forwardDate: true, | |
| }); | |
| } else if (midOf) { | |
| return custom.parseDate(`${midOf[1]} 15th`, new Date(), { | |
| forwardDate: true, | |
| }); | |
| } else { | |
| return custom.parseDate(selectedText, new Date(), {}); | |
| } | |
| } | |
| getSelectedText(editor: any) { | |
| if (editor.somethingSelected()) { | |
| return editor.getSelection(); | |
| } else { | |
| var wordBoundaries = this.getWordBoundaries(editor); | |
| editor.getDoc().setSelection(wordBoundaries.start, wordBoundaries.end); | |
| return editor.getSelection(); | |
| } | |
| } | |
| getWordBoundaries(editor: any) { | |
| var cursor = editor.getCursor(); | |
| var line = cursor.line; | |
| var word = editor.findWordAt({ | |
| line: line, | |
| ch: cursor.ch | |
| }); | |
| var wordStart = word.anchor.ch; | |
| var wordEnd = word.head.ch; | |
| return { | |
| start: { | |
| line: line, | |
| ch: wordStart | |
| }, | |
| end: { | |
| line: line, | |
| ch: wordEnd | |
| }, | |
| }; | |
| } | |
| getMoment(date: Date): any { | |
| return (window as any).moment(date); | |
| } | |
| getFormattedDate(date: Date): string { | |
| var formattedDate = this.getMoment(date).format(this.settings.format); | |
| return formattedDate; | |
| } | |
| getFormattedTime(date: Date): string { | |
| var formattedTime = this.getMoment(date).format(this.settings.timeFormat); | |
| return formattedTime; | |
| } | |
| /* | |
| @param dateString: A string that contains a date in natural language, e.g. today, tomorrow, next week | |
| @returns NLDResult: An object containing the date, a cloned Moment and the formatted string. | |
| */ | |
| parseDate(dateString: string): NLDResult { | |
| let date = this.getParsedDate(dateString); | |
| let formattedDate = this.getFormattedDate(date); | |
| if (formattedDate === "Invalid date") { | |
| console.debug("Input date " + dateString + " can't be parsed by nldates"); | |
| } | |
| let result = { | |
| formattedString: formattedDate, | |
| date: date, | |
| moment: this.getMoment(date), | |
| }; | |
| return result; | |
| } | |
| parseTime(dateString: string): NLDResult { | |
| let date = this.getParsedDate(dateString); | |
| let formattedTime = this.getFormattedTime(date); | |
| if (formattedTime === "Invalid date") { | |
| console.debug("Input date " + dateString + " can't be parsed by nldates"); | |
| } | |
| let result = { | |
| formattedString: formattedTime, | |
| date: date, | |
| moment: this.getMoment(date), | |
| }; | |
| return result; | |
| } | |
| onTrigger(mode: string) { | |
| let activeLeaf: any = this.app.workspace.activeLeaf; | |
| let editor = activeLeaf.view.sourceMode.cmEditor; | |
| var cursor = editor.getCursor(); | |
| var selectedText = this.getSelectedText(editor); | |
| let date = this.parseDate(selectedText); | |
| if (!date.moment.isValid()) { | |
| editor.setCursor({ | |
| line: cursor.line, | |
| ch: cursor.ch | |
| }); | |
| } else { | |
| //mode == "replace" | |
| var newStr = `[[${date.formattedString}]]`; | |
| if (mode == "link") { | |
| newStr = `[${selectedText}](${date.formattedString})`; | |
| } else if (mode == "clean") { | |
| newStr = `${date.formattedString}`; | |
| } else if (mode == "time") { | |
| let time = this.parseTime(selectedText); | |
| newStr = `${time.formattedString}`; | |
| } | |
| editor.replaceSelection(newStr); | |
| this.adjustCursor(editor, cursor, newStr, selectedText); | |
| editor.focus(); | |
| } | |
| } | |
| adjustCursor(editor: any, cursor: any, newStr: string, oldStr: string) { | |
| var cursorOffset = newStr.length - oldStr.length; | |
| editor.setCursor({ | |
| line: cursor.line, | |
| ch: cursor.ch + cursorOffset | |
| }); | |
| } | |
| getNowCommand() { | |
| let activeLeaf: any = this.app.workspace.activeLeaf; | |
| let editor = activeLeaf.view.sourceMode.cmEditor; | |
| editor.replaceSelection( | |
| this.getMoment(new Date()).format( | |
| `${this.settings.format}${this.settings.separator}${this.settings.timeFormat}` | |
| ) | |
| ); | |
| } | |
| getDateCommand() { | |
| let activeLeaf: any = this.app.workspace.activeLeaf; | |
| let editor = activeLeaf.view.sourceMode.cmEditor; | |
| editor.replaceSelection( | |
| this.getMoment(new Date()).format(this.settings.format) | |
| ); | |
| } | |
| getTimeCommand() { | |
| let activeLeaf: any = this.app.workspace.activeLeaf; | |
| let editor = activeLeaf.view.sourceMode.cmEditor; | |
| editor.replaceSelection( | |
| this.getMoment(new Date()).format(this.settings.timeFormat) | |
| ); | |
| } | |
| insertDateString(dateString: string, editor: any, cursor: any) { | |
| editor.replaceSelection(dateString); | |
| } | |
| getDateRange() {} | |
| async actionHandler(params: any) { | |
| let date = this.parseDate(params.day); | |
| console.log(date); | |
| const { | |
| workspace | |
| } = this.app; | |
| if (date.moment.isValid()) { | |
| let dailyNote = await this.getDailyNote(date.moment); | |
| const leaf = workspace.splitActiveLeaf(); | |
| await leaf.openFile(dailyNote); | |
| workspace.setActiveLeaf(leaf); | |
| } | |
| } | |
| getDailyNote(date: any) { | |
| // Borrowed from the Slated plugin: | |
| // https://github.com/tgrosinger/slated-obsidian/blob/main/src/vault.ts#L17 | |
| const desiredNote = getDailyNote(date, getAllDailyNotes()); | |
| if (desiredNote) { | |
| console.log("Note exists") | |
| return Promise.resolve(desiredNote); | |
| } else { | |
| console.log("Creating daily note") | |
| return Promise.resolve(createDailyNote(date)); | |
| } | |
| } | |
| } | |
| interface NLDSettings { | |
| format: string; | |
| timeFormat: string; | |
| separator: string; | |
| weekStart: string; | |
| modalToggleTime: boolean; | |
| modalToggleLink: boolean; | |
| modalMomentFormat: string; | |
| } | |
| const DEFAULT_SETTINGS: NLDSettings = { | |
| format: "YYYY-MM-DD", | |
| timeFormat: "HH:mm", | |
| separator: " ", | |
| weekStart: "Monday", | |
| modalToggleTime: false, | |
| modalToggleLink: false, | |
| modalMomentFormat: "YYYY-MM-DD HH:mm", | |
| } | |
| class NLDSettingsTab extends PluginSettingTab { | |
| plugin: NaturalLanguageDates; | |
| constructor(app: App, plugin: NaturalLanguageDates) { | |
| super(app, plugin); | |
| this.plugin = plugin; | |
| } | |
| display(): void { | |
| let { | |
| containerEl | |
| } = this; | |
| containerEl.empty(); | |
| new Setting(containerEl) | |
| .setName("Date format") | |
| .setDesc("Output format for parsed dates") | |
| .addMomentFormat((text) => | |
| text | |
| .setDefaultFormat("YYYY-MM-DD") | |
| .setValue(this.plugin.settings.format) | |
| .onChange(async (value) => { | |
| if (value === "") { | |
| this.plugin.settings.format = "YYYY-MM-DD"; | |
| } else { | |
| this.plugin.settings.format = value.trim(); | |
| } | |
| await this.plugin.saveSettings(); | |
| }) | |
| ); | |
| new Setting(containerEl) | |
| .setName("Week starts on") | |
| .setDesc("Which day to consider as the start of the week") | |
| .addDropdown((day) => | |
| day | |
| .addOption("Monday", "Monday") | |
| .addOption("Sunday", "Sunday") | |
| .setValue(this.plugin.settings.weekStart) | |
| .onChange(async (value) => { | |
| this.plugin.settings.weekStart = value.trim(); | |
| await this.plugin.saveSettings(); | |
| }) | |
| ); | |
| containerEl.createEl("h3", { | |
| text: "Hotkey formatting settings" | |
| }); | |
| new Setting(containerEl) | |
| .setName("Time format") | |
| .setDesc("Format for the hotkeys that include the current time") | |
| .addMomentFormat((text) => | |
| text | |
| .setDefaultFormat("HH:mm") | |
| .setValue(this.plugin.settings.timeFormat) | |
| .onChange(async (value) => { | |
| if (value === "") { | |
| this.plugin.settings.timeFormat = "HH:mm"; | |
| } else { | |
| this.plugin.settings.timeFormat = value.trim(); | |
| } | |
| await this.plugin.saveSettings(); | |
| }) | |
| ); | |
| new Setting(containerEl) | |
| .setName("Separator") | |
| .setDesc("Separator between date and time for entries that have both") | |
| .addText((text) => | |
| text | |
| .setPlaceholder("Separator is empty") | |
| .setValue(this.plugin.settings.separator) | |
| .onChange(async (value) => { | |
| this.plugin.settings.separator = value; | |
| await this.plugin.saveSettings(); | |
| }) | |
| ); | |
| } | |
| } | |
| class ParseMomentModal extends Modal { | |
| parsedDateString = ""; | |
| activeView: MarkdownView; | |
| activeEditor: CodeMirror.Editor; | |
| activeCursor: CodeMirror.Position; | |
| plugin: NaturalLanguageDates; | |
| constructor(app: App, plugin: NaturalLanguageDates) { | |
| super(app); | |
| this.plugin = plugin; | |
| this.activeView = this.app.workspace.getActiveViewOfType(MarkdownView); | |
| if (!this.activeView) return; | |
| this.activeEditor = this.activeView.sourceMode.cmEditor; | |
| this.activeCursor = this.activeEditor.getCursor(); | |
| } | |
| onOpen() { | |
| let { | |
| contentEl | |
| } = this; | |
| contentEl.appendText("Date: "); | |
| let inputDateField = new TextComponent(contentEl).setPlaceholder("Date"); | |
| contentEl.createEl("br"); | |
| contentEl.appendText("Format: "); | |
| let momentFormatField = new MomentFormatComponent(contentEl) | |
| .setDefaultFormat("YYYY-MM-DD HH:mm") | |
| .setValue(this.plugin.settings.modalMomentFormat) | |
| .onChange((value) => { | |
| this.plugin.settings.modalMomentFormat = value ? value : "YYYY-MM-DD HH:mm"; | |
| this.plugin.saveSettings(); | |
| }); | |
| contentEl.createEl("br"); | |
| contentEl.appendText("Add as link?"); | |
| let toggleLink = new ToggleComponent(contentEl) | |
| .setValue(this.plugin.settings.modalToggleLink) | |
| .onChange((value) => { | |
| this.plugin.settings.modalToggleLink = value; | |
| this.plugin.saveSettings(); | |
| }); | |
| contentEl.createEl("br"); | |
| let inputButton = new ButtonComponent(contentEl) | |
| .setButtonText("Insert date") | |
| .onClick(() => { | |
| let parsedDate = this.plugin.parseDate(inputDateField.getValue()); | |
| this.parsedDateString = parsedDate.moment.format( | |
| momentFormatField.getValue() | |
| ); | |
| if (!parsedDate.moment.isValid()) this.parsedDateString = ""; | |
| if (toggleLink.getValue() && this.parsedDateString !== "") | |
| this.parsedDateString = `[[${this.parsedDateString}]]`; | |
| this.activeEditor.focus(); | |
| this.activeEditor.setCursor(this.activeCursor); | |
| this.plugin.insertDateString( | |
| this.parsedDateString, | |
| this.activeEditor, | |
| this.activeCursor | |
| ); | |
| this.close(); | |
| }); | |
| inputDateField.inputEl.focus(); | |
| } | |
| onClose() { | |
| let { | |
| contentEl | |
| } = this; | |
| contentEl.empty(); | |
| } | |
| } |