@wonderwhy-er/desktop-commander
Version:
MCP server for terminal operations and file editing
229 lines (228 loc) • 7.8 kB
JavaScript
import { slugifyMarkdownHeading } from './slugify.js';
import { getParentDirectory, isWindowsAbsolutePath, normalizeFilePath, normalizePathSeparators } from '../path-utils.js';
const WIKI_LINK_PATTERN = /\[\[([^\]|#]*)(?:#([^\]|]+))?(?:\|([^\]]+))?\]\]/g;
const FENCE_PATTERN = /^(`{3,}|~{3,})/;
function encodeLinkPath(pathValue) {
return encodeURI(normalizePathSeparators(pathValue));
}
function safeDecodeURIComponent(value) {
try {
return decodeURIComponent(value);
}
catch {
return value;
}
}
function parseWikiLink(rawHref) {
const match = rawHref.match(/^\[\[([^\]|#]*)(?:#([^\]|]+))?(?:\|([^\]]+))?\]\]$/);
if (!match) {
return null;
}
return {
path: (match[1] ?? '').trim(),
anchor: match[2]?.trim(),
alias: match[3]?.trim(),
};
}
function buildWikiDisplayText(link) {
if (link.alias && link.alias.length > 0) {
return link.alias;
}
if (link.path && link.anchor) {
return `${link.path}#${link.anchor}`;
}
if (link.path) {
return link.path;
}
return link.anchor ?? '';
}
function appendMarkdownExtension(pathValue) {
if (/\.[A-Za-z0-9_-]+$/.test(pathValue)) {
return pathValue;
}
return `${pathValue}.md`;
}
function buildWikiHref(link) {
if (!link.path) {
if (!link.anchor) {
return '#';
}
return `#${slugifyMarkdownHeading(link.anchor)}`;
}
const normalizedPath = appendMarkdownExtension(normalizePathSeparators(link.path));
const prefixedPath = normalizedPath.startsWith('./')
|| normalizedPath.startsWith('../')
|| normalizedPath.startsWith('/')
|| isWindowsAbsolutePath(normalizedPath)
? normalizedPath
: `./${normalizedPath}`;
const encodedPath = encodeLinkPath(prefixedPath);
if (!link.anchor) {
return encodedPath;
}
return `${encodedPath}#${slugifyMarkdownHeading(link.anchor)}`;
}
function rewriteWikiLinksInPlainText(segment) {
return segment.replace(WIKI_LINK_PATTERN, (match) => {
const parsed = parseWikiLink(match);
if (!parsed) {
return match;
}
const displayText = buildWikiDisplayText(parsed);
const href = buildWikiHref(parsed);
return `[${displayText}](${href} "mcp-wiki:${encodeURIComponent(match)}")`;
});
}
function replaceWikiLinksOutsideInlineCode(line) {
let result = '';
let cursor = 0;
while (cursor < line.length) {
const codeStart = line.indexOf('`', cursor);
if (codeStart === -1) {
result += rewriteWikiLinksInPlainText(line.slice(cursor));
break;
}
result += rewriteWikiLinksInPlainText(line.slice(cursor, codeStart));
let delimiterEnd = codeStart;
while (delimiterEnd < line.length && line[delimiterEnd] === '`') {
delimiterEnd += 1;
}
const delimiter = line.slice(codeStart, delimiterEnd);
const codeEnd = line.indexOf(delimiter, delimiterEnd);
if (codeEnd === -1) {
result += line.slice(codeStart);
break;
}
result += line.slice(codeStart, codeEnd + delimiter.length);
cursor = codeEnd + delimiter.length;
}
return result;
}
function decodeAnchorFragment(fragment) {
if (!fragment || fragment.length === 0) {
return undefined;
}
return safeDecodeURIComponent(fragment);
}
function splitHref(rawHref) {
const hashIndex = rawHref.indexOf('#');
if (hashIndex === -1) {
return { pathPart: rawHref };
}
return {
pathPart: rawHref.slice(0, hashIndex),
anchorPart: rawHref.slice(hashIndex + 1),
};
}
function toDirectoryFileUrl(directoryPath) {
const normalized = normalizeFilePath(directoryPath);
const withTrailingSlash = normalized.endsWith('/') ? normalized : `${normalized}/`;
if (isWindowsAbsolutePath(withTrailingSlash)) {
return new URL(`file:///${encodeLinkPath(withTrailingSlash)}`);
}
if (withTrailingSlash.startsWith('/')) {
return new URL(`file://${encodeLinkPath(withTrailingSlash)}`);
}
return new URL(`file:///${encodeLinkPath(withTrailingSlash)}`);
}
function fromFileUrl(url) {
const decodedPath = safeDecodeURIComponent(url.pathname);
if (/^\/[A-Za-z]:\//.test(decodedPath)) {
return decodedPath.slice(1);
}
return decodedPath;
}
function isExternalHref(rawHref) {
return /^[A-Za-z][A-Za-z0-9+.-]*:/.test(rawHref) && !isWindowsAbsolutePath(rawHref);
}
function resolveFileTargetPath(currentPath, rawPath) {
const normalizedRawPath = normalizePathSeparators(safeDecodeURIComponent(rawPath));
if (normalizedRawPath.startsWith('/') || isWindowsAbsolutePath(normalizedRawPath)) {
return normalizeFilePath(normalizedRawPath);
}
const baseDirectory = getParentDirectory(currentPath);
if (baseDirectory === '.' && !normalizeFilePath(currentPath).includes('/')) {
return normalizeFilePath(normalizedRawPath);
}
const resolvedUrl = new URL(encodeURI(normalizedRawPath), toDirectoryFileUrl(baseDirectory));
return normalizeFilePath(fromFileUrl(resolvedUrl));
}
/**
* Invert `rewriteWikiLinks`: convert `[alias](href "mcp-wiki:ENCODED")` links
* back to their original `[[...]]` form. Used when serializing a WYSIWYG
* edit session back to markdown — the `mcp-wiki:` title prefix is the
* round-trip marker written by `rewriteWikiLinks`.
*/
export function restoreWikiLinks(markdown) {
return markdown.replace(/\[([^\]]*)\]\(([^)\s]*)(?:\s+"mcp-wiki:([^"]+)")\)/g, (_, _alias, _href, encoded) => {
try {
return decodeURIComponent(encoded);
}
catch {
return `[[${encoded}]]`;
}
});
}
export function rewriteWikiLinks(source) {
const lines = source.split('\n');
let activeFence = null;
return lines.map((line) => {
const trimmedStart = line.trimStart();
const fenceMatch = trimmedStart.match(FENCE_PATTERN);
if (fenceMatch) {
const marker = fenceMatch[1];
if (!activeFence) {
activeFence = marker;
}
else if (marker[0] === activeFence[0] && marker.length >= activeFence.length) {
activeFence = null;
}
return line;
}
if (activeFence) {
return line;
}
return replaceWikiLinksOutsideInlineCode(line);
}).join('\n');
}
export function resolveMarkdownLink(currentPath, rawHref) {
const wikiLink = parseWikiLink(rawHref);
if (wikiLink) {
const href = buildWikiHref(wikiLink);
if (href.startsWith('#')) {
return {
kind: 'anchor',
href: rawHref,
anchor: decodeAnchorFragment(href.slice(1)),
};
}
const [pathPart, anchorPart] = href.split('#');
return {
kind: 'file',
href: rawHref,
targetPath: resolveFileTargetPath(currentPath, pathPart),
...(anchorPart ? { anchor: decodeAnchorFragment(anchorPart) } : {}),
};
}
if (isExternalHref(rawHref)) {
return {
kind: 'external',
href: rawHref,
url: rawHref,
};
}
if (rawHref.startsWith('#')) {
return {
kind: 'anchor',
href: rawHref,
anchor: decodeAnchorFragment(rawHref.slice(1)),
};
}
const { pathPart, anchorPart } = splitHref(rawHref);
return {
kind: 'file',
href: rawHref,
targetPath: resolveFileTargetPath(currentPath, pathPart),
...(anchorPart ? { anchor: decodeAnchorFragment(anchorPart) } : {}),
};
}