diff --git a/docs/astro.config.mjs b/docs/astro.config.mjs
index e5eda0bd..6a8a8c9d 100644
--- a/docs/astro.config.mjs
+++ b/docs/astro.config.mjs
@@ -1,11 +1,15 @@
// @ts-check
import { defineConfig } from 'astro/config';
import starlight from '@astrojs/starlight';
+import rehypeBaseUrl from '../packages/core/src/codegen/rehype-base-url.mjs';
// https://astro.build/config
export default defineConfig({
site: 'https://intentius.io',
base: '/chant',
+ markdown: {
+ rehypePlugins: [[rehypeBaseUrl, { base: '/chant' }]],
+ },
integrations: [
starlight({
title: 'chant',
diff --git a/justfile b/justfile
index 2d3f8b3f..7678b722 100644
--- a/justfile
+++ b/justfile
@@ -65,6 +65,20 @@ docs-build:
docs-serve: docs-build
npx serve .docs-dist
+# Check internal doc links across the unified site (requires lychee: brew install lychee)
+docs-check-links: docs-build
+ #!/usr/bin/env bash
+ set -euo pipefail
+ if ! command -v lychee >/dev/null 2>&1; then
+ echo "lychee not installed. Install with: brew install lychee" >&2
+ exit 127
+ fi
+ lychee --offline --no-progress \
+ --root-dir "$PWD/.docs-dist" \
+ --exclude '\.(css|js|mjs|svg|png|jpe?g|ico|woff2?|map|json|xml|webp|avif|gif)$' \
+ --exclude 'pagefind/' \
+ '.docs-dist/chant/**/*.html'
+
# Build VS Code extension
ext-vscode-build:
cd editors/vscode && npm install && npm run build
diff --git a/lexicons/aws/docs/astro.config.mjs b/lexicons/aws/docs/astro.config.mjs
index ae9be3bc..67766a0e 100644
--- a/lexicons/aws/docs/astro.config.mjs
+++ b/lexicons/aws/docs/astro.config.mjs
@@ -1,9 +1,13 @@
// @ts-check
import { defineConfig } from 'astro/config';
import starlight from '@astrojs/starlight';
+import rehypeBaseUrl from './src/rehype-base-url.mjs';
export default defineConfig({
base: '/chant/lexicons/aws/',
+ markdown: {
+ rehypePlugins: [[rehypeBaseUrl, { base: '/chant/lexicons/aws/', projectBase: '/chant' }]],
+ },
integrations: [
starlight({
title: 'AWS CloudFormation',
diff --git a/lexicons/aws/docs/src/content/docs/index.mdx b/lexicons/aws/docs/src/content/docs/index.mdx
index f2c5f478..f1435630 100644
--- a/lexicons/aws/docs/src/content/docs/index.mdx
+++ b/lexicons/aws/docs/src/content/docs/index.mdx
@@ -17,9 +17,9 @@ npm install --save-dev @intentius/chant-lexicon-aws
| Metric | Count |
|--------|-------|
-| Resources | 1,500+ |
-| Property types | 13,000+ |
-| Services | 270+ |
+| Resources | 1592 |
+| Property types | 13357 |
+| Services | 278 |
| Intrinsic functions | 9 |
| Pseudo-parameters | 8 |
| Lint rules | 32 |
diff --git a/lexicons/aws/docs/src/rehype-base-url.mjs b/lexicons/aws/docs/src/rehype-base-url.mjs
new file mode 100644
index 00000000..b92d422a
--- /dev/null
+++ b/lexicons/aws/docs/src/rehype-base-url.mjs
@@ -0,0 +1,68 @@
+/**
+ * Rehype plugin: prepend a configured `base` to root-relative `` attributes.
+ *
+ * Astro/Starlight only base-prefixes its own internal navigation (sidebar `link:`
+ * entries, `slug:` entries). Root-relative links written in MD/MDX content body
+ * — e.g. `[AWS](/lexicons/aws/)` — are emitted verbatim and 404 in production
+ * when the site is served from a non-root `base`.
+ *
+ * This plugin walks the HAST tree, finds `` elements whose href starts with
+ * `/` (single leading slash, not `//`), and prepends the site's `base`. It
+ * idempotently skips hrefs that already start with the site's own base or the
+ * project-wide base.
+ *
+ * @typedef {Object} RehypeBaseUrlOptions
+ * @property {string} base - Site base, e.g. "/chant" or "/chant/lexicons/aws". Trailing/leading slashes optional.
+ * @property {string} [projectBase] - Project-wide base used to detect already-correctly-prefixed cross-site links.
+ */
+
+const PROTOCOL_RE = /^[a-z][a-z0-9+.-]*:/i;
+
+function normalizeBase(value) {
+ return "/" + value.replace(/^\/+|\/+$/g, "");
+}
+
+/**
+ * @param {RehypeBaseUrlOptions} opts
+ */
+export default function rehypeBaseUrl(opts) {
+ const base = normalizeBase(opts.base);
+ if (base === "/") {
+ return () => {};
+ }
+ const ownPrefix = base + "/";
+ const projectPrefix = opts.projectBase
+ ? normalizeBase(opts.projectBase) + "/"
+ : null;
+
+ function rewrite(node) {
+ if (
+ node &&
+ node.type === "element" &&
+ node.tagName === "a" &&
+ node.properties &&
+ typeof node.properties.href === "string"
+ ) {
+ const href = node.properties.href;
+ if (
+ href.length > 0 &&
+ !href.startsWith("//") &&
+ !PROTOCOL_RE.test(href) &&
+ !href.startsWith("#") &&
+ href.startsWith("/") &&
+ href !== base &&
+ !href.startsWith(ownPrefix) &&
+ !(projectPrefix && href.startsWith(projectPrefix))
+ ) {
+ node.properties.href = base + href;
+ }
+ }
+ if (node && node.children) {
+ for (const child of node.children) rewrite(child);
+ }
+ }
+
+ return (tree) => {
+ rewrite(tree);
+ };
+}
diff --git a/lexicons/azure/docs/astro.config.mjs b/lexicons/azure/docs/astro.config.mjs
index 5591c83a..8dcc718e 100644
--- a/lexicons/azure/docs/astro.config.mjs
+++ b/lexicons/azure/docs/astro.config.mjs
@@ -1,9 +1,13 @@
// @ts-check
import { defineConfig } from 'astro/config';
import starlight from '@astrojs/starlight';
+import rehypeBaseUrl from './src/rehype-base-url.mjs';
export default defineConfig({
base: '/chant/lexicons/azure/',
+ markdown: {
+ rehypePlugins: [[rehypeBaseUrl, { base: '/chant/lexicons/azure/', projectBase: '/chant' }]],
+ },
integrations: [
starlight({
title: 'Azure Resource Manager',
diff --git a/lexicons/azure/docs/src/content/docs/index.mdx b/lexicons/azure/docs/src/content/docs/index.mdx
index a7e11351..43c30c8c 100644
--- a/lexicons/azure/docs/src/content/docs/index.mdx
+++ b/lexicons/azure/docs/src/content/docs/index.mdx
@@ -17,9 +17,9 @@ npm install --save-dev @intentius/chant-lexicon-azure
| Metric | Count |
|--------|-------|
-| Resources | 1,900+ |
-| Property types | 300,000+ |
-| Services | 230+ |
+| Resources | 2039 |
+| Property types | 312218 |
+| Services | 243 |
| Intrinsic functions | 9 |
| Pseudo-parameters | 6 |
| Lint rules | 23 |
diff --git a/lexicons/azure/docs/src/rehype-base-url.mjs b/lexicons/azure/docs/src/rehype-base-url.mjs
new file mode 100644
index 00000000..b92d422a
--- /dev/null
+++ b/lexicons/azure/docs/src/rehype-base-url.mjs
@@ -0,0 +1,68 @@
+/**
+ * Rehype plugin: prepend a configured `base` to root-relative `` attributes.
+ *
+ * Astro/Starlight only base-prefixes its own internal navigation (sidebar `link:`
+ * entries, `slug:` entries). Root-relative links written in MD/MDX content body
+ * — e.g. `[AWS](/lexicons/aws/)` — are emitted verbatim and 404 in production
+ * when the site is served from a non-root `base`.
+ *
+ * This plugin walks the HAST tree, finds `` elements whose href starts with
+ * `/` (single leading slash, not `//`), and prepends the site's `base`. It
+ * idempotently skips hrefs that already start with the site's own base or the
+ * project-wide base.
+ *
+ * @typedef {Object} RehypeBaseUrlOptions
+ * @property {string} base - Site base, e.g. "/chant" or "/chant/lexicons/aws". Trailing/leading slashes optional.
+ * @property {string} [projectBase] - Project-wide base used to detect already-correctly-prefixed cross-site links.
+ */
+
+const PROTOCOL_RE = /^[a-z][a-z0-9+.-]*:/i;
+
+function normalizeBase(value) {
+ return "/" + value.replace(/^\/+|\/+$/g, "");
+}
+
+/**
+ * @param {RehypeBaseUrlOptions} opts
+ */
+export default function rehypeBaseUrl(opts) {
+ const base = normalizeBase(opts.base);
+ if (base === "/") {
+ return () => {};
+ }
+ const ownPrefix = base + "/";
+ const projectPrefix = opts.projectBase
+ ? normalizeBase(opts.projectBase) + "/"
+ : null;
+
+ function rewrite(node) {
+ if (
+ node &&
+ node.type === "element" &&
+ node.tagName === "a" &&
+ node.properties &&
+ typeof node.properties.href === "string"
+ ) {
+ const href = node.properties.href;
+ if (
+ href.length > 0 &&
+ !href.startsWith("//") &&
+ !PROTOCOL_RE.test(href) &&
+ !href.startsWith("#") &&
+ href.startsWith("/") &&
+ href !== base &&
+ !href.startsWith(ownPrefix) &&
+ !(projectPrefix && href.startsWith(projectPrefix))
+ ) {
+ node.properties.href = base + href;
+ }
+ }
+ if (node && node.children) {
+ for (const child of node.children) rewrite(child);
+ }
+ }
+
+ return (tree) => {
+ rewrite(tree);
+ };
+}
diff --git a/lexicons/docker/docs/astro.config.mjs b/lexicons/docker/docs/astro.config.mjs
index 942c6566..569c283e 100644
--- a/lexicons/docker/docs/astro.config.mjs
+++ b/lexicons/docker/docs/astro.config.mjs
@@ -1,9 +1,13 @@
// @ts-check
import { defineConfig } from 'astro/config';
import starlight from '@astrojs/starlight';
+import rehypeBaseUrl from '../../../packages/core/src/codegen/rehype-base-url.mjs';
export default defineConfig({
base: '/chant/lexicons/docker/',
+ markdown: {
+ rehypePlugins: [[rehypeBaseUrl, { base: '/chant/lexicons/docker/', projectBase: '/chant' }]],
+ },
integrations: [
starlight({
title: 'Docker',
diff --git a/lexicons/flyway/docs/astro.config.mjs b/lexicons/flyway/docs/astro.config.mjs
index fd38f6e9..0ed3e1cd 100644
--- a/lexicons/flyway/docs/astro.config.mjs
+++ b/lexicons/flyway/docs/astro.config.mjs
@@ -1,9 +1,13 @@
// @ts-check
import { defineConfig } from 'astro/config';
import starlight from '@astrojs/starlight';
+import rehypeBaseUrl from './src/rehype-base-url.mjs';
export default defineConfig({
base: '/chant/lexicons/flyway/',
+ markdown: {
+ rehypePlugins: [[rehypeBaseUrl, { base: '/chant/lexicons/flyway/', projectBase: '/chant' }]],
+ },
integrations: [
starlight({
title: 'Flyway',
diff --git a/lexicons/flyway/docs/src/rehype-base-url.mjs b/lexicons/flyway/docs/src/rehype-base-url.mjs
new file mode 100644
index 00000000..b92d422a
--- /dev/null
+++ b/lexicons/flyway/docs/src/rehype-base-url.mjs
@@ -0,0 +1,68 @@
+/**
+ * Rehype plugin: prepend a configured `base` to root-relative `` attributes.
+ *
+ * Astro/Starlight only base-prefixes its own internal navigation (sidebar `link:`
+ * entries, `slug:` entries). Root-relative links written in MD/MDX content body
+ * — e.g. `[AWS](/lexicons/aws/)` — are emitted verbatim and 404 in production
+ * when the site is served from a non-root `base`.
+ *
+ * This plugin walks the HAST tree, finds `` elements whose href starts with
+ * `/` (single leading slash, not `//`), and prepends the site's `base`. It
+ * idempotently skips hrefs that already start with the site's own base or the
+ * project-wide base.
+ *
+ * @typedef {Object} RehypeBaseUrlOptions
+ * @property {string} base - Site base, e.g. "/chant" or "/chant/lexicons/aws". Trailing/leading slashes optional.
+ * @property {string} [projectBase] - Project-wide base used to detect already-correctly-prefixed cross-site links.
+ */
+
+const PROTOCOL_RE = /^[a-z][a-z0-9+.-]*:/i;
+
+function normalizeBase(value) {
+ return "/" + value.replace(/^\/+|\/+$/g, "");
+}
+
+/**
+ * @param {RehypeBaseUrlOptions} opts
+ */
+export default function rehypeBaseUrl(opts) {
+ const base = normalizeBase(opts.base);
+ if (base === "/") {
+ return () => {};
+ }
+ const ownPrefix = base + "/";
+ const projectPrefix = opts.projectBase
+ ? normalizeBase(opts.projectBase) + "/"
+ : null;
+
+ function rewrite(node) {
+ if (
+ node &&
+ node.type === "element" &&
+ node.tagName === "a" &&
+ node.properties &&
+ typeof node.properties.href === "string"
+ ) {
+ const href = node.properties.href;
+ if (
+ href.length > 0 &&
+ !href.startsWith("//") &&
+ !PROTOCOL_RE.test(href) &&
+ !href.startsWith("#") &&
+ href.startsWith("/") &&
+ href !== base &&
+ !href.startsWith(ownPrefix) &&
+ !(projectPrefix && href.startsWith(projectPrefix))
+ ) {
+ node.properties.href = base + href;
+ }
+ }
+ if (node && node.children) {
+ for (const child of node.children) rewrite(child);
+ }
+ }
+
+ return (tree) => {
+ rewrite(tree);
+ };
+}
diff --git a/lexicons/gcp/docs/astro.config.mjs b/lexicons/gcp/docs/astro.config.mjs
index 8910d057..30e1765f 100644
--- a/lexicons/gcp/docs/astro.config.mjs
+++ b/lexicons/gcp/docs/astro.config.mjs
@@ -1,9 +1,13 @@
// @ts-check
import { defineConfig } from 'astro/config';
import starlight from '@astrojs/starlight';
+import rehypeBaseUrl from './src/rehype-base-url.mjs';
export default defineConfig({
base: '/chant/lexicons/gcp/',
+ markdown: {
+ rehypePlugins: [[rehypeBaseUrl, { base: '/chant/lexicons/gcp/', projectBase: '/chant' }]],
+ },
integrations: [
starlight({
title: 'GCP Config Connector',
diff --git a/lexicons/gcp/docs/src/content/docs/index.mdx b/lexicons/gcp/docs/src/content/docs/index.mdx
index bf56cd13..e321b4d0 100644
--- a/lexicons/gcp/docs/src/content/docs/index.mdx
+++ b/lexicons/gcp/docs/src/content/docs/index.mdx
@@ -40,14 +40,14 @@ The lexicon provides **300+ resource types** across Compute, Storage, IAM, Netwo
| Metric | Count |
|--------|-------|
-| Resources | 450+ |
-| Property types | 1,200+ |
-| Services | 120+ |
+| Resources | 453 |
+| Property types | 1257 |
+| Services | 124 |
| Intrinsic functions | 0 |
| Pseudo-parameters | 3 |
| Lint rules | 26 |
-**Lexicon version:** 0.1.5
+**Lexicon version:** 0.1.6
**Namespace:** `GCP`
- [Getting Started](./getting-started)
diff --git a/lexicons/gcp/docs/src/rehype-base-url.mjs b/lexicons/gcp/docs/src/rehype-base-url.mjs
new file mode 100644
index 00000000..b92d422a
--- /dev/null
+++ b/lexicons/gcp/docs/src/rehype-base-url.mjs
@@ -0,0 +1,68 @@
+/**
+ * Rehype plugin: prepend a configured `base` to root-relative `` attributes.
+ *
+ * Astro/Starlight only base-prefixes its own internal navigation (sidebar `link:`
+ * entries, `slug:` entries). Root-relative links written in MD/MDX content body
+ * — e.g. `[AWS](/lexicons/aws/)` — are emitted verbatim and 404 in production
+ * when the site is served from a non-root `base`.
+ *
+ * This plugin walks the HAST tree, finds `` elements whose href starts with
+ * `/` (single leading slash, not `//`), and prepends the site's `base`. It
+ * idempotently skips hrefs that already start with the site's own base or the
+ * project-wide base.
+ *
+ * @typedef {Object} RehypeBaseUrlOptions
+ * @property {string} base - Site base, e.g. "/chant" or "/chant/lexicons/aws". Trailing/leading slashes optional.
+ * @property {string} [projectBase] - Project-wide base used to detect already-correctly-prefixed cross-site links.
+ */
+
+const PROTOCOL_RE = /^[a-z][a-z0-9+.-]*:/i;
+
+function normalizeBase(value) {
+ return "/" + value.replace(/^\/+|\/+$/g, "");
+}
+
+/**
+ * @param {RehypeBaseUrlOptions} opts
+ */
+export default function rehypeBaseUrl(opts) {
+ const base = normalizeBase(opts.base);
+ if (base === "/") {
+ return () => {};
+ }
+ const ownPrefix = base + "/";
+ const projectPrefix = opts.projectBase
+ ? normalizeBase(opts.projectBase) + "/"
+ : null;
+
+ function rewrite(node) {
+ if (
+ node &&
+ node.type === "element" &&
+ node.tagName === "a" &&
+ node.properties &&
+ typeof node.properties.href === "string"
+ ) {
+ const href = node.properties.href;
+ if (
+ href.length > 0 &&
+ !href.startsWith("//") &&
+ !PROTOCOL_RE.test(href) &&
+ !href.startsWith("#") &&
+ href.startsWith("/") &&
+ href !== base &&
+ !href.startsWith(ownPrefix) &&
+ !(projectPrefix && href.startsWith(projectPrefix))
+ ) {
+ node.properties.href = base + href;
+ }
+ }
+ if (node && node.children) {
+ for (const child of node.children) rewrite(child);
+ }
+ }
+
+ return (tree) => {
+ rewrite(tree);
+ };
+}
diff --git a/lexicons/github/docs/astro.config.mjs b/lexicons/github/docs/astro.config.mjs
index 33c4018d..20d97271 100644
--- a/lexicons/github/docs/astro.config.mjs
+++ b/lexicons/github/docs/astro.config.mjs
@@ -1,9 +1,13 @@
// @ts-check
import { defineConfig } from 'astro/config';
import starlight from '@astrojs/starlight';
+import rehypeBaseUrl from './src/rehype-base-url.mjs';
export default defineConfig({
base: '/chant/lexicons/github/',
+ markdown: {
+ rehypePlugins: [[rehypeBaseUrl, { base: '/chant/lexicons/github/', projectBase: '/chant' }]],
+ },
integrations: [
starlight({
title: 'GitHub Actions',
diff --git a/lexicons/github/docs/src/rehype-base-url.mjs b/lexicons/github/docs/src/rehype-base-url.mjs
new file mode 100644
index 00000000..b92d422a
--- /dev/null
+++ b/lexicons/github/docs/src/rehype-base-url.mjs
@@ -0,0 +1,68 @@
+/**
+ * Rehype plugin: prepend a configured `base` to root-relative `` attributes.
+ *
+ * Astro/Starlight only base-prefixes its own internal navigation (sidebar `link:`
+ * entries, `slug:` entries). Root-relative links written in MD/MDX content body
+ * — e.g. `[AWS](/lexicons/aws/)` — are emitted verbatim and 404 in production
+ * when the site is served from a non-root `base`.
+ *
+ * This plugin walks the HAST tree, finds `` elements whose href starts with
+ * `/` (single leading slash, not `//`), and prepends the site's `base`. It
+ * idempotently skips hrefs that already start with the site's own base or the
+ * project-wide base.
+ *
+ * @typedef {Object} RehypeBaseUrlOptions
+ * @property {string} base - Site base, e.g. "/chant" or "/chant/lexicons/aws". Trailing/leading slashes optional.
+ * @property {string} [projectBase] - Project-wide base used to detect already-correctly-prefixed cross-site links.
+ */
+
+const PROTOCOL_RE = /^[a-z][a-z0-9+.-]*:/i;
+
+function normalizeBase(value) {
+ return "/" + value.replace(/^\/+|\/+$/g, "");
+}
+
+/**
+ * @param {RehypeBaseUrlOptions} opts
+ */
+export default function rehypeBaseUrl(opts) {
+ const base = normalizeBase(opts.base);
+ if (base === "/") {
+ return () => {};
+ }
+ const ownPrefix = base + "/";
+ const projectPrefix = opts.projectBase
+ ? normalizeBase(opts.projectBase) + "/"
+ : null;
+
+ function rewrite(node) {
+ if (
+ node &&
+ node.type === "element" &&
+ node.tagName === "a" &&
+ node.properties &&
+ typeof node.properties.href === "string"
+ ) {
+ const href = node.properties.href;
+ if (
+ href.length > 0 &&
+ !href.startsWith("//") &&
+ !PROTOCOL_RE.test(href) &&
+ !href.startsWith("#") &&
+ href.startsWith("/") &&
+ href !== base &&
+ !href.startsWith(ownPrefix) &&
+ !(projectPrefix && href.startsWith(projectPrefix))
+ ) {
+ node.properties.href = base + href;
+ }
+ }
+ if (node && node.children) {
+ for (const child of node.children) rewrite(child);
+ }
+ }
+
+ return (tree) => {
+ rewrite(tree);
+ };
+}
diff --git a/lexicons/gitlab/docs/astro.config.mjs b/lexicons/gitlab/docs/astro.config.mjs
index d1a01c98..f5daea5b 100644
--- a/lexicons/gitlab/docs/astro.config.mjs
+++ b/lexicons/gitlab/docs/astro.config.mjs
@@ -1,9 +1,13 @@
// @ts-check
import { defineConfig } from 'astro/config';
import starlight from '@astrojs/starlight';
+import rehypeBaseUrl from './src/rehype-base-url.mjs';
export default defineConfig({
base: '/chant/lexicons/gitlab/',
+ markdown: {
+ rehypePlugins: [[rehypeBaseUrl, { base: '/chant/lexicons/gitlab/', projectBase: '/chant' }]],
+ },
integrations: [
starlight({
title: 'GitLab CI/CD',
diff --git a/lexicons/gitlab/docs/src/rehype-base-url.mjs b/lexicons/gitlab/docs/src/rehype-base-url.mjs
new file mode 100644
index 00000000..b92d422a
--- /dev/null
+++ b/lexicons/gitlab/docs/src/rehype-base-url.mjs
@@ -0,0 +1,68 @@
+/**
+ * Rehype plugin: prepend a configured `base` to root-relative `` attributes.
+ *
+ * Astro/Starlight only base-prefixes its own internal navigation (sidebar `link:`
+ * entries, `slug:` entries). Root-relative links written in MD/MDX content body
+ * — e.g. `[AWS](/lexicons/aws/)` — are emitted verbatim and 404 in production
+ * when the site is served from a non-root `base`.
+ *
+ * This plugin walks the HAST tree, finds `` elements whose href starts with
+ * `/` (single leading slash, not `//`), and prepends the site's `base`. It
+ * idempotently skips hrefs that already start with the site's own base or the
+ * project-wide base.
+ *
+ * @typedef {Object} RehypeBaseUrlOptions
+ * @property {string} base - Site base, e.g. "/chant" or "/chant/lexicons/aws". Trailing/leading slashes optional.
+ * @property {string} [projectBase] - Project-wide base used to detect already-correctly-prefixed cross-site links.
+ */
+
+const PROTOCOL_RE = /^[a-z][a-z0-9+.-]*:/i;
+
+function normalizeBase(value) {
+ return "/" + value.replace(/^\/+|\/+$/g, "");
+}
+
+/**
+ * @param {RehypeBaseUrlOptions} opts
+ */
+export default function rehypeBaseUrl(opts) {
+ const base = normalizeBase(opts.base);
+ if (base === "/") {
+ return () => {};
+ }
+ const ownPrefix = base + "/";
+ const projectPrefix = opts.projectBase
+ ? normalizeBase(opts.projectBase) + "/"
+ : null;
+
+ function rewrite(node) {
+ if (
+ node &&
+ node.type === "element" &&
+ node.tagName === "a" &&
+ node.properties &&
+ typeof node.properties.href === "string"
+ ) {
+ const href = node.properties.href;
+ if (
+ href.length > 0 &&
+ !href.startsWith("//") &&
+ !PROTOCOL_RE.test(href) &&
+ !href.startsWith("#") &&
+ href.startsWith("/") &&
+ href !== base &&
+ !href.startsWith(ownPrefix) &&
+ !(projectPrefix && href.startsWith(projectPrefix))
+ ) {
+ node.properties.href = base + href;
+ }
+ }
+ if (node && node.children) {
+ for (const child of node.children) rewrite(child);
+ }
+ }
+
+ return (tree) => {
+ rewrite(tree);
+ };
+}
diff --git a/lexicons/helm/docs/astro.config.mjs b/lexicons/helm/docs/astro.config.mjs
index ac4e725a..26de30c4 100644
--- a/lexicons/helm/docs/astro.config.mjs
+++ b/lexicons/helm/docs/astro.config.mjs
@@ -1,9 +1,13 @@
// @ts-check
import { defineConfig } from 'astro/config';
import starlight from '@astrojs/starlight';
+import rehypeBaseUrl from './src/rehype-base-url.mjs';
export default defineConfig({
base: '/chant/lexicons/helm/',
+ markdown: {
+ rehypePlugins: [[rehypeBaseUrl, { base: '/chant/lexicons/helm/', projectBase: '/chant' }]],
+ },
integrations: [
starlight({
title: 'Helm',
diff --git a/lexicons/helm/docs/src/rehype-base-url.mjs b/lexicons/helm/docs/src/rehype-base-url.mjs
new file mode 100644
index 00000000..b92d422a
--- /dev/null
+++ b/lexicons/helm/docs/src/rehype-base-url.mjs
@@ -0,0 +1,68 @@
+/**
+ * Rehype plugin: prepend a configured `base` to root-relative `` attributes.
+ *
+ * Astro/Starlight only base-prefixes its own internal navigation (sidebar `link:`
+ * entries, `slug:` entries). Root-relative links written in MD/MDX content body
+ * — e.g. `[AWS](/lexicons/aws/)` — are emitted verbatim and 404 in production
+ * when the site is served from a non-root `base`.
+ *
+ * This plugin walks the HAST tree, finds `` elements whose href starts with
+ * `/` (single leading slash, not `//`), and prepends the site's `base`. It
+ * idempotently skips hrefs that already start with the site's own base or the
+ * project-wide base.
+ *
+ * @typedef {Object} RehypeBaseUrlOptions
+ * @property {string} base - Site base, e.g. "/chant" or "/chant/lexicons/aws". Trailing/leading slashes optional.
+ * @property {string} [projectBase] - Project-wide base used to detect already-correctly-prefixed cross-site links.
+ */
+
+const PROTOCOL_RE = /^[a-z][a-z0-9+.-]*:/i;
+
+function normalizeBase(value) {
+ return "/" + value.replace(/^\/+|\/+$/g, "");
+}
+
+/**
+ * @param {RehypeBaseUrlOptions} opts
+ */
+export default function rehypeBaseUrl(opts) {
+ const base = normalizeBase(opts.base);
+ if (base === "/") {
+ return () => {};
+ }
+ const ownPrefix = base + "/";
+ const projectPrefix = opts.projectBase
+ ? normalizeBase(opts.projectBase) + "/"
+ : null;
+
+ function rewrite(node) {
+ if (
+ node &&
+ node.type === "element" &&
+ node.tagName === "a" &&
+ node.properties &&
+ typeof node.properties.href === "string"
+ ) {
+ const href = node.properties.href;
+ if (
+ href.length > 0 &&
+ !href.startsWith("//") &&
+ !PROTOCOL_RE.test(href) &&
+ !href.startsWith("#") &&
+ href.startsWith("/") &&
+ href !== base &&
+ !href.startsWith(ownPrefix) &&
+ !(projectPrefix && href.startsWith(projectPrefix))
+ ) {
+ node.properties.href = base + href;
+ }
+ }
+ if (node && node.children) {
+ for (const child of node.children) rewrite(child);
+ }
+ }
+
+ return (tree) => {
+ rewrite(tree);
+ };
+}
diff --git a/lexicons/k8s/docs/astro.config.mjs b/lexicons/k8s/docs/astro.config.mjs
index 9ee6c58c..edcbf913 100644
--- a/lexicons/k8s/docs/astro.config.mjs
+++ b/lexicons/k8s/docs/astro.config.mjs
@@ -1,9 +1,13 @@
// @ts-check
import { defineConfig } from 'astro/config';
import starlight from '@astrojs/starlight';
+import rehypeBaseUrl from './src/rehype-base-url.mjs';
export default defineConfig({
base: '/chant/lexicons/k8s/',
+ markdown: {
+ rehypePlugins: [[rehypeBaseUrl, { base: '/chant/lexicons/k8s/', projectBase: '/chant' }]],
+ },
integrations: [
starlight({
title: 'Kubernetes',
diff --git a/lexicons/k8s/docs/src/content/docs/index.mdx b/lexicons/k8s/docs/src/content/docs/index.mdx
index 9ee4435e..ba68cc24 100644
--- a/lexicons/k8s/docs/src/content/docs/index.mdx
+++ b/lexicons/k8s/docs/src/content/docs/index.mdx
@@ -5,7 +5,7 @@ description: "Typed constructors for Kubernetes resource manifests"
The **Kubernetes** lexicon provides typed constructors for Kubernetes resource
manifests. It covers Deployments, Services, ConfigMaps, StatefulSets, Jobs,
-Ingress, RBAC, and 150+ resources and 70+ property types.
+Ingress, RBAC, and 147 resources and 50 property types.
New? Start with the [Getting Started](/chant/lexicons/k8s/getting-started/) guide.
@@ -51,21 +51,21 @@ export const service = new Service({
});
```
-The lexicon provides **150+ resource types** (Deployment, Service, ConfigMap, StatefulSet, and more), **70+ property types** (Container, Probe, Volume, SecurityContext, etc.), and composites (WebApp, StatefulApp, CronWorkload, AutoscaledService, WorkerPool, NamespaceEnv, NodeAgent) for common patterns.
+The lexicon provides **147 resource types** (Deployment, Service, ConfigMap, StatefulSet, and more), **50 property types** (Container, Probe, Volume, SecurityContext, etc.), and composites (WebApp, StatefulApp, CronWorkload, AutoscaledService, WorkerPool, NamespaceEnv, NodeAgent) for common patterns.
## At a Glance
| Metric | Count |
|--------|-------|
-| Resources | 150+ |
-| Property types | 70+ |
-| Services | 22+ |
+| Resources | 150 |
+| Property types | 70 |
+| Services | 22 |
| Intrinsic functions | 0 |
| Pseudo-parameters | 0 |
| Lint rules | 29 |
-**Lexicon version:** 0.1.5
+**Lexicon version:** 0.1.6
**Namespace:** `K8s`
- [Getting Started](./getting-started)
diff --git a/lexicons/k8s/docs/src/rehype-base-url.mjs b/lexicons/k8s/docs/src/rehype-base-url.mjs
new file mode 100644
index 00000000..b92d422a
--- /dev/null
+++ b/lexicons/k8s/docs/src/rehype-base-url.mjs
@@ -0,0 +1,68 @@
+/**
+ * Rehype plugin: prepend a configured `base` to root-relative `` attributes.
+ *
+ * Astro/Starlight only base-prefixes its own internal navigation (sidebar `link:`
+ * entries, `slug:` entries). Root-relative links written in MD/MDX content body
+ * — e.g. `[AWS](/lexicons/aws/)` — are emitted verbatim and 404 in production
+ * when the site is served from a non-root `base`.
+ *
+ * This plugin walks the HAST tree, finds `` elements whose href starts with
+ * `/` (single leading slash, not `//`), and prepends the site's `base`. It
+ * idempotently skips hrefs that already start with the site's own base or the
+ * project-wide base.
+ *
+ * @typedef {Object} RehypeBaseUrlOptions
+ * @property {string} base - Site base, e.g. "/chant" or "/chant/lexicons/aws". Trailing/leading slashes optional.
+ * @property {string} [projectBase] - Project-wide base used to detect already-correctly-prefixed cross-site links.
+ */
+
+const PROTOCOL_RE = /^[a-z][a-z0-9+.-]*:/i;
+
+function normalizeBase(value) {
+ return "/" + value.replace(/^\/+|\/+$/g, "");
+}
+
+/**
+ * @param {RehypeBaseUrlOptions} opts
+ */
+export default function rehypeBaseUrl(opts) {
+ const base = normalizeBase(opts.base);
+ if (base === "/") {
+ return () => {};
+ }
+ const ownPrefix = base + "/";
+ const projectPrefix = opts.projectBase
+ ? normalizeBase(opts.projectBase) + "/"
+ : null;
+
+ function rewrite(node) {
+ if (
+ node &&
+ node.type === "element" &&
+ node.tagName === "a" &&
+ node.properties &&
+ typeof node.properties.href === "string"
+ ) {
+ const href = node.properties.href;
+ if (
+ href.length > 0 &&
+ !href.startsWith("//") &&
+ !PROTOCOL_RE.test(href) &&
+ !href.startsWith("#") &&
+ href.startsWith("/") &&
+ href !== base &&
+ !href.startsWith(ownPrefix) &&
+ !(projectPrefix && href.startsWith(projectPrefix))
+ ) {
+ node.properties.href = base + href;
+ }
+ }
+ if (node && node.children) {
+ for (const child of node.children) rewrite(child);
+ }
+ }
+
+ return (tree) => {
+ rewrite(tree);
+ };
+}
diff --git a/lexicons/slurm/docs/astro.config.mjs b/lexicons/slurm/docs/astro.config.mjs
index 1b1f62fc..05a3504c 100644
--- a/lexicons/slurm/docs/astro.config.mjs
+++ b/lexicons/slurm/docs/astro.config.mjs
@@ -1,8 +1,13 @@
// @ts-check
import { defineConfig } from 'astro/config';
import starlight from '@astrojs/starlight';
+import rehypeBaseUrl from '../../../packages/core/src/codegen/rehype-base-url.mjs';
export default defineConfig({
+ base: '/chant/lexicons/slurm/',
+ markdown: {
+ rehypePlugins: [[rehypeBaseUrl, { base: '/chant/lexicons/slurm/', projectBase: '/chant' }]],
+ },
integrations: [
starlight({
title: 'Slurm',
diff --git a/lexicons/temporal/docs/astro.config.mjs b/lexicons/temporal/docs/astro.config.mjs
index 690ff316..efc9b7ec 100644
--- a/lexicons/temporal/docs/astro.config.mjs
+++ b/lexicons/temporal/docs/astro.config.mjs
@@ -1,9 +1,13 @@
// @ts-check
import { defineConfig } from 'astro/config';
import starlight from '@astrojs/starlight';
+import rehypeBaseUrl from './src/rehype-base-url.mjs';
export default defineConfig({
base: '/chant/lexicons/temporal/',
+ markdown: {
+ rehypePlugins: [[rehypeBaseUrl, { base: '/chant/lexicons/temporal/', projectBase: '/chant' }]],
+ },
integrations: [
starlight({
title: 'Temporal',
diff --git a/lexicons/temporal/docs/src/content/docs/serialization.mdx b/lexicons/temporal/docs/src/content/docs/serialization.mdx
index f1ae1d9d..627bbd2d 100644
--- a/lexicons/temporal/docs/src/content/docs/serialization.mdx
+++ b/lexicons/temporal/docs/src/content/docs/serialization.mdx
@@ -5,4 +5,4 @@ description: "Output format for the Temporal lexicon"
The Temporal lexicon serializes resources into its native output format during the build step.
-See the [Serialization](/serialization/output-formats) guide for general information about output formats in chant.
+See the [Serialization](/chant/serialization/output-formats) guide for general information about output formats in chant.
diff --git a/lexicons/temporal/docs/src/rehype-base-url.mjs b/lexicons/temporal/docs/src/rehype-base-url.mjs
new file mode 100644
index 00000000..b92d422a
--- /dev/null
+++ b/lexicons/temporal/docs/src/rehype-base-url.mjs
@@ -0,0 +1,68 @@
+/**
+ * Rehype plugin: prepend a configured `base` to root-relative `` attributes.
+ *
+ * Astro/Starlight only base-prefixes its own internal navigation (sidebar `link:`
+ * entries, `slug:` entries). Root-relative links written in MD/MDX content body
+ * — e.g. `[AWS](/lexicons/aws/)` — are emitted verbatim and 404 in production
+ * when the site is served from a non-root `base`.
+ *
+ * This plugin walks the HAST tree, finds `` elements whose href starts with
+ * `/` (single leading slash, not `//`), and prepends the site's `base`. It
+ * idempotently skips hrefs that already start with the site's own base or the
+ * project-wide base.
+ *
+ * @typedef {Object} RehypeBaseUrlOptions
+ * @property {string} base - Site base, e.g. "/chant" or "/chant/lexicons/aws". Trailing/leading slashes optional.
+ * @property {string} [projectBase] - Project-wide base used to detect already-correctly-prefixed cross-site links.
+ */
+
+const PROTOCOL_RE = /^[a-z][a-z0-9+.-]*:/i;
+
+function normalizeBase(value) {
+ return "/" + value.replace(/^\/+|\/+$/g, "");
+}
+
+/**
+ * @param {RehypeBaseUrlOptions} opts
+ */
+export default function rehypeBaseUrl(opts) {
+ const base = normalizeBase(opts.base);
+ if (base === "/") {
+ return () => {};
+ }
+ const ownPrefix = base + "/";
+ const projectPrefix = opts.projectBase
+ ? normalizeBase(opts.projectBase) + "/"
+ : null;
+
+ function rewrite(node) {
+ if (
+ node &&
+ node.type === "element" &&
+ node.tagName === "a" &&
+ node.properties &&
+ typeof node.properties.href === "string"
+ ) {
+ const href = node.properties.href;
+ if (
+ href.length > 0 &&
+ !href.startsWith("//") &&
+ !PROTOCOL_RE.test(href) &&
+ !href.startsWith("#") &&
+ href.startsWith("/") &&
+ href !== base &&
+ !href.startsWith(ownPrefix) &&
+ !(projectPrefix && href.startsWith(projectPrefix))
+ ) {
+ node.properties.href = base + href;
+ }
+ }
+ if (node && node.children) {
+ for (const child of node.children) rewrite(child);
+ }
+ }
+
+ return (tree) => {
+ rewrite(tree);
+ };
+}
diff --git a/packages/core/src/codegen/docs-sections.ts b/packages/core/src/codegen/docs-sections.ts
index 79ac7339..c2b46ed8 100644
--- a/packages/core/src/codegen/docs-sections.ts
+++ b/packages/core/src/codegen/docs-sections.ts
@@ -150,7 +150,7 @@ export function generateSerialization(config: DocsConfig): string {
lines.push(
`The ${config.displayName} lexicon serializes resources into its native output format during the build step.`,
"",
- "See the [Serialization](/serialization/output-formats) guide for general information about output formats in chant.",
+ "See the [Serialization](/chant/serialization/output-formats) guide for general information about output formats in chant.",
);
}
diff --git a/packages/core/src/codegen/docs.ts b/packages/core/src/codegen/docs.ts
index 4256e386..e14309ed 100644
--- a/packages/core/src/codegen/docs.ts
+++ b/packages/core/src/codegen/docs.ts
@@ -7,8 +7,9 @@
* (service grouping, resource type URLs, custom overview content).
*/
-import { readFileSync, writeFileSync, mkdirSync, rmSync } from "fs";
+import { copyFileSync, readFileSync, writeFileSync, mkdirSync, rmSync } from "fs";
import { join } from "path";
+import { fileURLToPath } from "url";
import { expandFileMarkers } from "./docs-file-markers";
import { scanRules, generateRules } from "./docs-rule-scanning";
@@ -222,14 +223,25 @@ export const collections = {
`,
);
+ // src/rehype-base-url.mjs — copied from chant core so Astro can import it
+ // without the generated docs site needing a workspace dep on @intentius/chant.
+ const pluginSrcPath = fileURLToPath(
+ new URL("./rehype-base-url.mjs", import.meta.url),
+ );
+ copyFileSync(pluginSrcPath, join(outDir, "src", "rehype-base-url.mjs"));
+
// astro.config.mjs
+ const rehypeLine = config.basePath
+ ? `\n markdown: {\n rehypePlugins: [[rehypeBaseUrl, { base: '${config.basePath}', projectBase: '/chant' }]],\n },`
+ : "";
writeFileSync(
join(outDir, "astro.config.mjs"),
`// @ts-check
import { defineConfig } from 'astro/config';
import starlight from '@astrojs/starlight';
+import rehypeBaseUrl from './src/rehype-base-url.mjs';
-export default defineConfig({${config.basePath ? `\n base: '${config.basePath}',` : ""}
+export default defineConfig({${config.basePath ? `\n base: '${config.basePath}',` : ""}${rehypeLine}
integrations: [
starlight({
title: '${config.displayName}',
diff --git a/packages/core/src/codegen/rehype-base-url.d.mts b/packages/core/src/codegen/rehype-base-url.d.mts
new file mode 100644
index 00000000..36246a52
--- /dev/null
+++ b/packages/core/src/codegen/rehype-base-url.d.mts
@@ -0,0 +1,17 @@
+export interface RehypeBaseUrlOptions {
+ /** Site base, e.g. "/chant" or "/chant/lexicons/aws". Trailing/leading slashes optional. */
+ base: string;
+ /** Project-wide base used to detect already-correctly-prefixed cross-site links. */
+ projectBase?: string;
+}
+
+type HastNode = {
+ type: string;
+ tagName?: string;
+ properties?: Record;
+ children?: HastNode[];
+};
+
+export default function rehypeBaseUrl(
+ opts: RehypeBaseUrlOptions,
+): (tree: HastNode) => void;
diff --git a/packages/core/src/codegen/rehype-base-url.mjs b/packages/core/src/codegen/rehype-base-url.mjs
new file mode 100644
index 00000000..b92d422a
--- /dev/null
+++ b/packages/core/src/codegen/rehype-base-url.mjs
@@ -0,0 +1,68 @@
+/**
+ * Rehype plugin: prepend a configured `base` to root-relative `` attributes.
+ *
+ * Astro/Starlight only base-prefixes its own internal navigation (sidebar `link:`
+ * entries, `slug:` entries). Root-relative links written in MD/MDX content body
+ * — e.g. `[AWS](/lexicons/aws/)` — are emitted verbatim and 404 in production
+ * when the site is served from a non-root `base`.
+ *
+ * This plugin walks the HAST tree, finds `` elements whose href starts with
+ * `/` (single leading slash, not `//`), and prepends the site's `base`. It
+ * idempotently skips hrefs that already start with the site's own base or the
+ * project-wide base.
+ *
+ * @typedef {Object} RehypeBaseUrlOptions
+ * @property {string} base - Site base, e.g. "/chant" or "/chant/lexicons/aws". Trailing/leading slashes optional.
+ * @property {string} [projectBase] - Project-wide base used to detect already-correctly-prefixed cross-site links.
+ */
+
+const PROTOCOL_RE = /^[a-z][a-z0-9+.-]*:/i;
+
+function normalizeBase(value) {
+ return "/" + value.replace(/^\/+|\/+$/g, "");
+}
+
+/**
+ * @param {RehypeBaseUrlOptions} opts
+ */
+export default function rehypeBaseUrl(opts) {
+ const base = normalizeBase(opts.base);
+ if (base === "/") {
+ return () => {};
+ }
+ const ownPrefix = base + "/";
+ const projectPrefix = opts.projectBase
+ ? normalizeBase(opts.projectBase) + "/"
+ : null;
+
+ function rewrite(node) {
+ if (
+ node &&
+ node.type === "element" &&
+ node.tagName === "a" &&
+ node.properties &&
+ typeof node.properties.href === "string"
+ ) {
+ const href = node.properties.href;
+ if (
+ href.length > 0 &&
+ !href.startsWith("//") &&
+ !PROTOCOL_RE.test(href) &&
+ !href.startsWith("#") &&
+ href.startsWith("/") &&
+ href !== base &&
+ !href.startsWith(ownPrefix) &&
+ !(projectPrefix && href.startsWith(projectPrefix))
+ ) {
+ node.properties.href = base + href;
+ }
+ }
+ if (node && node.children) {
+ for (const child of node.children) rewrite(child);
+ }
+ }
+
+ return (tree) => {
+ rewrite(tree);
+ };
+}
diff --git a/packages/core/src/codegen/rehype-base-url.test.ts b/packages/core/src/codegen/rehype-base-url.test.ts
new file mode 100644
index 00000000..f3861eff
--- /dev/null
+++ b/packages/core/src/codegen/rehype-base-url.test.ts
@@ -0,0 +1,161 @@
+import { describe, it, expect } from "vitest";
+import rehypeBaseUrl from "./rehype-base-url.mjs";
+
+type Element = {
+ type: "element";
+ tagName: string;
+ properties: Record;
+ children: Element[];
+};
+
+function a(href: string): Element {
+ return { type: "element", tagName: "a", properties: { href }, children: [] };
+}
+
+function tree(...links: Element[]): Element {
+ return { type: "element", tagName: "root", properties: {}, children: links };
+}
+
+function run(opts: { base: string; projectBase?: string }, href: string): string {
+ const plugin = rehypeBaseUrl(opts);
+ const root = tree(a(href));
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ (plugin as any)(root);
+ return root.children[0].properties.href as string;
+}
+
+describe("rehypeBaseUrl — main docs (base=/chant)", () => {
+ const opts = { base: "/chant", projectBase: "/chant" };
+
+ it("prepends base to plain root-relative link", () => {
+ expect(run(opts, "/lexicons/aws/")).toBe("/chant/lexicons/aws/");
+ });
+
+ it("prepends base to /api/* TypeDoc link", () => {
+ expect(run(opts, "/api/classes/attrref/")).toBe("/chant/api/classes/attrref/");
+ });
+
+ it("leaves already-prefixed /chant/ link unchanged", () => {
+ expect(run(opts, "/chant/concepts/philosophy/")).toBe(
+ "/chant/concepts/philosophy/",
+ );
+ });
+
+ it("leaves the bare base unchanged", () => {
+ expect(run(opts, "/chant")).toBe("/chant");
+ });
+
+ it("leaves https://… unchanged", () => {
+ expect(run(opts, "https://example.com/foo")).toBe("https://example.com/foo");
+ });
+
+ it("leaves protocol-relative // unchanged", () => {
+ expect(run(opts, "//cdn.example.com/x")).toBe("//cdn.example.com/x");
+ });
+
+ it("leaves mailto: unchanged", () => {
+ expect(run(opts, "mailto:a@b")).toBe("mailto:a@b");
+ });
+
+ it("leaves anchor #foo unchanged", () => {
+ expect(run(opts, "#section")).toBe("#section");
+ });
+
+ it("leaves relative path unchanged", () => {
+ expect(run(opts, "foo/bar")).toBe("foo/bar");
+ });
+
+ it("leaves dot-relative path unchanged", () => {
+ expect(run(opts, "../sibling/")).toBe("../sibling/");
+ });
+
+ it("leaves empty href unchanged", () => {
+ expect(run(opts, "")).toBe("");
+ });
+});
+
+describe("rehypeBaseUrl — lexicon (base=/chant/lexicons/aws, projectBase=/chant)", () => {
+ const opts = { base: "/chant/lexicons/aws", projectBase: "/chant" };
+
+ it("leaves cross-site /chant/… unchanged (projectBase guard)", () => {
+ expect(run(opts, "/chant/concepts/philosophy/")).toBe(
+ "/chant/concepts/philosophy/",
+ );
+ });
+
+ it("leaves cross-lexicon /chant/lexicons/k8s/… unchanged", () => {
+ expect(run(opts, "/chant/lexicons/k8s/")).toBe("/chant/lexicons/k8s/");
+ });
+
+ it("leaves the lexicon's own base prefix unchanged", () => {
+ expect(run(opts, "/chant/lexicons/aws/composites/")).toBe(
+ "/chant/lexicons/aws/composites/",
+ );
+ });
+
+ it("prepends lexicon base to bare /foo/ (interpreted as site-local)", () => {
+ expect(run(opts, "/foo/bar/")).toBe("/chant/lexicons/aws/foo/bar/");
+ });
+});
+
+describe("rehypeBaseUrl — base normalization", () => {
+ it("is a no-op when base is '/'", () => {
+ const plugin = rehypeBaseUrl({ base: "/" });
+ const root = tree(a("/foo"));
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ (plugin as any)(root);
+ expect(root.children[0].properties.href).toBe("/foo");
+ });
+
+ it("handles trailing slashes in base option", () => {
+ expect(run({ base: "/chant/", projectBase: "/chant/" }, "/foo")).toBe(
+ "/chant/foo",
+ );
+ });
+
+ it("handles missing leading slash in base option", () => {
+ expect(run({ base: "chant", projectBase: "chant" }, "/foo")).toBe("/chant/foo");
+ });
+});
+
+describe("rehypeBaseUrl — tree traversal", () => {
+ it("rewrites nested hrefs", () => {
+ const plugin = rehypeBaseUrl({ base: "/chant", projectBase: "/chant" });
+ const root = tree();
+ root.children = [
+ {
+ type: "element",
+ tagName: "div",
+ properties: {},
+ children: [a("/foo"), a("/bar")],
+ },
+ a("/baz"),
+ ];
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ (plugin as any)(root);
+ const div = root.children[0] as Element;
+ expect(div.children[0].properties.href).toBe("/chant/foo");
+ expect(div.children[1].properties.href).toBe("/chant/bar");
+ expect(root.children[1].properties.href).toBe("/chant/baz");
+ });
+
+ it("leaves non- elements alone", () => {
+ const plugin = rehypeBaseUrl({ base: "/chant" });
+ const root: Element = {
+ type: "element",
+ tagName: "root",
+ properties: {},
+ children: [
+ {
+ type: "element",
+ tagName: "img",
+ properties: { src: "/foo.png" },
+ children: [],
+ },
+ ],
+ };
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ (plugin as any)(root);
+ expect(root.children[0].properties.src).toBe("/foo.png");
+ });
+});