Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,12 @@ Waypoint is an Obsidian plugin that automatically generates tables of contents/M
- **INTRODUCING: Landmarks!**
- Landmarks use the default magic word of `%% Landmark %%`
- Landmarks are meant to be used between your waypoints to act as intermediary indexes. They can be placed in subfolder notes of waypoint tags to generate a "waypoint-like" tree without stopping a parent waypoint from including the child files/folders in your tree.
- Expanding on the description for waypoints above, say you have a parent folder that holds languages such as `Languages -> Latin -> Chapter I -> Vocab -> ...`. Similar to above, you want to have chapters listed for "Latin" and also have them show up on the "Language" folder note. This is achievable with Landmarks! By using `%% Landmark %%`, Waypoint will now generate an index in the current folder note listed as a landmark and will not stop the generation of the parent waypoint at that level, but keep going to list the chapters, as that is where the next waypoint is set.
- **Permanent and portable**
- Unlike other plugins, Waypoints are generated and saved as real markdown text within your folder notes. If you decide to switch to a different markdown editor that supports [[links]], all of your tables of contents will still be usable.
- Note that the Waypoint plugin currently only works with Obsidian, so moving files around in another editor will cause your waypoints to be out-of-date.
- Expanding on the description for waypoints above, say you have a parent folder that holds languages such as `Languages -> Latin -> Chapter I -> Vocab -> ...`. Similar to above, you want to have chapters listed for "Latin" and also have them show up on the "Language" folder note. This is achievable with Landmarks! By using `%% Landmark %%`, Waypoint will now generate an index in the current folder note listed as a landmark and will not stop the generation of the parent waypoint at that level, but keep going to list the chapters, as that is where the next waypoint is set.
- **Customizable sort order**
- Waypoints can mirror the File Explorer's sort settings or use a manual order defined in the plugin settings.
- **Permanent and portable**
- Unlike other plugins, Waypoints are generated and saved as real markdown text within your folder notes. If you decide to switch to a different markdown editor that supports [[links]], all of your tables of contents will still be usable.
- Note that the Waypoint plugin currently only works with Obsidian, so moving files around in another editor will cause your waypoints to be out-of-date.

## How To Use

Expand Down
242 changes: 188 additions & 54 deletions main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,12 @@ interface WaypointSettings {
useFrontMatterTitle: boolean;
showEnclosingNote: boolean;
folderNoteType: string;
ignorePaths: string[];
useSpaces: boolean;
numSpaces: number;
ignorePaths: string[];
useSpaces: boolean;
numSpaces: number;
useObsidianSort: boolean;
sortOrder: string;
sortDescending: boolean;
}

const DEFAULT_SETTINGS: WaypointSettings = {
Expand All @@ -37,9 +40,12 @@ const DEFAULT_SETTINGS: WaypointSettings = {
useFrontMatterTitle: false,
showEnclosingNote: false,
folderNoteType: FolderNoteType.InsideFolder,
ignorePaths: ["_attachments"],
useSpaces: false,
numSpaces: 2,
ignorePaths: ["_attachments"],
useSpaces: false,
numSpaces: 2,
useObsidianSort: true,
sortOrder: "name",
sortDescending: false,
};

export default class Waypoint extends Plugin {
Expand Down Expand Up @@ -348,14 +354,9 @@ export default class Waypoint extends Plugin {
if (!node.children || node.children.length == 0) {
return `${bullet} **${node.name}**`;
}
// 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",
});
});
// Print the files and nested folders within the folder
let children = node.children;
children = this.sortChildren(children);
if (!this.settings.showFolderNotes) {
if (this.settings.folderNoteType === FolderNoteType.InsideFolder) {
children = children.filter((child) => (this.settings.showFolderNotes || child.name !== node.name + ".md") && !this.ignorePath(child.path));
Expand All @@ -382,31 +383,125 @@ export default class Waypoint extends Plugin {
* @param node The node to which the path will be generated
* @returns The encoded path
*/
getEncodedUri(rootNode: TFolder, node: TAbstractFile) {
if (rootNode.isRoot()) {
return `./${encodeURI(node.path)}`;
getEncodedUri(rootNode: TFolder, node: TAbstractFile) {
if (rootNode.isRoot()) {
return `./${encodeURI(node.path)}`;
}
return `./${encodeURI(node.path.substring(rootNode.path.length + 1))}`;
}

sortChildren(children: TAbstractFile[]): TAbstractFile[] {
let sortOrder: string;
let reverse: boolean;

if (this.settings.useObsidianSort) {
const app = this.app as any;

// Default fallback values
sortOrder = "alphabetical";
reverse = false;

// Search workspace recursively for file-explorer view
const findFileExplorerView = (split: any): any => {
if (!split) return null;

// Check if this split has a file-explorer view
if (split.view && split.view.getViewType?.() === 'file-explorer') {
return split.view;
}

// Recursively search children
if (split.children && split.children.length > 0) {
for (const child of split.children) {
const found = findFileExplorerView(child);
if (found) return found;
}
}

return null;
};

// Search all workspace splits for file explorer
const fileExplorerView = findFileExplorerView(app.workspace.rootSplit) ||
findFileExplorerView(app.workspace.leftSplit) ||
findFileExplorerView(app.workspace.rightSplit);

if (fileExplorerView) {
sortOrder = fileExplorerView.sortOrder || "alphabetical";
reverse = fileExplorerView.sortOrderReverse || false;
}

// Map Obsidian's sort order values to our expected values
if (sortOrder === "alphabetical" || sortOrder === "name" || sortOrder === "byName") {
sortOrder = "name";
} else if (sortOrder === "modified" || sortOrder === "modtime" || sortOrder === "mtime" ||
sortOrder === "byModifiedTime" || sortOrder === "byModified") {
sortOrder = "modified";
} else if (sortOrder === "created" || sortOrder === "ctime" || sortOrder === "birthtime" ||
sortOrder === "byCreatedTime" || sortOrder === "byCreated") {
sortOrder = "created";
} else {
// If we don't recognize the sort order, fall back to name
sortOrder = "name";
}
} else {
sortOrder = this.settings.sortOrder;
reverse = this.settings.sortDescending;
}

const direction = reverse ? -1 : 1;
return children.sort((a, b) => {
let diff = 0;
if (sortOrder === "modified") {
const aTime = (a as TFile).stat?.mtime;
const bTime = (b as TFile).stat?.mtime;
if (aTime != null && bTime != null) {
diff = aTime - bTime;
} else if (aTime != null) {
diff = 1; // a is newer
} else if (bTime != null) {
diff = -1; // b is newer
}
} else if (sortOrder === "created") {
const aTime = (a as TFile).stat?.ctime;
const bTime = (b as TFile).stat?.ctime;
if (aTime != null && bTime != null) {
diff = aTime - bTime;
} else if (aTime != null) {
diff = 1; // a is newer
} else if (bTime != null) {
diff = -1; // b is newer
}
}
if (diff === 0) {
diff = a.name.localeCompare(b.name, undefined, {
numeric: true,
sensitivity: "base",
});
}
return diff * direction;
});
}


ignorePath(path: string): boolean {
let found = false;
this.settings.ignorePaths.forEach((comparePath) => {
if (comparePath === "") {
// Ignore empty paths (occurs when the setting value is empty)
return;
}
return `./${encodeURI(node.path.substring(rootNode.path.length + 1))}`;
}

ignorePath(path: string): boolean {
let found = false;
this.settings.ignorePaths.forEach((comparePath) => {
if (comparePath === "") {
// Ignore empty paths (occurs when the setting value is empty)
return;
}
const regex = new RegExp(comparePath);
if (path.match(regex)) {
this.log(`Ignoring path: ${path}`);
found = true;
}
});
if (found) {
return true;
const regex = new RegExp(comparePath);
if (path.match(regex)) {
this.log(`Ignoring path: ${path}`);
found = true;
}
return false;
});
if (found) {
return true;
}
return false;
}

/**
* Scan the changed folders and their ancestors for waypoints and update them if found.
Expand Down Expand Up @@ -548,24 +643,63 @@ class WaypointSettingsTab extends PluginSettingTab {
await this.plugin.saveSettings();
})
);
new Setting(containerEl)
.setName("Show Non-Markdown Files")
.setDesc("If enabled, non-Markdown files will be listed alongside other notes in the generated waypoints.")
.addToggle((toggle) =>
toggle.setValue(this.plugin.settings.showNonMarkdownFiles).onChange(async (value) => {
this.plugin.settings.showNonMarkdownFiles = value;
await this.plugin.saveSettings();
})
);
new Setting(containerEl)
.setName("Show Enclosing Note")
.setDesc("If enabled, the name of the folder note containing the waypoint will be listed at the top of the generated waypoints.")
.addToggle((toggle) =>
toggle.setValue(this.plugin.settings.showEnclosingNote).onChange(async (value) => {
this.plugin.settings.showEnclosingNote = value;
await this.plugin.saveSettings();
})
);
new Setting(containerEl)
.setName("Show Non-Markdown Files")
.setDesc("If enabled, non-Markdown files will be listed alongside other notes in the generated waypoints.")
.addToggle((toggle) =>
toggle.setValue(this.plugin.settings.showNonMarkdownFiles).onChange(async (value) => {
this.plugin.settings.showNonMarkdownFiles = value;
await this.plugin.saveSettings();
})
);
new Setting(containerEl)
.setName("Use Obsidian Sort Order")
.setDesc("If enabled, waypoint entries follow the File Explorer's sort settings. Disable to set a custom order for this plugin.")
.addToggle((toggle) =>
toggle.setValue(this.plugin.settings.useObsidianSort).onChange(async (value) => {
this.plugin.settings.useObsidianSort = value;
await this.plugin.saveSettings();
this.display();
})
);
new Setting(containerEl)
.setName("Manual Sort Order")
.setDesc("Sort order used when not following Obsidian's setting.")
.addDropdown((dropdown) =>
dropdown
.addOption("name", "Name")
.addOption("modified", "Last modified time")
.addOption("created", "Creation time")
.setValue(this.plugin.settings.sortOrder)
.onChange(async (value) => {
this.plugin.settings.sortOrder = value;
await this.plugin.saveSettings();
})
.setDisabled(this.plugin.settings.useObsidianSort)
);
new Setting(containerEl)
.setName("Manual Sort Direction")
.setDesc("Direction used when not following Obsidian's setting.")
.addDropdown((dropdown) =>
dropdown
.addOption("asc", "Ascending")
.addOption("desc", "Descending")
.setValue(this.plugin.settings.sortDescending ? "desc" : "asc")
.onChange(async (value) => {
this.plugin.settings.sortDescending = value === "desc";
await this.plugin.saveSettings();
})
.setDisabled(this.plugin.settings.useObsidianSort)
);
new Setting(containerEl)
.setName("Show Enclosing Note")
.setDesc("If enabled, the name of the folder note containing the waypoint will be listed at the top of the generated waypoints.")
.addToggle((toggle) =>
toggle.setValue(this.plugin.settings.showEnclosingNote).onChange(async (value) => {
this.plugin.settings.showEnclosingNote = value;
await this.plugin.saveSettings();
})
);
new Setting(containerEl)
.setName("Stop Scan at Folder Notes")
.setDesc("If enabled, the waypoint generator will stop scanning nested folders when it encounters a folder note. Otherwise, it will only stop if the folder note contains a waypoint.")
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.