Files
obsidian-mcp-server/src/utils/waypoint-utils.ts
Bill 48e429d59e fix: remove console.error from graceful error handlers
Removed console.error calls from error handlers that gracefully skip
problematic files and continue processing. These handlers catch errors
when reading or parsing files but successfully return fallback values,
so logging errors creates unnecessary noise during testing and deployment.

Changes:
- vault-tools.ts: Remove console.error from search and frontmatter extraction
- search-utils.ts: Remove console.error from file search handlers
- waypoint-utils.ts: Remove console.error from file read handler
- frontmatter-utils.ts: Remove console.error from YAML and Excalidraw parsing

Test updates:
- Remove test assertions checking for console.error calls since these
  are no longer emitted by graceful error handlers

All 709 tests pass with no console noise during error handling.
2025-10-26 12:44:00 -04: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 (error) {
// 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);
}
}