/** * Represents a reference to a page, with optional position, anchor and header. */ export type PageRef = { page: string; pos?: number | { line: number; column: number }; anchor?: string; header?: string; meta?: boolean; }; /** * Checks if a name looks like a full path (with a file extension), is not a conflicted file and not a search page. */ export function looksLikePathWithExtension(name: string): boolean { return /\.[a-zA-Z0-9]+$/.test(name) && !/\.conflicted\./.test(name) && !name.startsWith("🔍 "); } /** * Checks if a name looks like a full path (with a file extension), is not a conflicted file and not a search page. */ export function validatePageName(name: string) { // Page can not be empty and not end with a file extension (e.g. "bla.md") if (name === "") { throw new Error("Page name can not be empty"); } if (name.startsWith(".")) { throw new Error("Page name cannot start with a '.'"); } if (looksLikePathWithExtension(name)) { throw new Error("Page name can not end with a file extension"); } } const posRegex = /@(\d+)$/; const linePosRegex = /@[Ll](\d+)(?:[Cc](\d+))?$/; // column is optional, implicit 1 const anchorRegex = /\$([a-zA-Z\.\-\/]+[\w\.\-\/]*)$/; const headerRegex = /#([^#]*)$/; /** * Parses a page reference string into a PageRef object. * @param name the name of the page reference to parse * @returns the parsed PageRef object */ export function parsePageRef(name: string): PageRef { // Normalize the page name if (name.startsWith("[[") && name.endsWith("]]")) { name = name.slice(2, -2); } const pageRef: PageRef = { page: name }; if (pageRef.page.startsWith("^")) { // A caret prefix means we're looking for a meta page, but that doesn't matter for most use cases pageRef.page = pageRef.page.slice(1); pageRef.meta = true; } const posMatch = pageRef.page.match(posRegex); if (posMatch) { pageRef.pos = parseInt(posMatch[1]); pageRef.page = pageRef.page.replace(posRegex, ""); } const linePosMatch = pageRef.page.match(linePosRegex); if (linePosMatch) { pageRef.pos = { line: parseInt(linePosMatch[1]), column: 1 }; if (linePosMatch[2]) { pageRef.pos.column = parseInt(linePosMatch[2]); } pageRef.page = pageRef.page.replace(linePosRegex, ""); } const anchorMatch = pageRef.page.match(anchorRegex); if (anchorMatch) { pageRef.anchor = anchorMatch[1]; pageRef.page = pageRef.page.replace(anchorRegex, ""); } const headerMatch = pageRef.page.match(headerRegex); if (headerMatch) { pageRef.header = headerMatch[1]; pageRef.page = pageRef.page.replace(headerRegex, ""); } return pageRef; } /** * The inverse of parsePageRef, encodes a PageRef object into a string. * @param pageRef the page reference to encode * @returns a string representation of the page reference */ export function encodePageRef(pageRef: PageRef): string { let name = pageRef.page; if (pageRef.pos) { if (pageRef.pos instanceof Object) { name += `@L${pageRef.pos.line}`; if (pageRef.pos.column !== 1) { name += `C${pageRef.pos.column}`; } } else { // just a number name += `@${pageRef.pos}`; } } if (pageRef.anchor) { name += `$${pageRef.anchor}`; } if (pageRef.header) { name += `#${pageRef.header}`; } return name; } /** * Translate line and column number (counting from 1) to position in text (counting from 0) */ export function positionOfLine( text: string, line: number, column: number, ): number { const lines = text.split("\n"); let targetLine = ""; let targetPos = 0; for (let i = 0; i < line && lines.length; i++) { targetLine = lines[i]; targetPos += targetLine.length; } // How much to move inside the line, column number starts from 1 const columnOffset = Math.max( 0, Math.min(targetLine.length, column - 1), ); return targetPos - targetLine.length + columnOffset; } /** * Encodes a page name for use in a URI. Basically does encodeURIComponent, but puts slashes back in place. * @param page page name to encode * @returns */ export function encodePageURI(page: string): string { return encodeURIComponent(page).replace(/%2F/g, "/"); } /** * Decodes a page name from a URI. * @param page page name to decode * @returns */ export function decodePageURI(page: string): string { return decodeURIComponent(page); }