/** * -------------------------------------------------------------------- * docmd : the zero-config documentation engine. * * @package @docmd/parser * @website https://docmd.io * @repository https://github.com/docmd-io/docmd * @license MIT * @copyright Copyright (c) 2025-present docmd.io * * [docmd-source] - Please do not remove this header. * -------------------------------------------------------------------- */ /** * Centralised URL Utilities * ========================= * * This module is the **single source of truth** for all URL transformations * in the docmd ecosystem. Every plugin, template, and engine component * MUST use these utilities instead of rolling their own URL logic. * * Architecture: * * User Input (markdown, config) * │ * ▼ * resolveHref() ← normalize-href.ts (user-facing href → clean path) * │ * ▼ * Build Engine ← generator.ts produces outputPath per page * │ * ▼ * URL Utilities ← THIS FILE * │ * ├── outputPathToSlug() → "guide/" * ├── outputPathToCanonical() → "https://site.com/guide/" * ├── buildContextualUrl() → "../de/guide/" (relative, context-aware) * ├── sanitizeUrl() → collapse //, enforce trailing / * └── createUrlContext() → factory for page-level context * * Plugins receive pre-computed URLs via the page object, OR can import * these utilities directly for custom URL generation. */ import { sanitizeUrl } from './normalize-href.js'; export { sanitizeUrl }; /** * Immutable context object that captures all the environmental factors * needed to resolve a URL for a specific page render. * * Created once per page in generator.ts and passed to all templates * and plugin hooks. */ export interface UrlContext { /** Relative path from current page back to site root, e.g. `../../` or `./` */ readonly relativePathToRoot: string; /** Locale + version prefix for the current build pass, e.g. `de/v1/` or `` */ readonly outputPrefix: string; /** Whether we're generating for offline/file:// browsing */ readonly offline: boolean; /** The site base path from config, e.g. `/docs/` or `/` */ readonly base: string; /** The full site URL from config, e.g. `https://docmd.io` (no trailing slash) */ readonly siteUrl: string; /** * Path prefix for emitting asset URLs. Empty string when a `` tag * is being emitted (assets resolve against base via simple-relative paths), * otherwise equals relativePathToRoot so assets use page-depth-aware paths * (used by dev and --offline where no is emitted). */ readonly assetBaseUrl: string; /** Whether the renderer is emitting a `` tag for this page. */ readonly emitBase: boolean; /** The root-relative pathname of the current page, e.g. `/guide/` or `/project1/sub1/file1/` */ readonly pathname?: string; /** The prefix of the active project in the workspace, e.g. `/semantic` or `/` */ readonly projectPrefix?: string; /** All projects in the workspace */ readonly workspaceProjects?: readonly any[]; } /** * Pre-computed URL data attached to every page object. * Plugins can read these directly - zero computation needed. */ export interface PageUrls { /** Clean directory-style slug, e.g. `guide/` or `/` for root */ readonly slug: string; /** Full canonical URL, e.g. `https://docmd.io/guide/` (only if siteUrl is set) */ readonly canonical: string; /** Relative path from site root, e.g. `/guide/` or `/` */ readonly pathname: string; } /** * Collapse consecutive slashes (except after protocol `:`), enforce * consistent formatting. This is the **last-resort safety net** - if * the upstream logic is correct, this should be a no-op. * * Note: This function is imported from normalize-href.ts to ensure * single source of truth for URL sanitization logic. */ /** * Convert a build-engine outputPath to a clean directory-style slug. * * This is the **single canonical conversion** from the internal file path * representation to a URL path segment. Every consumer that previously * did its own `outputPath.replace('/index.html', '/')` MUST use this. * * @param outputPath - e.g. `guide/index.html`, `index.html`, `de/v1/api/index.html` * @returns Clean slug, e.g. `guide/`, `/`, `de/v1/api/` * * @example * outputPathToSlug('guide/index.html') → 'guide/' * outputPathToSlug('index.html') → '/' * outputPathToSlug('de/v1/api/index.html') → 'de/v1/api/' * outputPathToSlug('about.html') → 'about/' */ export declare function outputPathToSlug(outputPath: string): string; /** * Convert an outputPath to a root-relative pathname (always starts with `/`). * * @param outputPath - e.g. `guide/index.html` * @returns e.g. `/guide/` */ export declare function outputPathToPathname(outputPath: string, base?: string): string; /** * Convert an outputPath to a full canonical URL. * * @param outputPath - e.g. `guide/index.html` * @param siteUrl - e.g. `https://docmd.io` (no trailing slash) * @returns e.g. `https://docmd.io/guide/` */ export declare function outputPathToCanonical(outputPath: string, siteUrl: string, base?: string): string; /** * Build a context-aware relative URL from a clean href. * * This replaces ALL inline URL building in EJS templates and the * `buildRelativeUrl` function in generator.ts. It is the single * function that understands relativePathToRoot, outputPrefix, * offline mode, and base path. * * @param href - A clean, normalised href (output of resolveHref), e.g. `guide/`, `#section`, `https://...` * @param context - The UrlContext for the current page * @returns A fully resolved relative URL safe for use in `` * * @example * // Page at /de/v1/getting-started/index.html * buildContextualUrl('guide/', ctx) * // → '../../de/v1/guide/' (relative, with locale+version prefix) * * buildContextualUrl('#section', ctx) * // → '#section' (hash-only, untouched) * * buildContextualUrl('https://github.com', ctx) * // → 'https://github.com' (external, untouched) */ export declare function buildContextualUrl(href: string, context: UrlContext): string; /** * Build a **root-relative** URL from a clean href, ignoring the current * locale/version `outputPrefix`. * * This is the user-content counterpart to `buildContextualUrl`. Markdown * authors write links like `[link](/guide/)` meaning the site root, not * the current locale section. `buildContextualUrl` would prepend the * locale prefix (correct for system nav, wrong for author intent); this * function deliberately drops `outputPrefix` so the link resolves to the * same place regardless of which locale the page lives under. * * Shares the exact same offline / clean-URL / external / hash logic as * `buildContextualUrl` — there is exactly one implementation of each rule. * * @param href - A clean href, e.g. `guide/`, `/guide/`, `#section`, `https://...` * @param context - The UrlContext for the current page (outputPrefix is ignored) * @returns A file://-safe relative URL rooted at the site root */ export declare function buildRootRelativeUrl(href: string, context: UrlContext): string; /** * Strip the default-locale prefix from absolute `` URLs in a * rendered HTML fragment. * * When the default locale lives at root (e.g. `en/` → `/`), an author * writing `[link](/en/foo)` from a `fr/` page would produce a 404 because * `/en/foo` doesn't exist — the default locale is at `/foo`. This rewrites * `/en/foo` → `/foo` for every `` whose first segment matches the * default locale id. * * Only `` is touched (never `` — assets are never * locale-prefixed). External URLs, anchors, and non-locale absolute paths * are passed through unchanged. * * @param html - The rendered HTML fragment to post-process * @param defaultLocale - The default locale id, e.g. `en` * @returns The HTML with default-locale prefixes stripped from absolute hrefs */ export declare function stripDefaultLocalePrefixFromHtml(html: string, defaultLocale: string): string; /** * Walk every `` and `` in a rendered HTML * fragment and route each internal URL through `buildRootRelativeUrl`. * * This is the **single HTML post-processor** for user-authored content * (markdown bodies, button containers). It replaces the former parallel * implementations: `fixHtmlLinks` (html-renderer.ts) and * `rewriteInternalHrefsForOffline` (markdown-processor.ts). All URL * rules — offline `index.html` suffixing, clean-URL collapsing, external * pass-through, hash preservation, base-path stripping — live in exactly * one place: `buildContextualUrl` via `buildRootRelativeUrl`. * * When `opts.defaultLocale` and `opts.allLocales` are supplied, the * default-locale prefix is stripped from absolute hrefs first (see * `stripDefaultLocalePrefixFromHtml`). * * @param html - The rendered HTML fragment * @param context - The UrlContext for the current page * @param opts - Optional locale info for default-locale prefix stripping * @returns The HTML with all internal links rewritten for the current context */ export declare function rewriteHtmlLinks(html: string, context: UrlContext, opts?: { defaultLocale?: string | null; allLocales?: readonly string[]; }): string; /** * Create a UrlContext for a specific page render. * * Called once per page in generator.ts. The resulting context is then * passed to all templates and can be forwarded to plugin hooks. * * @param options - Configuration for this page's URL context */ export declare function createUrlContext(options: { relativePathToRoot: string; outputPrefix?: string; offline?: boolean; base?: string; siteUrl?: string; pathname?: string; projectPrefix?: string; workspaceProjects?: readonly any[]; }): UrlContext; /** * Compute pre-built URL data for a page. * * Called once per page in generator.ts. The resulting PageUrls object * is attached to the page object and available to all post-build plugins. * * @param outputPath - The page's output path, e.g. `guide/index.html` * @param siteUrl - The site URL from config, e.g. `https://docmd.io` */ export declare function computePageUrls(outputPath: string, siteUrl: string, base?: string): PageUrls; /** * Build an absolute URL from config.base + optional locale + optional version + page path. * * Used by version-dropdown.ejs and language-switcher.ejs for absolute navigation. * Replaces the inline JS computations in those templates. * * @param base - config.base, e.g. `/docs/` or `/` * @param localePrefix - e.g. `de/` or `` for default locale * @param versionPrefix - e.g. `v1/` or `` for current version * @param pagePath - e.g. `guide/` or `` * @returns Absolute path, e.g. `/docs/de/v1/guide/` */ export declare function buildAbsoluteUrl(base: string, localePrefix?: string, versionPrefix?: string, pagePath?: string): string; /** * Context-aware variant of `buildAbsoluteUrl` for cross-locale/version * navigation links (version dropdown, language switcher, project switcher). * * When the current render context is NOT offline, this is identical to * `buildAbsoluteUrl` — it returns a clean absolute path (`/de/v1/guide/`). * * When the context IS offline, it returns a **relative** URL rooted at the * current page's `relativePathToRoot`, with `index.html` appended for every * directory-style segment. Without this, version/language switcher links in * `--offline` builds emitted bare `/de/` hrefs that resolved to filesystem * directories instead of index.html files under `file://` (#179). * * The base path is stripped from the computed absolute path before being * re-rooted so that custom sub-path deploys (`base: '/docs/'`) still work. * * @param base - config.base, e.g. `/docs/` or `/` * @param localePrefix - e.g. `de/` or `` for default locale * @param versionPrefix - e.g. `v1/` or `` * @param pagePath - e.g. `guide/` or `` (root of the target prefix) * @param context - The UrlContext for the current page render * @returns Either the absolute path (non-offline) or a relative file://-safe path (offline) * * @example * // Non-offline build: * buildAbsoluteContextualUrl('/', 'de/', '', 'guide/', ctx) * // → '/de/guide/' * * // Offline build, page at /en/api/index.html (relativePathToRoot = '../../'): * buildAbsoluteContextualUrl('/', 'de/', '', 'guide/', ctx) * // → '../../de/guide/index.html' * * // Offline build, switching to the root of the site: * buildAbsoluteContextualUrl('/', '', '', '', ctx) * // → './index.html' */ export declare function buildAbsoluteContextualUrl(base: string, localePrefix?: string, versionPrefix?: string, pagePath?: string, context?: UrlContext): string; /** * Normalise the `` tag in a fully-rendered HTML document. * * The generator is the single source of truth for the `` tag. Templates * are NOT allowed to emit one themselves — anything a template emits is * stripped here and replaced with the canonical decision: * * • offline mode (`--offline`) → no `` tag (file:// re-roots) * • root deploy (`siteRootAbs === '/'`) → no `` tag (relative URLs * resolve against document path) * • subpath deploy (`siteRootAbs !== '/'`) → `` * * Implementation notes: * * • Removes ALL existing `` tags (any source: template, partial, * stray plugin output) using a regex that handles attributes in any * order, self-closing forms, single/double quotes. * • When emitting, inserts the canonical `` immediately after the * `` tag so it applies to all subsequent `<link>`, `<script>`, * and asset references in the document head. * • Idempotent: running it twice is a no-op. * * Centralising this means old templates (pre-0.8.13 with unconditional * `<base href="/">`), the default template, and any third-party template * all produce the same correct output. Templates do not need to know about * base tags at all. * * @param html - The fully-rendered HTML string from the layout template * @param isOffline - Whether the build was invoked with `--offline` * @param siteRootAbs - The absolute subpath root, e.g. `'/'`, `'/repo/'`, `'/docs/v1/'` * @returns - The HTML with the canonical `<base>` (or none) in place */ export declare function normaliseBaseTag(html: string, isOffline: boolean, siteRootAbs: string): string;