Files
obsidian-mcp-server/src/utils/waypoint-utils.ts
Bill Ballou 3593291596 fix: address ObsidianReviewBot linting issues
- Add type guard for recursive parameter in notifications.ts to ensure
  only boolean values are stringified (prevents [object Object] output)
- Remove unused error variables from catch blocks across 5 files:
  - vault-tools.ts (5 instances)
  - frontmatter-utils.ts (3 instances)
  - search-utils.ts (2 instances)
  - waypoint-utils.ts (1 instance)
2026-01-31 20:32:24 -05:00

192 lines
5.0 KiB
TypeScript

import { TFile } from 'obsidian';
import { IVaultAdapter } from '../adapters/interfaces';
/**
* Waypoint block information
*/
export interface WaypointBlock {
hasWaypoint: boolean;
waypointRange?: { start: number; end: number };
links?: string[];
rawContent?: string;
}
/**
* Folder note detection result
*/
export interface FolderNoteInfo {
isFolderNote: boolean;
reason: 'basename_match' | 'waypoint_marker' | 'both' | 'none';
folderPath?: string;
}
/**
* Utilities for working with Waypoint plugin markers
*/
export class WaypointUtils {
private static readonly WAYPOINT_START = /%% Begin Waypoint %%/;
private static readonly WAYPOINT_END = /%% End Waypoint %%/;
private static readonly LINK_PATTERN = /\[\[([^\]]+)\]\]/g;
/**
* Extract waypoint block from file content
*/
static extractWaypointBlock(content: string): WaypointBlock {
const lines = content.split('\n');
let inWaypoint = false;
let waypointStart = -1;
let waypointContent: string[] = [];
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
if (this.WAYPOINT_START.test(line)) {
inWaypoint = true;
waypointStart = i + 1; // 1-indexed, line after marker
waypointContent = [];
} else if (this.WAYPOINT_END.test(line) && inWaypoint) {
// Extract links from waypoint content
const contentStr = waypointContent.join('\n');
const links: string[] = [];
let linkMatch: RegExpExecArray | null;
// Reset lastIndex for global regex
this.LINK_PATTERN.lastIndex = 0;
while ((linkMatch = this.LINK_PATTERN.exec(contentStr)) !== null) {
links.push(linkMatch[1]);
}
return {
hasWaypoint: true,
waypointRange: {
start: waypointStart,
end: i + 1 // 1-indexed, line of end marker
},
links,
rawContent: contentStr
};
} else if (inWaypoint) {
waypointContent.push(line);
}
}
// No waypoint found or unclosed waypoint
return { hasWaypoint: false };
}
/**
* Check if content contains a waypoint block
*/
static hasWaypointMarker(content: string): boolean {
return this.WAYPOINT_START.test(content) && this.WAYPOINT_END.test(content);
}
/**
* Check if a file is a folder note
* A folder note is a note that:
* 1. Has the same basename as its parent folder, OR
* 2. Contains waypoint markers
*/
static async isFolderNote(vault: IVaultAdapter, file: TFile): Promise<FolderNoteInfo> {
const basename = file.basename;
const parentFolder = file.parent;
// Check basename match
const basenameMatch = parentFolder && parentFolder.name === basename;
// Check for waypoint markers
let hasWaypoint = false;
try {
const content = await vault.read(file);
hasWaypoint = this.hasWaypointMarker(content);
} catch {
// If we can't read the file, we can't check for waypoints
}
// Determine result
let reason: 'basename_match' | 'waypoint_marker' | 'both' | 'none';
if (basenameMatch && hasWaypoint) {
reason = 'both';
} else if (basenameMatch) {
reason = 'basename_match';
} else if (hasWaypoint) {
reason = 'waypoint_marker';
} else {
reason = 'none';
}
return {
isFolderNote: basenameMatch || hasWaypoint,
reason,
folderPath: parentFolder?.path
};
}
/**
* Check if an edit would affect a waypoint block
* Returns true if the edit should be blocked
*/
static wouldAffectWaypoint(
content: string,
newContent: string
): { affected: boolean; waypointRange?: { start: number; end: number } } {
const waypointBlock = this.extractWaypointBlock(content);
if (!waypointBlock.hasWaypoint) {
return { affected: false };
}
// Check if the waypoint block still exists in the new content
const newWaypointBlock = this.extractWaypointBlock(newContent);
if (!newWaypointBlock.hasWaypoint) {
// Waypoint was removed
return {
affected: true,
waypointRange: waypointBlock.waypointRange
};
}
// Check if waypoint content changed
if (waypointBlock.rawContent !== newWaypointBlock.rawContent) {
return {
affected: true,
waypointRange: waypointBlock.waypointRange
};
}
// Check if waypoint range changed (lines were added/removed before it)
if (
waypointBlock.waypointRange!.start !== newWaypointBlock.waypointRange!.start ||
waypointBlock.waypointRange!.end !== newWaypointBlock.waypointRange!.end
) {
// This is acceptable - waypoint content is the same, just moved
return { affected: false };
}
return { affected: false };
}
/**
* Get the parent folder path for a file path
*/
static getParentFolderPath(filePath: string): string | null {
const lastSlash = filePath.lastIndexOf('/');
if (lastSlash === -1) {
return null; // File is in root
}
return filePath.substring(0, lastSlash);
}
/**
* Get the basename without extension
*/
static getBasename(filePath: string): string {
const lastSlash = filePath.lastIndexOf('/');
const filename = lastSlash === -1 ? filePath : filePath.substring(lastSlash + 1);
const lastDot = filename.lastIndexOf('.');
return lastDot === -1 ? filename : filename.substring(0, lastDot);
}
}