import type MarkdownIt from "markdown-it"; import type StateInline from "markdown-it/lib/rules_inline/state_inline.mjs"; import { escapeHtml } from "../html.ts"; import { slugify } from "../slug.ts"; import { noteUrl, wikiUrl } from "../urls.ts"; export interface WikilinkEnv { wikiSlug?: string; ownerHandle?: string; } export interface WikilinkTarget { ownerHandle?: string; wikiSlug?: string; noteSlug: string; label: string; section?: string; } export function parseWikilinkTarget( content: string, currentWikiSlug?: string, currentOwnerHandle?: string, ): WikilinkTarget | null { const trimmed = content.trim(); if (!trimmed) return null; const pipeIdx = trimmed.indexOf("|"); const beforePipe = ( pipeIdx >= 0 ? trimmed.slice(0, pipeIdx) : trimmed ).trim(); const rawLabel = (pipeIdx >= 0 ? trimmed.slice(pipeIdx + 1) : "").trim(); const hashIdx = beforePipe.indexOf("#"); const rawTarget = ( hashIdx >= 0 ? beforePipe.slice(0, hashIdx) : beforePipe ).trim(); const section = hashIdx >= 0 ? beforePipe.slice(hashIdx + 1).trim() : undefined; // [[#section]] — anchor-only link in the same note if (!rawTarget && section) { return { noteSlug: "", label: rawLabel || section, section }; } if (!rawTarget) return null; const sectionSuffix = section ? `#${section}` : ""; // [[@handle/wiki/note]] or [[@handle/wiki/]] — explicit cross-user link if (rawTarget.startsWith("@")) { const withoutAt = rawTarget.slice(1); const firstSlash = withoutAt.indexOf("/"); if (firstSlash > 0) { const handle = withoutAt.slice(0, firstSlash); const rest = withoutAt.slice(firstSlash + 1); const secondSlash = rest.indexOf("/"); if (secondSlash > 0) { const wikiSlug = rest.slice(0, secondSlash); const noteSlug = rest.slice(secondSlash + 1).trim(); const defaultLabel = noteSlug ? `@${handle}/${wikiSlug}/${noteSlug}${sectionSuffix}` : `@${handle}/${wikiSlug}`; return { ownerHandle: handle, wikiSlug, noteSlug, label: rawLabel || defaultLabel, ...(section ? { section } : {}), }; } // [[@handle/wiki]] — wiki home const wikiSlug = rest.trim(); return { ownerHandle: handle, wikiSlug, noteSlug: "", label: rawLabel || `@${handle}/${wikiSlug}`, ...(section ? { section } : {}), }; } return null; } // [[note]] or [[note#section]] — note in the same wiki // Slugs never contain "/"; reject anything with a slash (use [[@handle/wiki/note]] instead) if (rawTarget.includes("/")) return null; return { ...(currentOwnerHandle ? { ownerHandle: currentOwnerHandle } : {}), ...(currentWikiSlug ? { wikiSlug: currentWikiSlug } : {}), noteSlug: rawTarget, label: rawLabel || `${rawTarget}${sectionSuffix}`, ...(section ? { section } : {}), }; } export function wikilinkPlugin(mdi: MarkdownIt): void { mdi.inline.ruler.push("wikilink", (state: StateInline, silent: boolean) => { const src = state.src; const pos = state.pos; if (src[pos] !== "[" || src[pos + 1] !== "[") return false; const closeIdx = src.indexOf("]]", pos + 2); if (closeIdx === -1) return false; if (!silent) { const env = state.env as WikilinkEnv; const target = parseWikilinkTarget( src.slice(pos + 2, closeIdx), env.wikiSlug, env.ownerHandle, ); if (!target) return false; const anchor = target.section ? `#${slugify(target.section)}` : ""; const noteSlug = target.noteSlug ? slugify(target.noteSlug) : ""; const wikiSlug = target.wikiSlug ? slugify(target.wikiSlug) : ""; let href: string; if (!wikiSlug && !noteSlug && target.section) { // [[#section]] — anchor only href = anchor; } else if (!noteSlug && wikiSlug && target.ownerHandle) { // [[@handle/wiki/]] — wiki home page href = wikiUrl(target.ownerHandle, wikiSlug) + anchor; } else if (wikiSlug && target.ownerHandle) { // [[@handle/wiki/note]] href = noteUrl(target.ownerHandle, wikiSlug, noteSlug) + anchor; } else { // [[note]] or [[note#section]] — bare slug in same wiki href = noteSlug + anchor; } const token = state.push("html_inline", "", 0); token.content = `${escapeHtml(target.label)}`; } state.pos = closeIdx + 2; return true; }); }