/**
* --------------------------------------------------------------------
* 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 ``, `