From fa2c1ed617a0b72d98514827e27028f786868ead Mon Sep 17 00:00:00 2001 From: Valerie Burzynski Date: Mon, 11 Dec 2023 19:27:00 -0600 Subject: [PATCH] feat: sorting options --- .editorconfig | 28 ++++---- .eslintignore | 3 +- README.md | 54 ++++++++++++++ main.ts | 177 ++++++++++++++++++++++++++++++++++++++++------ manifest.json | 2 +- package-lock.json | 24 +++---- package.json | 2 +- versions.json | 3 +- 8 files changed, 240 insertions(+), 53 deletions(-) diff --git a/.editorconfig b/.editorconfig index 3ee89ec..b95f5b5 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,14 +1,14 @@ -# editorconfig.org -root = true - -[*] -charset = utf-8 -end_of_line = lf -indent_size = 2 -indent_style = space -max_line_length = 120 -insert_final_newline = true -trim_trailing_whitespace = true - -[*.{diff,md,mdx}] -trim_trailing_whitespace = false +# editorconfig.org +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_size = 2 +indent_style = space +max_line_length = 120 +insert_final_newline = true +trim_trailing_whitespace = true + +[*.{diff,md,mdx}] +trim_trailing_whitespace = false diff --git a/.eslintignore b/.eslintignore index 32909b2..5a4e622 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,2 +1,3 @@ npm node_modules -build \ No newline at end of file +build +main.js diff --git a/README.md b/README.md index 66f10e5..fdbbcd0 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,60 @@ Waypoint is an Obsidian plugin that automatically generates tables of contents/M Note that since waypoints can only be generated in folder notes, **it is highly recommended that you install a plugin like [Folder Note](https://github.com/xpgo/obsidian-folder-note-plugin)** to ensure that folder notes don't get lost after folder renames and other actions that change the file tree. If a folder note containing a waypoint is renamed to something other than the folder's name, the waypoint will no longer be updated. +### Ordering Notes on Waypoint + +Waypoint provides the following methods for sorting your Notes: + +- Natural +- Lexicographic +- Priority +- Folders First + +#### Natural Sort Order + +This is the default setting. Filenames are sorted alphabetically, except numeric portions are sorted in numerical order. + +```text +- Note +- Note1 +- Note2 +- Note100 +``` + +#### Lexicograhic Order + +Filenames are sorted entirely alphabetically. + +```text +- Note +- Note1 +- Note100 +- Note2 +``` + +#### Folders First + +- All folders appear at the top of the list and are sorted in a natural sort order +- Files appear after any folders and sorted in natural sort order + +#### Priority Sort Order + +By default, notes in Waypoint are sorted alphabetically. However, with priority sort enabled, you can organize your notes in a specific order by enabling setting their Waypoint priorities. To do this, you would add an entry in your [note metadata](https://help.obsidian.md/Editing+and+formatting/Metadata) with a numeric value, such as: + +```markdown +--- +waypointPriority: 0 +--- + +# My note +``` + +Waypoint will then sort the notes based on the numeric value assigned to `waypointPriority`. Smaller values correspond to higher priority, so a note with `waypointPriority: 0` will appear above a note with `waypointPriority: 1`. If two notes have the same priority, they will be sorted alphabetically. + +In case a note does not have a `waypointPriority` defined, it will fall back to the default alphabetical sorting strategy. + +The `waypointPriority` key can be customized to a different key label in the Waypoint settings. This functionality works for both regular notes and folder notes. + ## Current Limitations - **Waypoints can only be created within a folder note** diff --git a/main.ts b/main.ts index 4a99bc9..aa42bdf 100644 --- a/main.ts +++ b/main.ts @@ -16,6 +16,13 @@ enum FolderNoteType { OutsideFolder = "OUTSIDE_FOLDER", } +enum SortType { + Priority = "PRIORITY", + Natural = "NATURAL", + Lexicographic = "LEXICOGRAPHIC", + FoldersFirst = "FOLDERS_FIRST", +} + interface WaypointSettings { waypointFlag: string; stopScanAtFolderNotes: boolean; @@ -25,6 +32,8 @@ interface WaypointSettings { useWikiLinks: boolean; showEnclosingNote: boolean; folderNoteType: string; + waypointPriorityKey: string; + sortType: string; useSpaces: boolean; numSpaces: number; ignoredFolders: string[]; @@ -40,6 +49,8 @@ const DEFAULT_SETTINGS: WaypointSettings = { useWikiLinks: true, showEnclosingNote: false, folderNoteType: FolderNoteType.InsideFolder, + waypointPriorityKey: "waypointPriority", + sortType: SortType.Natural, useSpaces: false, numSpaces: 2, ignoredFolders: ["Templates"], @@ -85,7 +96,14 @@ export default class Waypoint extends Plugin { this.scheduleUpdate(); }) ); - this.registerEvent(this.app.vault.on("modify", this.detectWaypointFlag)); + this.registerEvent( + this.app.vault.on("modify", (file) => { + this.log("modify " + file.name); + this.foldersWithChanges.add(file.parent); + this.scheduleUpdate(); + this.detectWaypointFlag(file as TFile); + }) + ); }); // This adds a settings tab so the user can configure various aspects of the plugin @@ -113,7 +131,7 @@ export default class Waypoint extends Plugin { * Scan the given file for the waypoint flag. If found, update the waypoint. * @param file The file to scan */ - detectWaypointFlag = async (file: TFile) => { + async detectWaypointFlag(file: TFile): Promise { this.log("Modification on " + file.name); this.log("Scanning for Waypoint flags..."); const text = await this.app.vault.cachedRead(file); @@ -123,7 +141,7 @@ export default class Waypoint extends Plugin { if (this.isFolderNote(file)) { this.log("Found waypoint flag in folder note!"); await this.updateWaypoint(file); - await this.updateParentWaypoint(file.parent, this.settings.folderNoteType === FolderNoteType.OutsideFolder); + await this.updateAncestorWaypoint(file.parent, this.settings.folderNoteType === FolderNoteType.OutsideFolder); return; } else if (file.parent.isRoot()) { this.log("Found waypoint flag in root folder."); @@ -142,7 +160,7 @@ export default class Waypoint extends Plugin { } } this.log("No waypoint flags found."); - }; + } isFolderNote(file: TFile): boolean { if (this.settings.folderNoteType === FolderNoteType.InsideFolder) { @@ -186,7 +204,7 @@ export default class Waypoint extends Plugin { * Given a file with a waypoint flag, generate a file tree representation and update the waypoint text. * @param file The file to update */ - async updateWaypoint(file: TFile) { + async updateWaypoint(file: TFile): Promise { this.log("Updating waypoint in " + file.path); let fileTree; if (this.settings.folderNoteType === FolderNoteType.InsideFolder) { @@ -220,8 +238,17 @@ export default class Waypoint extends Plugin { return; } this.log("Waypoint found at " + waypointStart + " to " + waypointEnd); - lines.splice(waypointStart, waypointEnd !== -1 ? waypointEnd - waypointStart + 1 : 1, waypoint); - await this.app.vault.modify(file, lines.join("\n")); + + // Get the current waypoint block from lines and join it to form a string + const currentWaypoint = + waypointEnd !== -1 ? lines.slice(waypointStart, waypointEnd + 1).join("\n") : lines[waypointStart]; + + // Only splice and modify if waypoint differs from the current block + if (currentWaypoint !== waypoint) { + this.log("Waypoint content changed, updating"); + lines.splice(waypointStart, waypointEnd !== -1 ? waypointEnd - waypointStart + 1 : 1, waypoint); + await this.app.vault.modify(file, lines.join("\n")); + } } /** @@ -241,7 +268,7 @@ export default class Waypoint extends Plugin { const indent = this.settings.useSpaces ? " ".repeat(this.settings.numSpaces) : " "; const bullet = indent.repeat(indentLevel) + "-"; if (node instanceof TFile) { - console.log(node); + this.log(node); // Print the file name // Check for the parent being the root because otherwise the "root note" would be included in the tree if (node.extension == "md" && !node.parent.isRoot()) { @@ -295,12 +322,21 @@ export default class Waypoint extends Plugin { if (node.children && node.children.length > 0) { // Print the files and nested folders within the folder let children = node.children; - children = children.sort((a, b) => { - return a.name.localeCompare(b.name, undefined, { - numeric: true, - sensitivity: "base", - }); - }); + + switch (this.settings.sortType) { + case SortType.Lexicographic: + children = children.sort(); + break; + case SortType.Priority: + children = children.sort(this.sortWithPriority); + break; + case SortType.FoldersFirst: + children = children.sort(this.sortFoldersFirst); + break; + default: + children = children.sort(this.sortWithNaturalOrder); + break; + } if (!this.settings.showFolderNotes) { if (this.settings.folderNoteType === FolderNoteType.InsideFolder) { children = children.filter((child) => this.settings.showFolderNotes || child.name !== node.name + ".md"); @@ -350,14 +386,14 @@ export default class Waypoint extends Plugin { /** * Scan the changed folders and their ancestors for waypoints and update them if found. */ - updateChangedFolders = async () => { + async updateChangedFolders() { this.log("Updating changed folders..."); this.foldersWithChanges.forEach((folder) => { this.log("Updating " + folder.path); - this.updateParentWaypoint(folder, true); + this.updateAncestorWaypoint(folder, true); }); this.foldersWithChanges.clear(); - }; + } /** * Schedule an update for the changed folders after debouncing to prevent excessive updates. @@ -365,16 +401,16 @@ export default class Waypoint extends Plugin { scheduleUpdate = debounce(this.updateChangedFolders.bind(this), 500, true); /** - * Update the ancestor waypoint (if any) of the given file/folder. + * Update all ancestor waypoints (if any) of the given file/folder. * @param node The node to start the search from * @param includeCurrentNode Whether to include the given folder in the search */ - updateParentWaypoint = async (node: TAbstractFile, includeCurrentNode: boolean) => { + async updateAncestorWaypoint(node: TAbstractFile, includeCurrentNode: boolean): Promise { const parentWaypoint = await this.locateParentWaypoint(node, includeCurrentNode); if (parentWaypoint !== null) { this.updateWaypoint(parentWaypoint); } - }; + } /** * Locate the ancestor waypoint (if any) of the given file/folder. @@ -383,10 +419,10 @@ export default class Waypoint extends Plugin { * @returns The ancestor waypoint, or null if none was found */ async locateParentWaypoint(node: TAbstractFile, includeCurrentNode: boolean): Promise { - this.log("Locating parent waypoint of " + node.name); + this.log("Locating all ancestor waypoints of " + node.name); let folder = includeCurrentNode ? node : node.parent; // When there's a root-level folder note - if (node.parent.isRoot() && this.settings.root !== null) { + if (node.parent?.isRoot() && this.settings.root !== null) { const file = this.app.vault.getAbstractFileByPath(this.settings.root); if (file instanceof TFile) { this.log("Found folder note: " + file.path); @@ -435,7 +471,7 @@ export default class Waypoint extends Plugin { } } - log(message: string) { + log(message?: string | TFile) { if (this.settings.debugLogging) { console.log(message); } @@ -448,6 +484,71 @@ export default class Waypoint extends Plugin { async saveSettings() { await this.saveData(this.settings); } + + getWaypointPriority(file: TAbstractFile): number | null { + if (file instanceof TFile) { + const { waypointPriorityKey } = this.settings; + const fileCache = this.app.metadataCache.getFileCache(file as TFile); + if (typeof fileCache?.frontmatter?.[waypointPriorityKey] === "number") { + return fileCache.frontmatter[waypointPriorityKey]; + } else { + return null; + } + } else if (file instanceof TFolder) { + if (this.settings.folderNoteType === FolderNoteType.InsideFolder) { + // If file is a folder and folder note is an inside note, attempt to find a child note with the same name. + const foldernote: TAbstractFile | null = file.children.find( + (child) => child instanceof TFile && child.basename === file.name + ); + return foldernote ? this.getWaypointPriority(foldernote) : null; + } else if (this.settings.folderNoteType === FolderNoteType.OutsideFolder) { + // If file is a folder and folder note is an outside note, attempt to find a sibling note with the same name. + if (!file.isRoot()) { + const foldernote: TAbstractFile | null = file.parent.children.find( + (child) => child instanceof TFile && child.basename === file.name + ); + return foldernote ? this.getWaypointPriority(foldernote) : null; + } else { + return null; // Handle case when the file is the root folder. + } + } + return null; + } + } + + sortWithNaturalOrder = (a: TAbstractFile, b: TAbstractFile): number => { + return a.name.localeCompare(b.name, undefined, { + numeric: true, + sensitivity: "base", + }); + }; + + sortFoldersFirst = (a: TAbstractFile, b: TAbstractFile): number => { + if (a instanceof TFolder) { + // if b is also a folder, sort normally, otherwise a (the folder) comes first + return b instanceof TFolder ? this.sortWithNaturalOrder(a, b) : -1; + } + // a is a file. if b is a folder, it comes first, otherwise sort both files normally + return b instanceof TFolder ? 1 : this.sortWithNaturalOrder(a, b); + }; + + sortWithPriority = (a: TAbstractFile, b: TAbstractFile): number => { + const aPriority = this.getWaypointPriority(a); + const bPriority = this.getWaypointPriority(b); + if (aPriority !== null && bPriority !== null) { + // If both have waypointPriority, the one with a lower priority number should come first. + return aPriority - bPriority; + } else if (aPriority !== null) { + // If only `a` has waypointPriority, `a` should come first. + return -1; + } else if (bPriority !== null) { + // If only `b` has waypointPriority, `b` should come first. + return 1; + } else { + // If neither has priority, sort alphabetically. + return this.sortWithNaturalOrder(a, b); + } + }; } class WaypointSettingsTab extends PluginSettingTab { @@ -473,6 +574,7 @@ class WaypointSettingsTab extends PluginSettingTab { .onChange(async (value) => { this.plugin.settings.folderNoteType = value; await this.plugin.saveSettings(); + this.display(); }) ); new Setting(containerEl) @@ -573,6 +675,35 @@ class WaypointSettingsTab extends PluginSettingTab { await this.plugin.saveSettings(); }) ); + new Setting(this.containerEl) + .setName("Sorting Method") + .setDesc("Select how you would like to have your waypoint lists sorted.") + .addDropdown((dropdown) => + dropdown + .addOption(SortType.Natural, "Natural") + .addOption(SortType.Priority, "Prioritized") + .addOption(SortType.Lexicographic, "Lexicographic") + .addOption(SortType.FoldersFirst, "Folders First") + .setValue(this.plugin.settings.sortType) + .onChange(async (value) => { + this.plugin.settings.sortType = value; + await this.plugin.saveSettings(); + this.display(); + }) + ); + new Setting(containerEl) + .setName("Frontmatter key for note priority") + .setDesc("The frontmatter key to set the note order piority when listed in a Waypoint.") + .addText((text) => + text + .setPlaceholder(DEFAULT_SETTINGS.waypointPriorityKey) + .setValue(this.plugin.settings.waypointPriorityKey) + .onChange(async (value) => { + this.plugin.settings.waypointPriorityKey = value; + await this.plugin.saveSettings(); + }) + ) + .setDisabled(this.plugin.settings.sortType !== SortType.Priority); new Setting(containerEl) .setName("Ignored folders") .setDesc("Folders that Waypoint should ignore") diff --git a/manifest.json b/manifest.json index a2644cd..9f796fb 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "id": "waypoint", "name": "Waypoint", - "version": "1.4.1", + "version": "1.4.2", "minAppVersion": "0.12.0", "description": "Easily generate dynamic content maps in your folder notes. Enables folders to show up in the graph view and removes the need for messy tags!", "author": "Idrees Hassan", diff --git a/package-lock.json b/package-lock.json index e4004f9..4eaf7ec 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1832,9 +1832,9 @@ } }, "node_modules/style-mod": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.0.0.tgz", - "integrity": "sha512-OPhtyEjyyN9x3nhPsu76f52yUGXiZcgvsrFVtvTkyGRQJ0XK+GPc6ov1z+lRpbeabka+MYEQxOYRnt5nF30aMw==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.0.3.tgz", + "integrity": "sha512-78Jv8kYJdjbvRwwijtCevYADfsI0lGzYJe4mMFdceO8l75DFFDoqBhR1jVDicDRRaX4//g1u9wKeo+ztc2h1Rw==", "dev": true }, "node_modules/supports-color": { @@ -1932,9 +1932,9 @@ "peer": true }, "node_modules/w3c-keyname": { - "version": "2.2.6", - "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.6.tgz", - "integrity": "sha512-f+fciywl1SJEniZHD6H+kUO8gOnwIr7f4ijKA6+ZvJFjeGi1r4PDLl53Ayud9O/rk64RqgoQine0feoeOU0kXg==", + "version": "2.2.7", + "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.7.tgz", + "integrity": "sha512-XB8aa62d4rrVfoZYQaYNy3fy+z4nrfy2ooea3/0BnBzXW0tSdZ+lRgjzBZhk0La0H6h8fVyYCxx/qkQcAIuvfg==", "dev": true }, "node_modules/which": { @@ -3276,9 +3276,9 @@ "peer": true }, "style-mod": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.0.0.tgz", - "integrity": "sha512-OPhtyEjyyN9x3nhPsu76f52yUGXiZcgvsrFVtvTkyGRQJ0XK+GPc6ov1z+lRpbeabka+MYEQxOYRnt5nF30aMw==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.0.3.tgz", + "integrity": "sha512-78Jv8kYJdjbvRwwijtCevYADfsI0lGzYJe4mMFdceO8l75DFFDoqBhR1jVDicDRRaX4//g1u9wKeo+ztc2h1Rw==", "dev": true }, "supports-color": { @@ -3354,9 +3354,9 @@ "peer": true }, "w3c-keyname": { - "version": "2.2.6", - "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.6.tgz", - "integrity": "sha512-f+fciywl1SJEniZHD6H+kUO8gOnwIr7f4ijKA6+ZvJFjeGi1r4PDLl53Ayud9O/rk64RqgoQine0feoeOU0kXg==", + "version": "2.2.7", + "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.7.tgz", + "integrity": "sha512-XB8aa62d4rrVfoZYQaYNy3fy+z4nrfy2ooea3/0BnBzXW0tSdZ+lRgjzBZhk0La0H6h8fVyYCxx/qkQcAIuvfg==", "dev": true }, "which": { diff --git a/package.json b/package.json index eab16d4..8372511 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "waypoint", - "version": "1.4.1", + "version": "1.4.2", "description": "Easily generate dynamic content maps in your folder notes. Enables folders to show up in the graph view and removes the need for messy tags!", "main": "main.js", "scripts": { diff --git a/versions.json b/versions.json index 68be0cc..017643b 100644 --- a/versions.json +++ b/versions.json @@ -3,5 +3,6 @@ "1.1.0": "0.12.0", "1.2.0": "0.12.0", "1.3.0": "0.12.0", - "1.4.0": "0.12.0" + "1.4.0": "0.12.0", + "1.5.0": "0.12.0" } \ No newline at end of file