ckelsoe_obsidian-shell-path.../path-utils.ts
Charles Kelsoe 20a7cf24c9 Add <absolute-folder> token: containing folder as full filesystem path
Resolves the parent folder of the copied file (or folder), dirname-style, with root edge cases kept shell-usable (/ and C:\ retained). Desktop tier: inherits the existing mobile blank-and-notify mechanics. The unresolved-token notice wording now names the folder token too.

Requested in discussion #17.
2026-06-11 09:09:50 -04:00

89 lines
3.4 KiB
TypeScript

export type PathWrapping = 'none' | 'double-quotes' | 'single-quotes' | 'backticks';
export type PathFormat = 'unix' | 'windows';
export type MarkdownLinkFormat = 'wiki-style' | 'standard-markdown';
export function wrapPath(filePath: string, wrapping: PathWrapping): string {
switch (wrapping) {
case 'double-quotes': return `"${filePath}"`;
case 'single-quotes': return `'${filePath}'`;
case 'backticks': return `\`${filePath}\``;
case 'none':
default: return filePath;
}
}
export function formatRelativePath(filePath: string, format: PathFormat): string {
if (!filePath) {
return format === 'windows' ? '.\\' : './';
}
if (format === 'windows') {
const winPath = filePath.replace(/\//g, '\\');
return winPath.startsWith('.\\') ? winPath : '.\\' + winPath;
}
return filePath.startsWith('./') ? filePath : './' + filePath;
}
export function buildFileUrl(absolutePath: string): string {
if (/^[a-zA-Z]:/.test(absolutePath)) {
// Windows absolute path (e.g. C:\Users\name\file.md)
// The drive letter contains a colon that must NOT be encoded.
// Split on the first '/' after converting backslashes so the drive
// letter ("C:") is kept intact and only the remaining segments are encoded.
const forwardSlashed = absolutePath.replace(/\\/g, '/');
const [drive, ...segments] = forwardSlashed.split('/');
const encodedSegments = segments.map(seg => seg === '' ? '' : encodeURIComponent(seg));
return `file:///${drive}/${encodedSegments.join('/')}`;
} else {
// Unix/Mac absolute path (e.g. /home/user/file.md)
const segments = absolutePath.split('/');
const encodedSegments = segments.map(seg => seg === '' ? '' : encodeURIComponent(seg));
return `file://${encodedSegments.join('/')}`;
}
}
export function buildObsidianUrl(vaultName: string, filePath: string, anchor?: string): string {
const normalizedPath = filePath.endsWith('.md') ? filePath.slice(0, -3) : filePath;
// Obsidian's open URI has no separate heading or block parameter. The anchor
// is appended to the file value after "#": a heading name, or "^id" for a
// block. encodeURIComponent turns "#" into "%23" and "^" into "%5E".
const fileParam = anchor ? `${normalizedPath}#${anchor}` : normalizedPath;
return `obsidian://open?vault=${encodeURIComponent(vaultName)}&file=${encodeURIComponent(fileParam)}`;
}
export function extractFilename(fileName: string, includeExtension: boolean): string {
if (!includeExtension) {
const lastDot = fileName.lastIndexOf('.');
if (lastDot > 0) {
return fileName.substring(0, lastDot);
}
}
return fileName;
}
export function extractParentPath(absolutePath: string): string {
const lastSep = Math.max(absolutePath.lastIndexOf('/'), absolutePath.lastIndexOf('\\'));
if (lastSep < 0) {
return absolutePath;
}
const parent = absolutePath.substring(0, lastSep);
if (parent === '') {
// Unix root: "/file.md" parents to "/", not "".
return '/';
}
if (/^[a-zA-Z]:$/.test(parent)) {
// Windows drive root: "C:\file.md" parents to "C:\". A bare "C:" means
// "current directory on C:" to a shell, which is not the intent.
return parent + '\\';
}
return parent;
}
export function buildMarkdownLink(fileName: string, filePath: string, format: MarkdownLinkFormat): string {
const fileNameWithoutExt = extractFilename(fileName, false);
if (format === 'wiki-style') {
return `[[${fileNameWithoutExt}]]`;
}
return `[${fileName}](${formatRelativePath(filePath, 'unix')})`;
}