From 2c197f7fe5543ede2f5708e4bb749a31d142acfb Mon Sep 17 00:00:00 2001 From: Martijn Laarman Date: Mon, 29 Sep 2025 19:17:21 +0200 Subject: [PATCH 001/171] Add CLI command documentation drafts - Introduced draft documentation for all CLI commands under `docs/cli`. - Updated `_docset.yml` to include new CLI documentation. - Adjusted navigation order and reinstated missing migration files. --- docs/_docset.yml | 68 +++++++---- docs/cli/assemble.md | 35 ++++++ docs/cli/assembler-bloom-filter-create.md | 14 +++ docs/cli/assembler-bloom-filter-lookup.md | 14 +++ docs/cli/assembler-build.md | 26 +++++ docs/cli/assembler-clone.md | 23 ++++ docs/cli/assembler-config-init.md | 17 +++ docs/cli/assembler-content-source-match.md | 15 +++ docs/cli/assembler-content-source-validate.md | 7 ++ docs/cli/assembler-deploy-apply.md | 20 ++++ docs/cli/assembler-deploy-plan.md | 23 ++++ docs/cli/assembler-deploy-update-redirects.md | 17 +++ docs/cli/assembler-index.md | 71 +++++++++++ ...bler-navigation-validate-link-reference.md | 14 +++ docs/cli/assembler-navigation-validate.md | 9 ++ docs/cli/assembler-serve.md | 17 +++ docs/cli/diff-validate.md | 14 +++ docs/cli/generate.md | 110 ++++++++++++++++++ docs/cli/inbound-links-validate-all.md | 9 ++ .../inbound-links-validate-link-reference.md | 17 +++ docs/cli/inbound-links-validate.md | 17 +++ docs/cli/index-command.md | 71 +++++++++++ docs/cli/index.md | 30 +++++ docs/cli/mv.md | 25 ++++ docs/cli/serve.md | 18 +++ docs/configure/site/legacy-url-mappings.md | 3 + 26 files changed, 683 insertions(+), 21 deletions(-) create mode 100644 docs/cli/assemble.md create mode 100644 docs/cli/assembler-bloom-filter-create.md create mode 100644 docs/cli/assembler-bloom-filter-lookup.md create mode 100644 docs/cli/assembler-build.md create mode 100644 docs/cli/assembler-clone.md create mode 100644 docs/cli/assembler-config-init.md create mode 100644 docs/cli/assembler-content-source-match.md create mode 100644 docs/cli/assembler-content-source-validate.md create mode 100644 docs/cli/assembler-deploy-apply.md create mode 100644 docs/cli/assembler-deploy-plan.md create mode 100644 docs/cli/assembler-deploy-update-redirects.md create mode 100644 docs/cli/assembler-index.md create mode 100644 docs/cli/assembler-navigation-validate-link-reference.md create mode 100644 docs/cli/assembler-navigation-validate.md create mode 100644 docs/cli/assembler-serve.md create mode 100644 docs/cli/diff-validate.md create mode 100644 docs/cli/generate.md create mode 100644 docs/cli/inbound-links-validate-all.md create mode 100644 docs/cli/inbound-links-validate-link-reference.md create mode 100644 docs/cli/inbound-links-validate.md create mode 100644 docs/cli/index-command.md create mode 100644 docs/cli/index.md create mode 100644 docs/cli/mv.md create mode 100644 docs/cli/serve.md diff --git a/docs/_docset.yml b/docs/_docset.yml index dd11ae52c..dbbab6312 100644 --- a/docs/_docset.yml +++ b/docs/_docset.yml @@ -46,26 +46,6 @@ toc: - file: branching-strategy.md - file: add-repo.md - file: release-new-version.md - - folder: migration - children: - - file: index.md - - folder: freeze - children: - - file: index.md - - file: gh-action.md - - file: syntax.md - - file: ia.md - - file: versioning.md - - file: engineering.md - - folder: guide - children: - - file: index.md - - file: working-in-docs-content.md - - file: automated.md - - file: tooling.md - - file: move-ref-docs.md - - file: mapping.md - - file: how-to-set-up-docs-previews.md - folder: configure children: - file: index.md @@ -74,9 +54,9 @@ toc: - file: index.md - file: content.md - file: navigation.md + - file: products.md - file: versions.md - file: legacy-url-mappings.md - - file: products.md - folder: content-set children: - file: index.md @@ -121,6 +101,52 @@ toc: - file: tabs.md - file: tagged_regions.md - file: titles.md + - folder: cli + children: + - file: index.md + - file: assemble.md + - file: assembler-bloom-filter-create.md + - file: assembler-bloom-filter-lookup.md + - file: assembler-build.md + - file: assembler-clone.md + - file: assembler-config-init.md + - file: assembler-content-source-match.md + - file: assembler-content-source-validate.md + - file: assembler-deploy-apply.md + - file: assembler-deploy-plan.md + - file: assembler-deploy-update-redirects.md + - file: assembler-index.md + - file: assembler-navigation-validate.md + - file: assembler-navigation-validate-link-reference.md + - file: assembler-serve.md + - file: diff-validate.md + - file: generate.md + - file: inbound-links-validate.md + - file: inbound-links-validate-all.md + - file: inbound-links-validate-link-reference.md + - file: index-command.md + - file: mv.md + - file: serve.md + - folder: migration + children: + - file: index.md + - folder: freeze + children: + - file: index.md + - file: gh-action.md + - file: syntax.md + - file: ia.md + - file: versioning.md + - file: engineering.md + - folder: guide + children: + - file: index.md + - file: working-in-docs-content.md + - file: automated.md + - file: tooling.md + - file: move-ref-docs.md + - file: mapping.md + - file: how-to-set-up-docs-previews.md # nested TOCs are only allowed from docset.yml by default # to prevent them from being nested deeply arbitrarily # use max_toc_depth to allow deeper nesting (Expert mode, consult with docs team) diff --git a/docs/cli/assemble.md b/docs/cli/assemble.md new file mode 100644 index 000000000..2123af23b --- /dev/null +++ b/docs/cli/assemble.md @@ -0,0 +1,35 @@ +# assemble + +Do a full assembler clone and assembler build in one swoop + +## Usage + +``` +assemble [options...] [-h|--help] [--version] +``` + +## Options + +`--strict` `` +: Treat warnings as errors and fail the build on warnings (Default: null) + +`--environment` `` +: The environment to build (Default: null) + +`--fetch-latest` `` +: If true, fetch the latest commit of the branch instead of the link registry entry ref (Default: null) + +`--assume-cloned` `` +: If true, assume the repository folder already exists on disk assume it's cloned already, primarily used for testing (Default: null) + +`--metadata-only` `` +: Only emit documentation metadata to output, ignored if 'exporters' is also set (Default: null) + +`--show-hints` `` +: Show hints from all documentation sets during assembler build (Default: null) + +`--exporters` `?>` +: Set available exporters: html, es, config, links, state, llm, redirect, metadata, none. Defaults to (html, config, links, state, redirect) or 'default'. (Default: null) + +`--serve` +: Serve the documentation on port 4000 after successful build (Optional) \ No newline at end of file diff --git a/docs/cli/assembler-bloom-filter-create.md b/docs/cli/assembler-bloom-filter-create.md new file mode 100644 index 000000000..ff9a0e483 --- /dev/null +++ b/docs/cli/assembler-bloom-filter-create.md @@ -0,0 +1,14 @@ +# assembler bloom-filter create + +Generate the bloom filter binary file + +## Usage + +``` +assembler bloom-filter create [options...] [-h|--help] [--version] +``` + +## Options + +`--built-docs-dir` `` +: The local dir of local elastic/built-docs repository (Required) \ No newline at end of file diff --git a/docs/cli/assembler-bloom-filter-lookup.md b/docs/cli/assembler-bloom-filter-lookup.md new file mode 100644 index 000000000..efc3f76d8 --- /dev/null +++ b/docs/cli/assembler-bloom-filter-lookup.md @@ -0,0 +1,14 @@ +# assembler bloom-filter lookup + +Lookup whether a path exists in the bloomfilter + +## Usage + +``` +assembler bloom-filter lookup [options...] [-h|--help] [--version] +``` + +## Options + +`--path` `` +: The local dir of local elastic/built-docs repository (Required) \ No newline at end of file diff --git a/docs/cli/assembler-build.md b/docs/cli/assembler-build.md new file mode 100644 index 000000000..e84ad4739 --- /dev/null +++ b/docs/cli/assembler-build.md @@ -0,0 +1,26 @@ +# assembler build + +Builds all repositories + +## Usage + +``` +assembler build [options...] [-h|--help] [--version] +``` + +## Options + +`--strict` `` +: Treat warnings as errors and fail the build on warnings (Default: null) + +`--environment` `` +: The environment to build (Default: null) + +`--metadata-only` `` +: Only emit documentation metadata to output, ignored if 'exporters' is also set (Default: null) + +`--show-hints` `` +: Show hints from all documentation sets during assembler build (Default: null) + +`--exporters` `?>` +: Set available exporters: html, es, config, links, state, llm, redirect, metadata, none. Defaults to (html, config, links, state, redirect) or 'default'. (Default: null) \ No newline at end of file diff --git a/docs/cli/assembler-clone.md b/docs/cli/assembler-clone.md new file mode 100644 index 000000000..9758993a1 --- /dev/null +++ b/docs/cli/assembler-clone.md @@ -0,0 +1,23 @@ +# assembler clone + +Clones all repositories + +## Usage + +``` +assembler clone [options...] [-h|--help] [--version] +``` + +## Options + +`--strict` `` +: Treat warnings as errors and fail the build on warnings (Default: null) + +`--environment` `` +: The environment to build (Default: null) + +`--fetch-latest` `` +: If true, fetch the latest commit of the branch instead of the link registry entry ref (Default: null) + +`--assume-cloned` `` +: If true, assume the repository folder already exists on disk assume it's cloned already, primarily used for testing (Default: null) \ No newline at end of file diff --git a/docs/cli/assembler-config-init.md b/docs/cli/assembler-config-init.md new file mode 100644 index 000000000..9eacb7876 --- /dev/null +++ b/docs/cli/assembler-config-init.md @@ -0,0 +1,17 @@ +# assembler config init + +Clone the configuration folder + +## Usage + +``` +assembler config init [options...] [-h|--help] [--version] +``` + +## Options + +`--git-ref` `` +: The git reference of the config, defaults to 'main' (Default: null) + +`--local` +: Save the remote configuration locally in the pwd so later commands can pick it up as local (Optional) \ No newline at end of file diff --git a/docs/cli/assembler-content-source-match.md b/docs/cli/assembler-content-source-match.md new file mode 100644 index 000000000..7927990c6 --- /dev/null +++ b/docs/cli/assembler-content-source-match.md @@ -0,0 +1,15 @@ +# assembler content-source match + +## Usage + +``` +assembler content-source match [arguments...] [-h|--help] [--version] +``` + +## Arguments + +`[0] ` +: + +`[1] ` +: \ No newline at end of file diff --git a/docs/cli/assembler-content-source-validate.md b/docs/cli/assembler-content-source-validate.md new file mode 100644 index 000000000..cf5e8261b --- /dev/null +++ b/docs/cli/assembler-content-source-validate.md @@ -0,0 +1,7 @@ +# assembler content-source validate + +## Usage + +``` +assembler content-source validate [-h|--help] [--version] +``` \ No newline at end of file diff --git a/docs/cli/assembler-deploy-apply.md b/docs/cli/assembler-deploy-apply.md new file mode 100644 index 000000000..56e97acf5 --- /dev/null +++ b/docs/cli/assembler-deploy-apply.md @@ -0,0 +1,20 @@ +# assembler deploy apply + +Applies a sync plan + +## Usage + +``` +assembler deploy apply [options...] [-h|--help] [--version] +``` + +## Options + +`--environment` `` +: The environment to build (Required) + +`--s3-bucket-name` `` +: The S3 bucket name to deploy to (Required) + +`--plan-file` `` +: The file path to the plan file to apply (Required) \ No newline at end of file diff --git a/docs/cli/assembler-deploy-plan.md b/docs/cli/assembler-deploy-plan.md new file mode 100644 index 000000000..9476c56fd --- /dev/null +++ b/docs/cli/assembler-deploy-plan.md @@ -0,0 +1,23 @@ +# assembler deploy plan + +Creates a sync plan + +## Usage + +``` +assembler deploy plan [options...] [-h|--help] [--version] +``` + +## Options + +`--environment` `` +: The environment to build (Required) + +`--s3-bucket-name` `` +: The S3 bucket name to deploy to (Required) + +`--out` `` +: The file to write the plan to (Default: "") + +`--delete-threshold` `` +: The percentage of deletions allowed in the plan as float (Default: null) \ No newline at end of file diff --git a/docs/cli/assembler-deploy-update-redirects.md b/docs/cli/assembler-deploy-update-redirects.md new file mode 100644 index 000000000..50907008b --- /dev/null +++ b/docs/cli/assembler-deploy-update-redirects.md @@ -0,0 +1,17 @@ +# assembler deploy update-redirects + +Refreshes the redirects mapping in Cloudfront's KeyValueStore + +## Usage + +``` +assembler deploy update-redirects [options...] [-h|--help] [--version] +``` + +## Options + +`--environment` `` +: The environment to build (Required) + +`--redirects-file` `` +: Path to the redirects mapping pre-generated by docs-assembler (Default: null) \ No newline at end of file diff --git a/docs/cli/assembler-index.md b/docs/cli/assembler-index.md new file mode 100644 index 000000000..8b44bff6f --- /dev/null +++ b/docs/cli/assembler-index.md @@ -0,0 +1,71 @@ +# assembler index + +Index documentation to Elasticsearch, calls `docs-builder assembler build --exporters elasticsearch`. Exposes more options + +## Usage + +``` +assembler index [options...] [-h|--help] [--version] +``` + +## Options + +`-es|--endpoint ` +: Elasticsearch endpoint, alternatively set env DOCUMENTATION_ELASTIC_URL (Default: null) + +`--environment` `` +: The --environment used to clone ends up being part of the index name (Default: null) + +`--api-key` `` +: Elasticsearch API key, alternatively set env DOCUMENTATION_ELASTIC_APIKEY (Default: null) + +`--username` `` +: Elasticsearch username (basic auth), alternatively set env DOCUMENTATION_ELASTIC_USERNAME (Default: null) + +`--password` `` +: Elasticsearch password (basic auth), alternatively set env DOCUMENTATION_ELASTIC_PASSWORD (Default: null) + +`--no-semantic` `` +: Index without semantic fields (Default: null) + +`--search-num-threads` `` +: The number of search threads the inference endpoint should use. Defaults: 8 (Default: null) + +`--index-num-threads` `` +: The number of index threads the inference endpoint should use. Defaults: 8 (Default: null) + +`--bootstrap-timeout` `` +: Timeout in minutes for the inference endpoint creation. Defaults: 4 (Default: null) + +`--index-name-prefix` `` +: The prefix for the computed index/alias names. Defaults: semantic-docs (Default: null) + +`--buffer-size` `` +: The number of documents to send to ES as part of the bulk. Defaults: 100 (Default: null) + +`--max-retries` `` +: The number of times failed bulk items should be retried. Defaults: 3 (Default: null) + +`--debug-mode` `` +: Buffer ES request/responses for better error messages and pass ?pretty to all requests (Default: null) + +`--proxy-address` `` +: Route requests through a proxy server (Default: null) + +`--proxy-password` `` +: Proxy server password (Default: null) + +`--proxy-username` `` +: Proxy server username (Default: null) + +`--disable-ssl-verification` `` +: Disable SSL certificate validation (EXPERT OPTION) (Default: null) + +`--certificate-fingerprint` `` +: Pass a self-signed certificate fingerprint to validate the SSL connection (Default: null) + +`--certificate-path` `` +: Pass a self-signed certificate to validate the SSL connection (Default: null) + +`--certificate-not-root` `` +: If the certificate is not root but only part of the validation chain pass this (Default: null) \ No newline at end of file diff --git a/docs/cli/assembler-navigation-validate-link-reference.md b/docs/cli/assembler-navigation-validate-link-reference.md new file mode 100644 index 000000000..ad69081d1 --- /dev/null +++ b/docs/cli/assembler-navigation-validate-link-reference.md @@ -0,0 +1,14 @@ +# assembler navigation validate-link-reference + +Validate all published links in links.json do not collide with navigation path_prefixes and all urls are unique. + +## Usage + +``` +assembler navigation validate-link-reference [arguments...] [-h|--help] [--version] +``` + +## Arguments + +`[0] ` +: Path to `links.json` defaults to '.artifacts/docs/html/links.json' \ No newline at end of file diff --git a/docs/cli/assembler-navigation-validate.md b/docs/cli/assembler-navigation-validate.md new file mode 100644 index 000000000..d063ab438 --- /dev/null +++ b/docs/cli/assembler-navigation-validate.md @@ -0,0 +1,9 @@ +# assembler navigation validate + +Validates navigation.yml does not contain colliding path prefixes and all urls are unique + +## Usage + +``` +assembler navigation validate [-h|--help] [--version] +``` \ No newline at end of file diff --git a/docs/cli/assembler-serve.md b/docs/cli/assembler-serve.md new file mode 100644 index 000000000..c2184b7f6 --- /dev/null +++ b/docs/cli/assembler-serve.md @@ -0,0 +1,17 @@ +# assembler serve + +Serve the output of an assembler build + +## Usage + +``` +assembler serve [options...] [-h|--help] [--version] +``` + +## Options + +`--port` `` +: Port to serve the documentation. (Default: 4000) + +`--path` `` +: (Default: null) \ No newline at end of file diff --git a/docs/cli/diff-validate.md b/docs/cli/diff-validate.md new file mode 100644 index 000000000..17c1b8a80 --- /dev/null +++ b/docs/cli/diff-validate.md @@ -0,0 +1,14 @@ +# diff validate + +Validates redirect updates in the current branch using the redirect file against changes reported by git. + +## Usage + +``` +diff validate [options...] [-h|--help] [--version] +``` + +## Options + +`-p|--path ` +: Defaults to the`{pwd}/docs` folder (Default: null) \ No newline at end of file diff --git a/docs/cli/generate.md b/docs/cli/generate.md new file mode 100644 index 000000000..ce0aba91a --- /dev/null +++ b/docs/cli/generate.md @@ -0,0 +1,110 @@ +# generate (root command) + +Converts a source Markdown folder or file to an output folder + +## Usage + +``` +[command] [options...] [-h|--help] [--version] +``` + +## Global Options + +- `--log-level` `level` + +## Options + +`-p|--path ` +: Defaults to the`{pwd}/docs` folder (Default: null) + +`-o|--output ` +: Defaults to `.artifacts/html` (Default: null) + +`--path-prefix` `` +: Specifies the path prefix for urls (Default: null) + +`--force` `` +: Force a full rebuild of the destination folder (Default: null) + +`--strict` `` +: Treat warnings as errors and fail the build on warnings (Default: null) + +`--allow-indexing` `` +: Allow indexing and following of HTML files (Default: null) + +`--metadata-only` `` +: Only emit documentation metadata to output, ignored if 'exporters' is also set (Default: null) + +`--exporters` `?>` +: Set available exporters: html, es, config, links, state, llm, redirect, metadata, none. Defaults to (html, config, links, state, redirect) or 'default'. (Default: null) + +`--canonical-base-url` `` +: The base URL for the canonical url tag (Default: null) + +## Commands + +`assemble` +: Do a full assembler clone and assembler build in one swoop + +`assembler bloom-filter create` +: Generate the bloom filter binary file + +`assembler bloom-filter lookup` +: Lookup whether path exists in the bloomfilter + +`assembler build` +: Builds all repositories + +`assembler clone` +: Clones all repositories + +`assembler config init` +: Clone the configuration folder + +`assembler content-source match` +: + +`assembler content-source validate` +: + +`assembler deploy apply` +: Applies a sync plan + +`assembler deploy plan` +: Creates a sync plan + +`assembler deploy update-redirects` +: Refreshes the redirects mapping in Cloudfront's KeyValueStore + +`assembler index` +: Index documentation to Elasticsearch, calls `docs-builder assembler build --exporters elasticsearch`. Exposes more options + +`assembler navigation validate` +: Validates navigation.yml does not contain colliding path prefixes and all urls are unique + +`assembler navigation validate-link-reference` +: Validate all published links in links.json do not collide with navigation path_prefixes and all urls are unique. + +`assembler serve` +: Serve the output of an assembler build + +`diff validate` +: Validates redirect updates in the current branch using the redirect file against changes reported by git. + +`inbound-links validate` +: Validate all published cross_links in all published links.json files. + +`inbound-links validate-all` +: Validate all published cross_links in all published links.json files. + +`inbound-links validate-link-reference` +: Validate a locally published links.json file against all published links.json files in the registry + +`index` +: Index a single documentation set to Elasticsearch, calls `docs-builder --exporters elasticsearch`. Exposes more options + +`mv` +: Move a file from one location to another and update all links in the documentation + +`serve` +: Continuously serve a documentation folder at http://localhost:3000. File systems changes will be reflected without having to restart the server. \ No newline at end of file diff --git a/docs/cli/inbound-links-validate-all.md b/docs/cli/inbound-links-validate-all.md new file mode 100644 index 000000000..5f4cc0a91 --- /dev/null +++ b/docs/cli/inbound-links-validate-all.md @@ -0,0 +1,9 @@ +# inbound-links validate-all + +Validate all published cross_links in all published links.json files. + +## Usage + +``` +inbound-links validate-all [-h|--help] [--version] +``` \ No newline at end of file diff --git a/docs/cli/inbound-links-validate-link-reference.md b/docs/cli/inbound-links-validate-link-reference.md new file mode 100644 index 000000000..502f4ec0a --- /dev/null +++ b/docs/cli/inbound-links-validate-link-reference.md @@ -0,0 +1,17 @@ +# inbound-links validate-link-reference + +Validate a locally published links.json file against all published links.json files in the registry + +## Usage + +``` +inbound-links validate-link-reference [options...] [-h|--help] [--version] +``` + +## Options + +`--file` `` +: Path to `links.json` defaults to '.artifacts/docs/html/links.json' (Default: null) + +`-p|--path ` +: Defaults to the `{pwd}` folder (Default: null) \ No newline at end of file diff --git a/docs/cli/inbound-links-validate.md b/docs/cli/inbound-links-validate.md new file mode 100644 index 000000000..16e36710c --- /dev/null +++ b/docs/cli/inbound-links-validate.md @@ -0,0 +1,17 @@ +# inbound-links validate + +Validate all published cross_links in all published links.json files. + +## Usage + +``` +inbound-links validate [options...] [-h|--help] [--version] +``` + +## Options + +`--from` `` +: (Default: null) + +`--to` `` +: (Default: null) \ No newline at end of file diff --git a/docs/cli/index-command.md b/docs/cli/index-command.md new file mode 100644 index 000000000..8ebd521c8 --- /dev/null +++ b/docs/cli/index-command.md @@ -0,0 +1,71 @@ +# index + +Index a single documentation set to Elasticsearch, calls `docs-builder --exporters elasticsearch`. Exposes more options + +## Usage + +``` +index [options...] [-h|--help] [--version] +``` + +## Options + +`-es|--endpoint ` +: Elasticsearch endpoint, alternatively set env DOCUMENTATION_ELASTIC_URL (Default: null) + +`--path` `` +: path to the documentation folder, defaults to pwd. (Default: null) + +`--api-key` `` +: Elasticsearch API key, alternatively set env DOCUMENTATION_ELASTIC_APIKEY (Default: null) + +`--username` `` +: Elasticsearch username (basic auth), alternatively set env DOCUMENTATION_ELASTIC_USERNAME (Default: null) + +`--password` `` +: Elasticsearch password (basic auth), alternatively set env DOCUMENTATION_ELASTIC_PASSWORD (Default: null) + +`--no-semantic` `` +: Index without semantic fields (Default: null) + +`--search-num-threads` `` +: The number of search threads the inference endpoint should use. Defaults: 8 (Default: null) + +`--index-num-threads` `` +: The number of index threads the inference endpoint should use. Defaults: 8 (Default: null) + +`--bootstrap-timeout` `` +: Timeout in minutes for the inference endpoint creation. Defaults: 4 (Default: null) + +`--index-name-prefix` `` +: The prefix for the computed index/alias names. Defaults: semantic-docs (Default: null) + +`--buffer-size` `` +: The number of documents to send to ES as part of the bulk. Defaults: 100 (Default: null) + +`--max-retries` `` +: The number of times failed bulk items should be retried. Defaults: 3 (Default: null) + +`--debug-mode` `` +: Buffer ES request/responses for better error messages and pass ?pretty to all requests (Default: null) + +`--proxy-address` `` +: Route requests through a proxy server (Default: null) + +`--proxy-password` `` +: Proxy server password (Default: null) + +`--proxy-username` `` +: Proxy server username (Default: null) + +`--disable-ssl-verification` `` +: Disable SSL certificate validation (EXPERT OPTION) (Default: null) + +`--certificate-fingerprint` `` +: Pass a self-signed certificate fingerprint to validate the SSL connection (Default: null) + +`--certificate-path` `` +: Pass a self-signed certificate to validate the SSL connection (Default: null) + +`--certificate-not-root` `` +: If the certificate is not root but only part of the validation chain pass this (Default: null) \ No newline at end of file diff --git a/docs/cli/index.md b/docs/cli/index.md new file mode 100644 index 000000000..5a454f483 --- /dev/null +++ b/docs/cli/index.md @@ -0,0 +1,30 @@ +--- +navigation_title: docs-builder +--- + +# Command line interface + +- assemble +- assembler bloom-filter create +- assembler bloom-filter lookup +- assembler build +- assembler clone +- assembler config init +- assembler content-source match +- assembler content-source validate +- assembler deploy apply +- assembler deploy plan +- assembler deploy update-redirects +- assembler index +- assembler navigation validate +- assembler navigation validate-link-reference +- assembler serve +- diff validate +- inbound-links validate +- inbound-links validate-all +- inbound-links validate-link-reference +- index +- mv +- serve + + diff --git a/docs/cli/mv.md b/docs/cli/mv.md new file mode 100644 index 000000000..4afc570ef --- /dev/null +++ b/docs/cli/mv.md @@ -0,0 +1,25 @@ +# mv + +Move a file from one location to another and update all links in the documentation + +## Usage + +``` +mv [arguments...] [options...] [-h|--help] [--version] +``` + +## Arguments + +`[0] ` +: The source file or folder path to move from + +`[1] ` +: The target file or folder path to move to + +## Options + +`--dry-run` `` +: Dry run the move operation (Default: null) + +`-p|--path ` +: Defaults to the`{pwd}` folder (Default: null) \ No newline at end of file diff --git a/docs/cli/serve.md b/docs/cli/serve.md new file mode 100644 index 000000000..a77cb4c72 --- /dev/null +++ b/docs/cli/serve.md @@ -0,0 +1,18 @@ +# serve + +Continuously serve a documentation folder at http://localhost:3000. +File systems changes will be reflected without having to restart the server. + +## Usage + +``` +serve [options...] [-h|--help] [--version] +``` + +## Options + +`-p|--path ` +: Path to serve the documentation. Defaults to the`{pwd}/docs` folder (Default: null) + +`--port` `` +: Port to serve the documentation. (Default: 3000) \ No newline at end of file diff --git a/docs/configure/site/legacy-url-mappings.md b/docs/configure/site/legacy-url-mappings.md index 0d72bb433..294e2cc81 100644 --- a/docs/configure/site/legacy-url-mappings.md +++ b/docs/configure/site/legacy-url-mappings.md @@ -1,3 +1,6 @@ +--- +navigation_title: 'legacy-url-mappings.yml' +--- # Legacy URL mappings This [`legacy-url-mappings.yml`](https://github.com/elastic/docs-builder/blob/main/config/legacy-url-mappings.yml) file manages legacy URL patterns for Elastic documentation, mapping the path of each legacy build URL to a list of documentation versions. It ensures that users can easily find previous versions of our documentation. From 771b84b38ff26105291c291aed5bb0215948ab05 Mon Sep 17 00:00:00 2001 From: Martijn Laarman Date: Tue, 30 Sep 2025 08:20:11 +0200 Subject: [PATCH 002/171] organize cli command documentation --- docs/_docset.yml | 55 ++++++++------- docs/cli/assemble.md | 35 ---------- docs/cli/assembler-clone.md | 23 ------- docs/cli/assembler-content-source-match.md | 15 ---- docs/cli/assembler-content-source-validate.md | 7 -- docs/cli/assembler/assemble.md | 48 +++++++++++++ .../assembler-bloom-filter-create.md | 6 +- .../assembler-bloom-filter-lookup.md | 5 +- docs/cli/{ => assembler}/assembler-build.md | 18 +++-- docs/cli/assembler/assembler-clone.md | 27 ++++++++ .../{ => assembler}/assembler-config-init.md | 10 ++- .../assembler-content-source-match.md | 19 ++++++ .../assembler-content-source-validate.md | 11 +++ .../{ => assembler}/assembler-deploy-apply.md | 6 +- .../{ => assembler}/assembler-deploy-plan.md | 8 ++- .../assembler-deploy-update-redirects.md | 10 ++- docs/cli/{ => assembler}/assembler-index.md | 68 ++++++++++--------- ...bler-navigation-validate-link-reference.md | 8 ++- .../assembler-navigation-validate.md | 6 +- docs/cli/{ => assembler}/assembler-serve.md | 10 ++- docs/cli/assembler/index.md | 5 ++ docs/cli/{generate.md => docset/build.md} | 28 ++++---- docs/cli/{ => docset}/diff-validate.md | 6 +- docs/cli/{ => docset}/index-command.md | 64 ++++++++--------- docs/cli/docset/index.md | 5 ++ docs/cli/{ => docset}/mv.md | 8 +-- docs/cli/{ => docset}/serve.md | 6 +- docs/cli/inbound-links-validate.md | 17 ----- docs/cli/index.md | 2 +- .../{ => links}/inbound-links-validate-all.md | 2 +- .../inbound-links-validate-link-reference.md | 10 +-- docs/cli/links/inbound-links-validate.md | 17 +++++ 32 files changed, 326 insertions(+), 239 deletions(-) delete mode 100644 docs/cli/assemble.md delete mode 100644 docs/cli/assembler-clone.md delete mode 100644 docs/cli/assembler-content-source-match.md delete mode 100644 docs/cli/assembler-content-source-validate.md create mode 100644 docs/cli/assembler/assemble.md rename docs/cli/{ => assembler}/assembler-bloom-filter-create.md (60%) rename docs/cli/{ => assembler}/assembler-bloom-filter-lookup.md (60%) rename docs/cli/{ => assembler}/assembler-build.md (58%) create mode 100644 docs/cli/assembler/assembler-clone.md rename docs/cli/{ => assembler}/assembler-config-init.md (50%) create mode 100644 docs/cli/assembler/assembler-content-source-match.md create mode 100644 docs/cli/assembler/assembler-content-source-validate.md rename docs/cli/{ => assembler}/assembler-deploy-apply.md (72%) rename docs/cli/{ => assembler}/assembler-deploy-plan.md (63%) rename docs/cli/{ => assembler}/assembler-deploy-update-redirects.md (59%) rename docs/cli/{ => assembler}/assembler-index.md (54%) rename docs/cli/{ => assembler}/assembler-navigation-validate-link-reference.md (60%) rename docs/cli/{ => assembler}/assembler-navigation-validate.md (54%) rename docs/cli/{ => assembler}/assembler-serve.md (55%) create mode 100644 docs/cli/assembler/index.md rename docs/cli/{generate.md => docset/build.md} (78%) rename docs/cli/{ => docset}/diff-validate.md (53%) rename docs/cli/{ => docset}/index-command.md (52%) create mode 100644 docs/cli/docset/index.md rename docs/cli/{ => docset}/mv.md (61%) rename docs/cli/{ => docset}/serve.md (77%) delete mode 100644 docs/cli/inbound-links-validate.md rename docs/cli/{ => links}/inbound-links-validate-all.md (64%) rename docs/cli/{ => links}/inbound-links-validate-link-reference.md (54%) create mode 100644 docs/cli/links/inbound-links-validate.md diff --git a/docs/_docset.yml b/docs/_docset.yml index dbbab6312..5833e223a 100644 --- a/docs/_docset.yml +++ b/docs/_docset.yml @@ -104,29 +104,38 @@ toc: - folder: cli children: - file: index.md - - file: assemble.md - - file: assembler-bloom-filter-create.md - - file: assembler-bloom-filter-lookup.md - - file: assembler-build.md - - file: assembler-clone.md - - file: assembler-config-init.md - - file: assembler-content-source-match.md - - file: assembler-content-source-validate.md - - file: assembler-deploy-apply.md - - file: assembler-deploy-plan.md - - file: assembler-deploy-update-redirects.md - - file: assembler-index.md - - file: assembler-navigation-validate.md - - file: assembler-navigation-validate-link-reference.md - - file: assembler-serve.md - - file: diff-validate.md - - file: generate.md - - file: inbound-links-validate.md - - file: inbound-links-validate-all.md - - file: inbound-links-validate-link-reference.md - - file: index-command.md - - file: mv.md - - file: serve.md + - folder: docset + children: + - file: index.md + - file: build.md + - file: diff-validate.md + - file: index-command.md + - file: mv.md + - file: serve.md + - folder: assembler + children: + - file: index.md + - file: assemble.md + - file: assembler-bloom-filter-create.md + - file: assembler-bloom-filter-lookup.md + - file: assembler-build.md + - file: assembler-clone.md + - file: assembler-config-init.md + - file: assembler-content-source-match.md + - file: assembler-content-source-validate.md + - file: assembler-deploy-apply.md + - file: assembler-deploy-plan.md + - file: assembler-deploy-update-redirects.md + - file: assembler-index.md + - file: assembler-navigation-validate.md + - file: assembler-navigation-validate-link-reference.md + - file: assembler-serve.md + - folder: links + children: + - file: index.md + - file: inbound-links-validate.md + - file: inbound-links-validate-all.md + - file: inbound-links-validate-link-reference.md - folder: migration children: - file: index.md diff --git a/docs/cli/assemble.md b/docs/cli/assemble.md deleted file mode 100644 index 2123af23b..000000000 --- a/docs/cli/assemble.md +++ /dev/null @@ -1,35 +0,0 @@ -# assemble - -Do a full assembler clone and assembler build in one swoop - -## Usage - -``` -assemble [options...] [-h|--help] [--version] -``` - -## Options - -`--strict` `` -: Treat warnings as errors and fail the build on warnings (Default: null) - -`--environment` `` -: The environment to build (Default: null) - -`--fetch-latest` `` -: If true, fetch the latest commit of the branch instead of the link registry entry ref (Default: null) - -`--assume-cloned` `` -: If true, assume the repository folder already exists on disk assume it's cloned already, primarily used for testing (Default: null) - -`--metadata-only` `` -: Only emit documentation metadata to output, ignored if 'exporters' is also set (Default: null) - -`--show-hints` `` -: Show hints from all documentation sets during assembler build (Default: null) - -`--exporters` `?>` -: Set available exporters: html, es, config, links, state, llm, redirect, metadata, none. Defaults to (html, config, links, state, redirect) or 'default'. (Default: null) - -`--serve` -: Serve the documentation on port 4000 after successful build (Optional) \ No newline at end of file diff --git a/docs/cli/assembler-clone.md b/docs/cli/assembler-clone.md deleted file mode 100644 index 9758993a1..000000000 --- a/docs/cli/assembler-clone.md +++ /dev/null @@ -1,23 +0,0 @@ -# assembler clone - -Clones all repositories - -## Usage - -``` -assembler clone [options...] [-h|--help] [--version] -``` - -## Options - -`--strict` `` -: Treat warnings as errors and fail the build on warnings (Default: null) - -`--environment` `` -: The environment to build (Default: null) - -`--fetch-latest` `` -: If true, fetch the latest commit of the branch instead of the link registry entry ref (Default: null) - -`--assume-cloned` `` -: If true, assume the repository folder already exists on disk assume it's cloned already, primarily used for testing (Default: null) \ No newline at end of file diff --git a/docs/cli/assembler-content-source-match.md b/docs/cli/assembler-content-source-match.md deleted file mode 100644 index 7927990c6..000000000 --- a/docs/cli/assembler-content-source-match.md +++ /dev/null @@ -1,15 +0,0 @@ -# assembler content-source match - -## Usage - -``` -assembler content-source match [arguments...] [-h|--help] [--version] -``` - -## Arguments - -`[0] ` -: - -`[1] ` -: \ No newline at end of file diff --git a/docs/cli/assembler-content-source-validate.md b/docs/cli/assembler-content-source-validate.md deleted file mode 100644 index cf5e8261b..000000000 --- a/docs/cli/assembler-content-source-validate.md +++ /dev/null @@ -1,7 +0,0 @@ -# assembler content-source validate - -## Usage - -``` -assembler content-source validate [-h|--help] [--version] -``` \ No newline at end of file diff --git a/docs/cli/assembler/assemble.md b/docs/cli/assembler/assemble.md new file mode 100644 index 000000000..135cc499d --- /dev/null +++ b/docs/cli/assembler/assemble.md @@ -0,0 +1,48 @@ +# assemble + +Do a full assembler clone and assembler build in one swoop + +## Usage + +``` +docs-builder assemble [options...] [-h|--help] [--version] +``` + +## Options + +`--strict` `` +: Treat warnings as errors and fail the build on warnings (optional) + +`--environment` `` +: The environment to build (optional) defaults to 'dev' + +`--fetch-latest` `` +: If true, fetch the latest commit of the branch instead of the link registry entry ref (optional) + +`--assume-cloned` `` +: If true, assume the repository folder already exists on disk assume it's cloned already, primarily used for testing (optional) + +`--metadata-only` `` +: Only emit documentation metadata to output, ignored if 'exporters' is also set (optional) + +`--show-hints` `` +: Show hints from all documentation sets during assembler build (optional) + +`--exporters` `` +: Set available exporters: + + * html + * es, + * config, + * links, + * state, + * llm, + * redirect, + * metadata, + * default + * none. + + Defaults to (html, llm, config, links, state, redirect) or 'default'. (optional) + +`--serve` +: Serve the documentation on port 4000 after successful build (Optional) \ No newline at end of file diff --git a/docs/cli/assembler-bloom-filter-create.md b/docs/cli/assembler/assembler-bloom-filter-create.md similarity index 60% rename from docs/cli/assembler-bloom-filter-create.md rename to docs/cli/assembler/assembler-bloom-filter-create.md index ff9a0e483..968a2536b 100644 --- a/docs/cli/assembler-bloom-filter-create.md +++ b/docs/cli/assembler/assembler-bloom-filter-create.md @@ -1,3 +1,7 @@ +--- +navigation_title: "bloom-filter create" +--- + # assembler bloom-filter create Generate the bloom filter binary file @@ -5,7 +9,7 @@ Generate the bloom filter binary file ## Usage ``` -assembler bloom-filter create [options...] [-h|--help] [--version] +docs-builder assembler bloom-filter create [options...] [-h|--help] [--version] ``` ## Options diff --git a/docs/cli/assembler-bloom-filter-lookup.md b/docs/cli/assembler/assembler-bloom-filter-lookup.md similarity index 60% rename from docs/cli/assembler-bloom-filter-lookup.md rename to docs/cli/assembler/assembler-bloom-filter-lookup.md index efc3f76d8..3164f5a9c 100644 --- a/docs/cli/assembler-bloom-filter-lookup.md +++ b/docs/cli/assembler/assembler-bloom-filter-lookup.md @@ -1,3 +1,6 @@ +--- +navigation_title: "bloom-filter lookup" +--- # assembler bloom-filter lookup Lookup whether a path exists in the bloomfilter @@ -5,7 +8,7 @@ Lookup whether a path exists in the bloomfilter ## Usage ``` -assembler bloom-filter lookup [options...] [-h|--help] [--version] +docs-builder assembler bloom-filter lookup [options...] [-h|--help] [--version] ``` ## Options diff --git a/docs/cli/assembler-build.md b/docs/cli/assembler/assembler-build.md similarity index 58% rename from docs/cli/assembler-build.md rename to docs/cli/assembler/assembler-build.md index e84ad4739..bceaa633b 100644 --- a/docs/cli/assembler-build.md +++ b/docs/cli/assembler/assembler-build.md @@ -1,3 +1,7 @@ +--- +navigation_title: "build" +--- + # assembler build Builds all repositories @@ -5,22 +9,22 @@ Builds all repositories ## Usage ``` -assembler build [options...] [-h|--help] [--version] +docs-builder assembler build [options...] [-h|--help] [--version] ``` ## Options `--strict` `` -: Treat warnings as errors and fail the build on warnings (Default: null) +: Treat warnings as errors and fail the build on warnings (optional) -`--environment` `` -: The environment to build (Default: null) +`--environment` `` +: The environment to build (optional) `--metadata-only` `` -: Only emit documentation metadata to output, ignored if 'exporters' is also set (Default: null) +: Only emit documentation metadata to output, ignored if 'exporters' is also set (optional) `--show-hints` `` -: Show hints from all documentation sets during assembler build (Default: null) +: Show hints from all documentation sets during assembler build (optional) `--exporters` `?>` -: Set available exporters: html, es, config, links, state, llm, redirect, metadata, none. Defaults to (html, config, links, state, redirect) or 'default'. (Default: null) \ No newline at end of file +: Set available exporters: html, es, config, links, state, llm, redirect, metadata, none. Defaults to (html, config, links, state, redirect) or 'default'. (optional) \ No newline at end of file diff --git a/docs/cli/assembler/assembler-clone.md b/docs/cli/assembler/assembler-clone.md new file mode 100644 index 000000000..3469bc13d --- /dev/null +++ b/docs/cli/assembler/assembler-clone.md @@ -0,0 +1,27 @@ +--- +navigation_title: "clone" +--- + +# assembler clone + +Clones all repositories + +## Usage + +``` +docs-builder assembler clone [options...] [-h|--help] [--version] +``` + +## Options + +`--strict` `` +: Treat warnings as errors and fail the build on warnings (optional) + +`--environment` `` +: The environment to build (optional) + +`--fetch-latest` `` +: If true, fetch the latest commit of the branch instead of the link registry entry ref (optional) + +`--assume-cloned` `` +: If true, assume the repository folder already exists on disk assume it's cloned already, primarily used for testing (optional) \ No newline at end of file diff --git a/docs/cli/assembler-config-init.md b/docs/cli/assembler/assembler-config-init.md similarity index 50% rename from docs/cli/assembler-config-init.md rename to docs/cli/assembler/assembler-config-init.md index 9eacb7876..ea40dc237 100644 --- a/docs/cli/assembler-config-init.md +++ b/docs/cli/assembler/assembler-config-init.md @@ -1,3 +1,7 @@ +--- +navigation_title: "config init" +--- + # assembler config init Clone the configuration folder @@ -5,13 +9,13 @@ Clone the configuration folder ## Usage ``` -assembler config init [options...] [-h|--help] [--version] +docs-builder assembler config init [options...] [-h|--help] [--version] ``` ## Options -`--git-ref` `` -: The git reference of the config, defaults to 'main' (Default: null) +`--git-ref` `` +: The git reference of the config, defaults to 'main' (optional) `--local` : Save the remote configuration locally in the pwd so later commands can pick it up as local (Optional) \ No newline at end of file diff --git a/docs/cli/assembler/assembler-content-source-match.md b/docs/cli/assembler/assembler-content-source-match.md new file mode 100644 index 000000000..79f2bb890 --- /dev/null +++ b/docs/cli/assembler/assembler-content-source-match.md @@ -0,0 +1,19 @@ +--- +navigation_title: "content-source match" +--- + +# assembler content-source match + +## Usage + +``` +docs-builder assembler content-source match [arguments...] [-h|--help] [--version] +``` + +## Arguments + +`[0] ` +: + +`[1] ` +: \ No newline at end of file diff --git a/docs/cli/assembler/assembler-content-source-validate.md b/docs/cli/assembler/assembler-content-source-validate.md new file mode 100644 index 000000000..e1742c7bf --- /dev/null +++ b/docs/cli/assembler/assembler-content-source-validate.md @@ -0,0 +1,11 @@ +--- +navigation_title: "content-source validate" +--- + +# assembler content-source validate + +## Usage + +``` +docs-builder assembler content-source validate [-h|--help] [--version] +``` \ No newline at end of file diff --git a/docs/cli/assembler-deploy-apply.md b/docs/cli/assembler/assembler-deploy-apply.md similarity index 72% rename from docs/cli/assembler-deploy-apply.md rename to docs/cli/assembler/assembler-deploy-apply.md index 56e97acf5..4e2af8a50 100644 --- a/docs/cli/assembler-deploy-apply.md +++ b/docs/cli/assembler/assembler-deploy-apply.md @@ -1,3 +1,7 @@ +--- +navigation_title: "deploy apply" +--- + # assembler deploy apply Applies a sync plan @@ -5,7 +9,7 @@ Applies a sync plan ## Usage ``` -assembler deploy apply [options...] [-h|--help] [--version] +docs-builder assembler deploy apply [options...] [-h|--help] [--version] ``` ## Options diff --git a/docs/cli/assembler-deploy-plan.md b/docs/cli/assembler/assembler-deploy-plan.md similarity index 63% rename from docs/cli/assembler-deploy-plan.md rename to docs/cli/assembler/assembler-deploy-plan.md index 9476c56fd..83a86d4e6 100644 --- a/docs/cli/assembler-deploy-plan.md +++ b/docs/cli/assembler/assembler-deploy-plan.md @@ -1,3 +1,7 @@ +--- +navigation_title: "deploy plan" +--- + # assembler deploy plan Creates a sync plan @@ -5,7 +9,7 @@ Creates a sync plan ## Usage ``` -assembler deploy plan [options...] [-h|--help] [--version] +docs-builder assembler deploy plan [options...] [-h|--help] [--version] ``` ## Options @@ -20,4 +24,4 @@ assembler deploy plan [options...] [-h|--help] [--version] : The file to write the plan to (Default: "") `--delete-threshold` `` -: The percentage of deletions allowed in the plan as float (Default: null) \ No newline at end of file +: The percentage of deletions allowed in the plan as float (optional) \ No newline at end of file diff --git a/docs/cli/assembler-deploy-update-redirects.md b/docs/cli/assembler/assembler-deploy-update-redirects.md similarity index 59% rename from docs/cli/assembler-deploy-update-redirects.md rename to docs/cli/assembler/assembler-deploy-update-redirects.md index 50907008b..ec0255d5d 100644 --- a/docs/cli/assembler-deploy-update-redirects.md +++ b/docs/cli/assembler/assembler-deploy-update-redirects.md @@ -1,3 +1,7 @@ +--- +navigation_title: "deploy update-redirects" +--- + # assembler deploy update-redirects Refreshes the redirects mapping in Cloudfront's KeyValueStore @@ -5,7 +9,7 @@ Refreshes the redirects mapping in Cloudfront's KeyValueStore ## Usage ``` -assembler deploy update-redirects [options...] [-h|--help] [--version] +docs-builder assembler deploy update-redirects [options...] [-h|--help] [--version] ``` ## Options @@ -13,5 +17,5 @@ assembler deploy update-redirects [options...] [-h|--help] [--version] `--environment` `` : The environment to build (Required) -`--redirects-file` `` -: Path to the redirects mapping pre-generated by docs-assembler (Default: null) \ No newline at end of file +`--redirects-file` `` +: Path to the redirects mapping pre-generated by docs-assembler (optional) \ No newline at end of file diff --git a/docs/cli/assembler-index.md b/docs/cli/assembler/assembler-index.md similarity index 54% rename from docs/cli/assembler-index.md rename to docs/cli/assembler/assembler-index.md index 8b44bff6f..ea8d6947c 100644 --- a/docs/cli/assembler-index.md +++ b/docs/cli/assembler/assembler-index.md @@ -1,3 +1,7 @@ +--- +navigation_title: "index" +--- + # assembler index Index documentation to Elasticsearch, calls `docs-builder assembler build --exporters elasticsearch`. Exposes more options @@ -5,67 +9,67 @@ Index documentation to Elasticsearch, calls `docs-builder assembler build --expo ## Usage ``` -assembler index [options...] [-h|--help] [--version] +docs-builder assembler index [options...] [-h|--help] [--version] ``` ## Options -`-es|--endpoint ` -: Elasticsearch endpoint, alternatively set env DOCUMENTATION_ELASTIC_URL (Default: null) +`-es|--endpoint ` +: Elasticsearch endpoint, alternatively set env DOCUMENTATION_ELASTIC_URL (optional) -`--environment` `` -: The --environment used to clone ends up being part of the index name (Default: null) +`--environment` `` +: The --environment used to clone ends up being part of the index name (optional) -`--api-key` `` -: Elasticsearch API key, alternatively set env DOCUMENTATION_ELASTIC_APIKEY (Default: null) +`--api-key` `` +: Elasticsearch API key, alternatively set env DOCUMENTATION_ELASTIC_APIKEY (optional) -`--username` `` -: Elasticsearch username (basic auth), alternatively set env DOCUMENTATION_ELASTIC_USERNAME (Default: null) +`--username` `` +: Elasticsearch username (basic auth), alternatively set env DOCUMENTATION_ELASTIC_USERNAME (optional) -`--password` `` -: Elasticsearch password (basic auth), alternatively set env DOCUMENTATION_ELASTIC_PASSWORD (Default: null) +`--password` `` +: Elasticsearch password (basic auth), alternatively set env DOCUMENTATION_ELASTIC_PASSWORD (optional) `--no-semantic` `` -: Index without semantic fields (Default: null) +: Index without semantic fields (optional) `--search-num-threads` `` -: The number of search threads the inference endpoint should use. Defaults: 8 (Default: null) +: The number of search threads the inference endpoint should use. Defaults: 8 (optional) `--index-num-threads` `` -: The number of index threads the inference endpoint should use. Defaults: 8 (Default: null) +: The number of index threads the inference endpoint should use. Defaults: 8 (optional) `--bootstrap-timeout` `` -: Timeout in minutes for the inference endpoint creation. Defaults: 4 (Default: null) +: Timeout in minutes for the inference endpoint creation. Defaults: 4 (optional) -`--index-name-prefix` `` -: The prefix for the computed index/alias names. Defaults: semantic-docs (Default: null) +`--index-name-prefix` `` +: The prefix for the computed index/alias names. Defaults: semantic-docs (optional) `--buffer-size` `` -: The number of documents to send to ES as part of the bulk. Defaults: 100 (Default: null) +: The number of documents to send to ES as part of the bulk. Defaults: 100 (optional) `--max-retries` `` -: The number of times failed bulk items should be retried. Defaults: 3 (Default: null) +: The number of times failed bulk items should be retried. Defaults: 3 (optional) `--debug-mode` `` -: Buffer ES request/responses for better error messages and pass ?pretty to all requests (Default: null) +: Buffer ES request/responses for better error messages and pass ?pretty to all requests (optional) -`--proxy-address` `` -: Route requests through a proxy server (Default: null) +`--proxy-address` `` +: Route requests through a proxy server (optional) -`--proxy-password` `` -: Proxy server password (Default: null) +`--proxy-password` `` +: Proxy server password (optional) -`--proxy-username` `` -: Proxy server username (Default: null) +`--proxy-username` `` +: Proxy server username (optional) `--disable-ssl-verification` `` -: Disable SSL certificate validation (EXPERT OPTION) (Default: null) +: Disable SSL certificate validation (EXPERT OPTION) (optional) -`--certificate-fingerprint` `` -: Pass a self-signed certificate fingerprint to validate the SSL connection (Default: null) +`--certificate-fingerprint` `` +: Pass a self-signed certificate fingerprint to validate the SSL connection (optional) -`--certificate-path` `` -: Pass a self-signed certificate to validate the SSL connection (Default: null) +`--certificate-path` `` +: Pass a self-signed certificate to validate the SSL connection (optional) `--certificate-not-root` `` -: If the certificate is not root but only part of the validation chain pass this (Default: null) \ No newline at end of file +: If the certificate is not root but only part of the validation chain pass this (optional) \ No newline at end of file diff --git a/docs/cli/assembler-navigation-validate-link-reference.md b/docs/cli/assembler/assembler-navigation-validate-link-reference.md similarity index 60% rename from docs/cli/assembler-navigation-validate-link-reference.md rename to docs/cli/assembler/assembler-navigation-validate-link-reference.md index ad69081d1..c06ca4dc6 100644 --- a/docs/cli/assembler-navigation-validate-link-reference.md +++ b/docs/cli/assembler/assembler-navigation-validate-link-reference.md @@ -1,3 +1,7 @@ +--- +navigation_title: "navigation validate-link-reference" +--- + # assembler navigation validate-link-reference Validate all published links in links.json do not collide with navigation path_prefixes and all urls are unique. @@ -5,10 +9,10 @@ Validate all published links in links.json do not collide with navigation path_p ## Usage ``` -assembler navigation validate-link-reference [arguments...] [-h|--help] [--version] +docs-builder assembler navigation validate-link-reference [arguments...] [-h|--help] [--version] ``` ## Arguments -`[0] ` +`[0] ` : Path to `links.json` defaults to '.artifacts/docs/html/links.json' \ No newline at end of file diff --git a/docs/cli/assembler-navigation-validate.md b/docs/cli/assembler/assembler-navigation-validate.md similarity index 54% rename from docs/cli/assembler-navigation-validate.md rename to docs/cli/assembler/assembler-navigation-validate.md index d063ab438..d438f9b3c 100644 --- a/docs/cli/assembler-navigation-validate.md +++ b/docs/cli/assembler/assembler-navigation-validate.md @@ -1,3 +1,7 @@ +--- +navigation_title: "navigation validate" +--- + # assembler navigation validate Validates navigation.yml does not contain colliding path prefixes and all urls are unique @@ -5,5 +9,5 @@ Validates navigation.yml does not contain colliding path prefixes and all urls a ## Usage ``` -assembler navigation validate [-h|--help] [--version] +docs-builder assembler navigation validate [-h|--help] [--version] ``` \ No newline at end of file diff --git a/docs/cli/assembler-serve.md b/docs/cli/assembler/assembler-serve.md similarity index 55% rename from docs/cli/assembler-serve.md rename to docs/cli/assembler/assembler-serve.md index c2184b7f6..e96797cca 100644 --- a/docs/cli/assembler-serve.md +++ b/docs/cli/assembler/assembler-serve.md @@ -1,3 +1,7 @@ +--- +navigation_title: "serve" +--- + # assembler serve Serve the output of an assembler build @@ -5,7 +9,7 @@ Serve the output of an assembler build ## Usage ``` -assembler serve [options...] [-h|--help] [--version] +docs-builder assembler serve [options...] [-h|--help] [--version] ``` ## Options @@ -13,5 +17,5 @@ assembler serve [options...] [-h|--help] [--version] `--port` `` : Port to serve the documentation. (Default: 4000) -`--path` `` -: (Default: null) \ No newline at end of file +`--path` `` +: (optional) \ No newline at end of file diff --git a/docs/cli/assembler/index.md b/docs/cli/assembler/index.md new file mode 100644 index 000000000..e43e896a9 --- /dev/null +++ b/docs/cli/assembler/index.md @@ -0,0 +1,5 @@ +--- +navigation_title: "assembler" +--- + +# The assembler namespace \ No newline at end of file diff --git a/docs/cli/generate.md b/docs/cli/docset/build.md similarity index 78% rename from docs/cli/generate.md rename to docs/cli/docset/build.md index ce0aba91a..c071f51c7 100644 --- a/docs/cli/generate.md +++ b/docs/cli/docset/build.md @@ -5,7 +5,7 @@ Converts a source Markdown folder or file to an output folder ## Usage ``` -[command] [options...] [-h|--help] [--version] +docs-builder [command] [options...] [-h|--help] [--version] ``` ## Global Options @@ -14,32 +14,32 @@ Converts a source Markdown folder or file to an output folder ## Options -`-p|--path ` -: Defaults to the`{pwd}/docs` folder (Default: null) +`-p|--path ` +: Defaults to the`{pwd}/docs` folder (optional) -`-o|--output ` -: Defaults to `.artifacts/html` (Default: null) +`-o|--output ` +: Defaults to `.artifacts/html` (optional) -`--path-prefix` `` -: Specifies the path prefix for urls (Default: null) +`--path-prefix` `` +: Specifies the path prefix for urls (optional) `--force` `` -: Force a full rebuild of the destination folder (Default: null) +: Force a full rebuild of the destination folder (optional) `--strict` `` -: Treat warnings as errors and fail the build on warnings (Default: null) +: Treat warnings as errors and fail the build on warnings (optional) `--allow-indexing` `` -: Allow indexing and following of HTML files (Default: null) +: Allow indexing and following of HTML files (optional) `--metadata-only` `` -: Only emit documentation metadata to output, ignored if 'exporters' is also set (Default: null) +: Only emit documentation metadata to output, ignored if 'exporters' is also set (optional) `--exporters` `?>` -: Set available exporters: html, es, config, links, state, llm, redirect, metadata, none. Defaults to (html, config, links, state, redirect) or 'default'. (Default: null) +: Set available exporters: html, es, config, links, state, llm, redirect, metadata, none. Defaults to (html, config, links, state, redirect) or 'default'. (optional) -`--canonical-base-url` `` -: The base URL for the canonical url tag (Default: null) +`--canonical-base-url` `` +: The base URL for the canonical url tag (optional) ## Commands diff --git a/docs/cli/diff-validate.md b/docs/cli/docset/diff-validate.md similarity index 53% rename from docs/cli/diff-validate.md rename to docs/cli/docset/diff-validate.md index 17c1b8a80..e29f5a06a 100644 --- a/docs/cli/diff-validate.md +++ b/docs/cli/docset/diff-validate.md @@ -5,10 +5,10 @@ Validates redirect updates in the current branch using the redirect file against ## Usage ``` -diff validate [options...] [-h|--help] [--version] +docs-builder diff validate [options...] [-h|--help] [--version] ``` ## Options -`-p|--path ` -: Defaults to the`{pwd}/docs` folder (Default: null) \ No newline at end of file +`-p|--path ` +: Defaults to the`{pwd}/docs` folder (optional) \ No newline at end of file diff --git a/docs/cli/index-command.md b/docs/cli/docset/index-command.md similarity index 52% rename from docs/cli/index-command.md rename to docs/cli/docset/index-command.md index 8ebd521c8..833e6a8a6 100644 --- a/docs/cli/index-command.md +++ b/docs/cli/docset/index-command.md @@ -5,67 +5,67 @@ Index a single documentation set to Elasticsearch, calls `docs-builder --exporte ## Usage ``` -index [options...] [-h|--help] [--version] +docs-builder index [options...] [-h|--help] [--version] ``` ## Options -`-es|--endpoint ` -: Elasticsearch endpoint, alternatively set env DOCUMENTATION_ELASTIC_URL (Default: null) +`-es|--endpoint ` +: Elasticsearch endpoint, alternatively set env DOCUMENTATION_ELASTIC_URL (optional) -`--path` `` -: path to the documentation folder, defaults to pwd. (Default: null) +`--path` `` +: path to the documentation folder, defaults to pwd. (optional) -`--api-key` `` -: Elasticsearch API key, alternatively set env DOCUMENTATION_ELASTIC_APIKEY (Default: null) +`--api-key` `` +: Elasticsearch API key, alternatively set env DOCUMENTATION_ELASTIC_APIKEY (optional) -`--username` `` -: Elasticsearch username (basic auth), alternatively set env DOCUMENTATION_ELASTIC_USERNAME (Default: null) +`--username` `` +: Elasticsearch username (basic auth), alternatively set env DOCUMENTATION_ELASTIC_USERNAME (optional) -`--password` `` -: Elasticsearch password (basic auth), alternatively set env DOCUMENTATION_ELASTIC_PASSWORD (Default: null) +`--password` `` +: Elasticsearch password (basic auth), alternatively set env DOCUMENTATION_ELASTIC_PASSWORD (optional) `--no-semantic` `` -: Index without semantic fields (Default: null) +: Index without semantic fields (optional) `--search-num-threads` `` -: The number of search threads the inference endpoint should use. Defaults: 8 (Default: null) +: The number of search threads the inference endpoint should use. Defaults: 8 (optional) `--index-num-threads` `` -: The number of index threads the inference endpoint should use. Defaults: 8 (Default: null) +: The number of index threads the inference endpoint should use. Defaults: 8 (optional) `--bootstrap-timeout` `` -: Timeout in minutes for the inference endpoint creation. Defaults: 4 (Default: null) +: Timeout in minutes for the inference endpoint creation. Defaults: 4 (optional) -`--index-name-prefix` `` -: The prefix for the computed index/alias names. Defaults: semantic-docs (Default: null) +`--index-name-prefix` `` +: The prefix for the computed index/alias names. Defaults: semantic-docs (optional) `--buffer-size` `` -: The number of documents to send to ES as part of the bulk. Defaults: 100 (Default: null) +: The number of documents to send to ES as part of the bulk. Defaults: 100 (optional) `--max-retries` `` -: The number of times failed bulk items should be retried. Defaults: 3 (Default: null) +: The number of times failed bulk items should be retried. Defaults: 3 (optional) `--debug-mode` `` -: Buffer ES request/responses for better error messages and pass ?pretty to all requests (Default: null) +: Buffer ES request/responses for better error messages and pass ?pretty to all requests (optional) -`--proxy-address` `` -: Route requests through a proxy server (Default: null) +`--proxy-address` `` +: Route requests through a proxy server (optional) -`--proxy-password` `` -: Proxy server password (Default: null) +`--proxy-password` `` +: Proxy server password (optional) -`--proxy-username` `` -: Proxy server username (Default: null) +`--proxy-username` `` +: Proxy server username (optional) `--disable-ssl-verification` `` -: Disable SSL certificate validation (EXPERT OPTION) (Default: null) +: Disable SSL certificate validation (EXPERT OPTION) (optional) -`--certificate-fingerprint` `` -: Pass a self-signed certificate fingerprint to validate the SSL connection (Default: null) +`--certificate-fingerprint` `` +: Pass a self-signed certificate fingerprint to validate the SSL connection (optional) -`--certificate-path` `` -: Pass a self-signed certificate to validate the SSL connection (Default: null) +`--certificate-path` `` +: Pass a self-signed certificate to validate the SSL connection (optional) `--certificate-not-root` `` -: If the certificate is not root but only part of the validation chain pass this (Default: null) \ No newline at end of file +: If the certificate is not root but only part of the validation chain pass this (optional) \ No newline at end of file diff --git a/docs/cli/docset/index.md b/docs/cli/docset/index.md new file mode 100644 index 000000000..c456dee22 --- /dev/null +++ b/docs/cli/docset/index.md @@ -0,0 +1,5 @@ +--- +navigation_title: "documentation set" +--- + +# Documentation set commands \ No newline at end of file diff --git a/docs/cli/mv.md b/docs/cli/docset/mv.md similarity index 61% rename from docs/cli/mv.md rename to docs/cli/docset/mv.md index 4afc570ef..ec803baf9 100644 --- a/docs/cli/mv.md +++ b/docs/cli/docset/mv.md @@ -5,7 +5,7 @@ Move a file from one location to another and update all links in the documentati ## Usage ``` -mv [arguments...] [options...] [-h|--help] [--version] +docs-builder mv [arguments...] [options...] [-h|--help] [--version] ``` ## Arguments @@ -19,7 +19,7 @@ mv [arguments...] [options...] [-h|--help] [--version] ## Options `--dry-run` `` -: Dry run the move operation (Default: null) +: Dry run the move operation (optional) -`-p|--path ` -: Defaults to the`{pwd}` folder (Default: null) \ No newline at end of file +`-p|--path ` +: Defaults to the`{pwd}` folder (optional) \ No newline at end of file diff --git a/docs/cli/serve.md b/docs/cli/docset/serve.md similarity index 77% rename from docs/cli/serve.md rename to docs/cli/docset/serve.md index a77cb4c72..c3284eb18 100644 --- a/docs/cli/serve.md +++ b/docs/cli/docset/serve.md @@ -6,13 +6,13 @@ File systems changes will be reflected without having to restart the server. ## Usage ``` -serve [options...] [-h|--help] [--version] +docs-builder serve [options...] [-h|--help] [--version] ``` ## Options -`-p|--path ` -: Path to serve the documentation. Defaults to the`{pwd}/docs` folder (Default: null) +`-p|--path ` +: Path to serve the documentation. Defaults to the`{pwd}/docs` folder (optional) `--port` `` : Port to serve the documentation. (Default: 3000) \ No newline at end of file diff --git a/docs/cli/inbound-links-validate.md b/docs/cli/inbound-links-validate.md deleted file mode 100644 index 16e36710c..000000000 --- a/docs/cli/inbound-links-validate.md +++ /dev/null @@ -1,17 +0,0 @@ -# inbound-links validate - -Validate all published cross_links in all published links.json files. - -## Usage - -``` -inbound-links validate [options...] [-h|--help] [--version] -``` - -## Options - -`--from` `` -: (Default: null) - -`--to` `` -: (Default: null) \ No newline at end of file diff --git a/docs/cli/index.md b/docs/cli/index.md index 5a454f483..0d92b0c57 100644 --- a/docs/cli/index.md +++ b/docs/cli/index.md @@ -1,5 +1,5 @@ --- -navigation_title: docs-builder +navigation_title: docs-builder CLI --- # Command line interface diff --git a/docs/cli/inbound-links-validate-all.md b/docs/cli/links/inbound-links-validate-all.md similarity index 64% rename from docs/cli/inbound-links-validate-all.md rename to docs/cli/links/inbound-links-validate-all.md index 5f4cc0a91..3563db79d 100644 --- a/docs/cli/inbound-links-validate-all.md +++ b/docs/cli/links/inbound-links-validate-all.md @@ -5,5 +5,5 @@ Validate all published cross_links in all published links.json files. ## Usage ``` -inbound-links validate-all [-h|--help] [--version] +docs-builder inbound-links validate-all [-h|--help] [--version] ``` \ No newline at end of file diff --git a/docs/cli/inbound-links-validate-link-reference.md b/docs/cli/links/inbound-links-validate-link-reference.md similarity index 54% rename from docs/cli/inbound-links-validate-link-reference.md rename to docs/cli/links/inbound-links-validate-link-reference.md index 502f4ec0a..b9eaca3c2 100644 --- a/docs/cli/inbound-links-validate-link-reference.md +++ b/docs/cli/links/inbound-links-validate-link-reference.md @@ -5,13 +5,13 @@ Validate a locally published links.json file against all published links.json fi ## Usage ``` -inbound-links validate-link-reference [options...] [-h|--help] [--version] +docs-builder inbound-links validate-link-reference [options...] [-h|--help] [--version] ``` ## Options -`--file` `` -: Path to `links.json` defaults to '.artifacts/docs/html/links.json' (Default: null) +`--file` `` +: Path to `links.json` defaults to '.artifacts/docs/html/links.json' (optional) -`-p|--path ` -: Defaults to the `{pwd}` folder (Default: null) \ No newline at end of file +`-p|--path ` +: Defaults to the `{pwd}` folder (optional) \ No newline at end of file diff --git a/docs/cli/links/inbound-links-validate.md b/docs/cli/links/inbound-links-validate.md new file mode 100644 index 000000000..80036eec2 --- /dev/null +++ b/docs/cli/links/inbound-links-validate.md @@ -0,0 +1,17 @@ +# inbound-links validate + +Validate all published cross_links in all published links.json files. + +## Usage + +``` +docs-builder inbound-links validate [options...] [-h|--help] [--version] +``` + +## Options + +`--from` `` +: (optional) + +`--to` `` +: (optional) \ No newline at end of file From 335d7a6e70a7fada202b24d7c1819fb8e9274ebd Mon Sep 17 00:00:00 2001 From: Martijn Laarman Date: Tue, 30 Sep 2025 16:34:39 +0200 Subject: [PATCH 003/171] stage changes --- docs/cli/assembler/index.md | 61 ++++++++++++++++++++++++++++++++++++- docs/cli/docset/build.md | 2 +- docs/cli/docset/index.md | 23 +++++++++++++- docs/cli/index.md | 61 ++++++++++++++++++++++++------------- docs/cli/links/index.md | 15 +++++++++ 5 files changed, 137 insertions(+), 25 deletions(-) create mode 100644 docs/cli/links/index.md diff --git a/docs/cli/assembler/index.md b/docs/cli/assembler/index.md index e43e896a9..6c92bac9e 100644 --- a/docs/cli/assembler/index.md +++ b/docs/cli/assembler/index.md @@ -2,4 +2,63 @@ navigation_title: "assembler" --- -# The assembler namespace \ No newline at end of file +# Assembler commands + +Assembler builds bring together all isolated builds and turn them into the overall documentation that gets published. + +If you want to build the latest documentation, you can do so using the following commands + +:::{note} +When assembling using the `config init --local` option, it's advised to create an empty directory to run these commands in. +This creates a dedicated workspace for the assembler build and any local changes that you might want to test. +::: + +```bash +docs-builder assembler config init --local +docs-builder assemble --serve +``` + +The full assembled documentation should now be running at http://localhost:4000. + +The [assemble](assemble.md) command is syntactic sugar over the following commands: + +```bash +docs-builder assembler config init --local +docs-builder assembler clone +docs-builder assembler build +docs-builder assembler serve +``` + +Which may be more appropriate to call in isolation depending on the workflow you are going for. + +All `assembler` commans take an `--environment ` argument that defaults to 'dev' but can be set e.g to 'prod' to +build the production documentation. See [assembler.yml](../../configure/site/index.md) configuration for which environments are +available + +## Build commands + +- [assemble](assemble.md) +- [assembler build](assembler-build.md) +- [assembler clone](assembler-clone.md) +- [assembler config init](assembler-config-init.md) +- [assembler index](assembler-index.md) +- [assembler serve](assembler-serve.md) + +## Specialized build commands + +- [assembler bloom-filter create](assembler-bloom-filter-create.md) +- [assembler bloom-filter lookup](assembler-bloom-filter-lookup.md) + +## Validation commands + +- [assembler content-source match](assembler-content-source-match.md) +- [assembler content-source validate](assembler-content-source-validate.md) +- [assembler navigation validate](assembler-navigation-validate.md) +- [assembler navigation validate-link-reference](assembler-navigation-validate-link-reference.md) + +## Deploy commands + +- [assembler deploy apply](assembler-deploy-apply.md) +- [assembler deploy plan](assembler-deploy-plan.md) +- [assembler deploy update-redirects](assembler-deploy-update-redirects.md) + diff --git a/docs/cli/docset/build.md b/docs/cli/docset/build.md index c071f51c7..fcac7e3da 100644 --- a/docs/cli/docset/build.md +++ b/docs/cli/docset/build.md @@ -1,4 +1,4 @@ -# generate (root command) +# build Converts a source Markdown folder or file to an output folder diff --git a/docs/cli/docset/index.md b/docs/cli/docset/index.md index c456dee22..fc11c183c 100644 --- a/docs/cli/docset/index.md +++ b/docs/cli/docset/index.md @@ -2,4 +2,25 @@ navigation_title: "documentation set" --- -# Documentation set commands \ No newline at end of file +# Documentation Set Commands + +An isolated build means building a single documentation set. + +A `Documentation Set` is defined as a folder containing a [docset.yml](../configure/content-set/index.md) file. + +These commands are typically what you interface with when you are working on the documentation of a single repository locally. + +## Isolated build commands + +`build` is the default command so you can just run `docs-builder` to build a single documentation set. `docs-builder` will +locate the `docset.yml` anywhere in the directory tree automatically and build the documentation. + +- [build](docset/build.md) - build a single documentation set (incrementally) +- [serve](docset/serve.md) - partial build and serve documentation as needed at http://localhost:3000 +- [index](docset/index-command.md) - ingest a single documentation set to an Elasticsearch index. + +## Refactor commands + +- [mv](docset/mv.md) - move a file or folder to a new location. This will rewrite all links in all files too. +- [diff validate](docset/diff-validate.md) - validate that local changes are reflected in [redirects.yml](../contribute/redirects.md) + diff --git a/docs/cli/index.md b/docs/cli/index.md index 0d92b0c57..84db961d4 100644 --- a/docs/cli/index.md +++ b/docs/cli/index.md @@ -4,27 +4,44 @@ navigation_title: docs-builder CLI # Command line interface -- assemble -- assembler bloom-filter create -- assembler bloom-filter lookup -- assembler build -- assembler clone -- assembler config init -- assembler content-source match -- assembler content-source validate -- assembler deploy apply -- assembler deploy plan -- assembler deploy update-redirects -- assembler index -- assembler navigation validate -- assembler navigation validate-link-reference -- assembler serve -- diff validate -- inbound-links validate -- inbound-links validate-all -- inbound-links validate-link-reference -- index -- mv -- serve +`docs-builder` is the binary used to invoke various commands. +These commands can be roughly grouped into three main categories +- [Isolated commands](#isolated-commands) +- [Link commands](#link-commands) +- [Assembler commands](#assembler-commands) +### Global options + +The following options are available for all commands: + +`--log-level ` +: Change the log level one of ( `trace`, `debug`, `info`, `warn`, `error`, `critical`). Defaults to `info` + +`--config-source` or `-c` +: Explicitly set the configuration source one of `local`, `remote` or `embedded`. Defaults to `local` if available + other wise `embedded` + +## Isolated Commands + +An isolated build means building a single documentation set. + +A `Documentation Set` is defined as a folder containing a [docset.yml](../configure/content-set/index.md) file. + +These commands are typically what you interface with when you are working on the documentation of a single repository locally. + +[See available CLI commands for documentation sets](docset/index.md) + +## Link Commands + +Outbound links, those going from the documentation set to other sources, are validated as part of the build process. + +Inbound links, those going from other sources to the documentation set, are validated using specialized commands. + +[See available CLI commands for inbound links](links/index.md) + +## Assembler Commands + +Assembler builds bring together all isolated builds and turn them into the overall documentation that gets published. + +[See available CLI commands for assembler](assembler/index.md) diff --git a/docs/cli/links/index.md b/docs/cli/links/index.md new file mode 100644 index 000000000..60f838ba3 --- /dev/null +++ b/docs/cli/links/index.md @@ -0,0 +1,15 @@ +--- +navigation_title: links +--- + +# Inbound Links + +Outbound links, those going from the documentation set to other sources, are validated as part of the build process. + +Inbound links, those going from other sources to the documentation set, are validated using specialized commands. + +### Inbound link validation commands + +- [inbound-links validate-all](links/inbound-links-validate-all.md) - validate all inbounds links as published to the links registry. +- [inbound-links validate](links/inbound-links-validate.md) - validate inbound links from and to specific repositories +- [inbound-links validate-link-reference](links/inbound-links-validate-link-reference.md) - validate a local link reference artifact from [build](docset/build.md) with the published registry From 9b65802d4dcdfb0655c1f14dfecf1438d1a3b0f2 Mon Sep 17 00:00:00 2001 From: Martijn Laarman Date: Tue, 30 Sep 2025 18:51:56 +0200 Subject: [PATCH 004/171] Finish documenting cli commands --- docs/cli/assembler/assemble.md | 45 ++++++++- .../assembler-bloom-filter-create.md | 8 +- .../assembler-bloom-filter-lookup.md | 2 +- docs/cli/assembler/assembler-build.md | 27 +++++- docs/cli/assembler/assembler-clone.md | 4 +- docs/cli/assembler/assembler-config-init.md | 12 ++- .../assembler-content-source-match.md | 24 ++++- .../assembler-content-source-validate.md | 2 + docs/cli/assembler/assembler-deploy-apply.md | 2 +- docs/cli/assembler/assembler-deploy-plan.md | 2 +- ...bler-navigation-validate-link-reference.md | 2 + .../assembler-navigation-validate.md | 2 +- docs/cli/assembler/assembler-serve.md | 2 +- docs/cli/docset/build.md | 92 +++++-------------- docs/cli/docset/diff-validate.md | 6 +- docs/cli/docset/mv.md | 6 +- docs/cli/docset/serve.md | 7 +- .../Commands/IsolatedBuildCommand.cs | 2 +- 18 files changed, 154 insertions(+), 93 deletions(-) diff --git a/docs/cli/assembler/assemble.md b/docs/cli/assembler/assemble.md index 135cc499d..e8c5bcc8a 100644 --- a/docs/cli/assembler/assemble.md +++ b/docs/cli/assembler/assemble.md @@ -1,6 +1,6 @@ # assemble -Do a full assembler clone and assembler build in one swoop +Do a full assembler clone, build and optional serving of the full documentation in one swoop ## Usage @@ -8,6 +8,49 @@ Do a full assembler clone and assembler build in one swoop docs-builder assemble [options...] [-h|--help] [--version] ``` + + +## Usage examples + +The following will clone the repository, build the documentation and serve it on port 4000 using the embedded configuration inside the `docs-builder` binary. + +```bash +docs-builder assemble --serve +``` + +This single command is equivalent to the following commands: + +```bash +docs-builder assembler clone +docs-builder assembler build +docs-builder assembler serve +``` + +### Using a local workspace for assembler builds + +Where this command really shines is when you want to create a temporary workspace folder to validate: + +* changes to [site wide configuration](../../configure/site/index.md) +* changes to one or more repositories and their effect on the assembler build. + +To do that inside an empty folder, call: + +```bash +docs-builder assembler config init --local +docs-builder assemble --serve +``` + +This will source the latest configuration from [The `config` folder on the `main` branch of `docs-builder`](https://github.com/elastic/docs-builder/tree/main/config) +and place them inside the `$(pwd)/config` folder. + +Now when you call `docs-builder assemble` rather than using the embedded configuration, it will use local one that one you just created. +You can be explicit about the configuration source to use: + +```bash +docs-builder assembler config init --local +docs-builder assemble --serve -c local +``` + ## Options `--strict` `` diff --git a/docs/cli/assembler/assembler-bloom-filter-create.md b/docs/cli/assembler/assembler-bloom-filter-create.md index 968a2536b..cda2ec4a2 100644 --- a/docs/cli/assembler/assembler-bloom-filter-create.md +++ b/docs/cli/assembler/assembler-bloom-filter-create.md @@ -4,7 +4,13 @@ navigation_title: "bloom-filter create" # assembler bloom-filter create -Generate the bloom filter binary file +Generates a bloom filter that gets embedded into the `docs-builder` binary. + +This bloom filter is used to determine whether a document's `mapped_page` in the frontmatter exists in + +the project of [legacy-url-mappings](../../configure/site/legacy-url-mappings.md) + +The existence determines how the document history selector should be populated. ## Usage diff --git a/docs/cli/assembler/assembler-bloom-filter-lookup.md b/docs/cli/assembler/assembler-bloom-filter-lookup.md index 3164f5a9c..bdb302712 100644 --- a/docs/cli/assembler/assembler-bloom-filter-lookup.md +++ b/docs/cli/assembler/assembler-bloom-filter-lookup.md @@ -3,7 +3,7 @@ navigation_title: "bloom-filter lookup" --- # assembler bloom-filter lookup -Lookup whether a path exists in the bloomfilter +Test command to assert if an old V2 url matches with our bloom filter ## Usage diff --git a/docs/cli/assembler/assembler-build.md b/docs/cli/assembler/assembler-build.md index bceaa633b..6134277a1 100644 --- a/docs/cli/assembler/assembler-build.md +++ b/docs/cli/assembler/assembler-build.md @@ -4,7 +4,14 @@ navigation_title: "build" # assembler build -Builds all repositories +:::note +This command requires that you've previously ran `docs-builder assembler clone` to clone the documentation sets. +If you clone using a certain `--environment` you must also use that same `--environment` when building. +::: + +Builds all the documentation sets and assembles them into an assembled complete documentation site that's ready to be deployed. + +It uses [the site configuration files](../../configure/site/index.md) to direct how the documentation sets should be assembled. ## Usage @@ -26,5 +33,19 @@ docs-builder assembler build [options...] [-h|--help] [--version] `--show-hints` `` : Show hints from all documentation sets during assembler build (optional) -`--exporters` `?>` -: Set available exporters: html, es, config, links, state, llm, redirect, metadata, none. Defaults to (html, config, links, state, redirect) or 'default'. (optional) \ No newline at end of file +`--exporters` `` +: Set available exporters: + + * html + * es, + * config, + * links, + * state, + * llm, + * redirect, + * metadata, + * default + * none. + + Defaults to (html, llm, config, links, state, redirect) or 'default'. (optional) + diff --git a/docs/cli/assembler/assembler-clone.md b/docs/cli/assembler/assembler-clone.md index 3469bc13d..0a555990f 100644 --- a/docs/cli/assembler/assembler-clone.md +++ b/docs/cli/assembler/assembler-clone.md @@ -4,7 +4,9 @@ navigation_title: "clone" # assembler clone -Clones all repositories +Clones all repositories. Defaults to `$(pwd)/.artifacts/checkouts/{content_source}`. + +The `content_source` is the `content_source` of the `--environment` option as configured in `assembly.yaml` ## Usage diff --git a/docs/cli/assembler/assembler-config-init.md b/docs/cli/assembler/assembler-config-init.md index ea40dc237..b113be0e4 100644 --- a/docs/cli/assembler/assembler-config-init.md +++ b/docs/cli/assembler/assembler-config-init.md @@ -4,7 +4,17 @@ navigation_title: "config init" # assembler config init -Clone the configuration folder +Sources the configuration from [The `config` folder on the `main` branch of `docs-builder`](https://github.com/elastic/docs-builder/tree/main/config) + +By default, the configuration is placed in a special application folder as its main usages is to be used by CI environments. + +* OSX: `~/Library/Application Support/docs-builder` [NSApplicationSupportDirectory](https://developer.apple.com/documentation/foundation/filemanager/searchpathdirectory/applicationsupportdirectory) +* Linux: `~/.config/docs-builder` +* Windows: `%APPDATA%\docs-builder` + +You can also use the `--local` option to save the configuration locally in the current working directory. This exposes a great way to assemble the full documentation locally in an empty directory. + +See [using assemble to create local workspaces](assemble.md#using-a-local-workspace-for-assembler-builds) for more information. ## Usage diff --git a/docs/cli/assembler/assembler-content-source-match.md b/docs/cli/assembler/assembler-content-source-match.md index 79f2bb890..67781a42a 100644 --- a/docs/cli/assembler/assembler-content-source-match.md +++ b/docs/cli/assembler/assembler-content-source-match.md @@ -4,16 +4,30 @@ navigation_title: "content-source match" # assembler content-source match +This command is used to match a repository and branch to a content source it will emit the following `$GITHUB_OUTPUT`: + +* `content-source-match` - whether the branch is a configured content source +* `content-source-next` - whether the branch is the next content source +* `content-source-current` - whether the branch is the current content source +* `content-source-speculative` - whether the branch is a speculative content source. + +#### Speculative builds + +If branches follow semantic versioning, if a branch is cut that is greater than the current version, it will be considered a speculative build. +`docs-builer`'s shared workflow will trigger even if it's not specified as a content source in `assembler.yml`. + +This allows a branch `links.json` to be published to the `Link Service` a head of time before it's configured as a content source. + ## Usage ``` -docs-builder assembler content-source match [arguments...] [-h|--help] [--version] +docs-builder assembler content-source match [-h|--help] [--version] ``` ## Arguments -`[0] ` -: +` +: The name of the `elastic/` repository you want to match if it should be build on CI -`[1] ` -: \ No newline at end of file +` +: The branch you want to match if it should be build on CI` \ No newline at end of file diff --git a/docs/cli/assembler/assembler-content-source-validate.md b/docs/cli/assembler/assembler-content-source-validate.md index e1742c7bf..661269a81 100644 --- a/docs/cli/assembler/assembler-content-source-validate.md +++ b/docs/cli/assembler/assembler-content-source-validate.md @@ -4,6 +4,8 @@ navigation_title: "content-source validate" # assembler content-source validate +Validates that the configured content source branches are publishing succesfully to the `Links Service`. + ## Usage ``` diff --git a/docs/cli/assembler/assembler-deploy-apply.md b/docs/cli/assembler/assembler-deploy-apply.md index 4e2af8a50..ce1c0df27 100644 --- a/docs/cli/assembler/assembler-deploy-apply.md +++ b/docs/cli/assembler/assembler-deploy-apply.md @@ -4,7 +4,7 @@ navigation_title: "deploy apply" # assembler deploy apply -Applies a sync plan +Applies an incremental synchronization plan created by [`docs-builder assembler deploy plan`](./assembler-deploy-plan). ## Usage diff --git a/docs/cli/assembler/assembler-deploy-plan.md b/docs/cli/assembler/assembler-deploy-plan.md index 83a86d4e6..cf4e5c5e7 100644 --- a/docs/cli/assembler/assembler-deploy-plan.md +++ b/docs/cli/assembler/assembler-deploy-plan.md @@ -4,7 +4,7 @@ navigation_title: "deploy plan" # assembler deploy plan -Creates a sync plan +Creates an incremental synchronization plan by comparing the reote `--s3-bucket-name` with the local output of the build. ## Usage diff --git a/docs/cli/assembler/assembler-navigation-validate-link-reference.md b/docs/cli/assembler/assembler-navigation-validate-link-reference.md index c06ca4dc6..678724217 100644 --- a/docs/cli/assembler/assembler-navigation-validate-link-reference.md +++ b/docs/cli/assembler/assembler-navigation-validate-link-reference.md @@ -6,6 +6,8 @@ navigation_title: "navigation validate-link-reference" Validate all published links in links.json do not collide with navigation path_prefixes and all urls are unique. +Read more about [navigation](../../configure/site/navigation.md). + ## Usage ``` diff --git a/docs/cli/assembler/assembler-navigation-validate.md b/docs/cli/assembler/assembler-navigation-validate.md index d438f9b3c..4e897d41e 100644 --- a/docs/cli/assembler/assembler-navigation-validate.md +++ b/docs/cli/assembler/assembler-navigation-validate.md @@ -4,7 +4,7 @@ navigation_title: "navigation validate" # assembler navigation validate -Validates navigation.yml does not contain colliding path prefixes and all urls are unique +Validates [navigation.yml](../../configure/site/navigation.md) does not contain colliding path prefixes and all urls are unique ## Usage diff --git a/docs/cli/assembler/assembler-serve.md b/docs/cli/assembler/assembler-serve.md index e96797cca..805d1b438 100644 --- a/docs/cli/assembler/assembler-serve.md +++ b/docs/cli/assembler/assembler-serve.md @@ -4,7 +4,7 @@ navigation_title: "serve" # assembler serve -Serve the output of an assembler build +Serve the output of an assembler build on `http://localhost:4000/` ## Usage diff --git a/docs/cli/docset/build.md b/docs/cli/docset/build.md index fcac7e3da..d300c6ee9 100644 --- a/docs/cli/docset/build.md +++ b/docs/cli/docset/build.md @@ -1,6 +1,12 @@ # build -Converts a source Markdown folder or file to an output folder +Builds a local documentation set folder. + +Repeated invocations will do incremental builds of only the changed files unless: + +* The base branch has changed +* The state file in the output folder has been removed +* The `--force` option is specified. ## Usage @@ -35,76 +41,22 @@ docs-builder [command] [options...] [-h|--help] [--version] `--metadata-only` `` : Only emit documentation metadata to output, ignored if 'exporters' is also set (optional) -`--exporters` `?>` -: Set available exporters: html, es, config, links, state, llm, redirect, metadata, none. Defaults to (html, config, links, state, redirect) or 'default'. (optional) - -`--canonical-base-url` `` -: The base URL for the canonical url tag (optional) - -## Commands - -`assemble` -: Do a full assembler clone and assembler build in one swoop - -`assembler bloom-filter create` -: Generate the bloom filter binary file - -`assembler bloom-filter lookup` -: Lookup whether path exists in the bloomfilter - -`assembler build` -: Builds all repositories - -`assembler clone` -: Clones all repositories - -`assembler config init` -: Clone the configuration folder - -`assembler content-source match` -: +`--exporters` `` +: Set available exporters: -`assembler content-source validate` -: + * html + * es, + * config, + * links, + * state, + * llm, + * redirect, + * metadata, + * default + * none. -`assembler deploy apply` -: Applies a sync plan + Defaults to (html, llm, config, links, state, redirect) or 'default'. (optional) -`assembler deploy plan` -: Creates a sync plan -`assembler deploy update-redirects` -: Refreshes the redirects mapping in Cloudfront's KeyValueStore - -`assembler index` -: Index documentation to Elasticsearch, calls `docs-builder assembler build --exporters elasticsearch`. Exposes more options - -`assembler navigation validate` -: Validates navigation.yml does not contain colliding path prefixes and all urls are unique - -`assembler navigation validate-link-reference` -: Validate all published links in links.json do not collide with navigation path_prefixes and all urls are unique. - -`assembler serve` -: Serve the output of an assembler build - -`diff validate` -: Validates redirect updates in the current branch using the redirect file against changes reported by git. - -`inbound-links validate` -: Validate all published cross_links in all published links.json files. - -`inbound-links validate-all` -: Validate all published cross_links in all published links.json files. - -`inbound-links validate-link-reference` -: Validate a locally published links.json file against all published links.json files in the registry - -`index` -: Index a single documentation set to Elasticsearch, calls `docs-builder --exporters elasticsearch`. Exposes more options - -`mv` -: Move a file from one location to another and update all links in the documentation - -`serve` -: Continuously serve a documentation folder at http://localhost:3000. File systems changes will be reflected without having to restart the server. \ No newline at end of file +`--canonical-base-url` `` +: The base URL for the canonical url tag (optional) \ No newline at end of file diff --git a/docs/cli/docset/diff-validate.md b/docs/cli/docset/diff-validate.md index e29f5a06a..02c97d84b 100644 --- a/docs/cli/docset/diff-validate.md +++ b/docs/cli/docset/diff-validate.md @@ -1,6 +1,10 @@ # diff validate -Validates redirect updates in the current branch using the redirect file against changes reported by git. +Gathers the local changes by inspecting the git log, stashed and unstashed changes. + +It currently validates the following: + +* Ensures that renames and deletions are reflected in [redirects.yml](../../contribute/redirects.md) ## Usage diff --git a/docs/cli/docset/mv.md b/docs/cli/docset/mv.md index ec803baf9..173563b1a 100644 --- a/docs/cli/docset/mv.md +++ b/docs/cli/docset/mv.md @@ -1,6 +1,6 @@ # mv -Move a file from one location to another and update all links in the documentation +Move a file or folder from one location to another and update all links in the documentation ## Usage @@ -11,10 +11,10 @@ docs-builder mv [arguments...] [options...] [-h|--help] [--version] ## Arguments `[0] ` -: The source file or folder path to move from +: The source file or folder path to move from (required) `[1] ` -: The target file or folder path to move to +: The target file or folder path to move to (required) ## Options diff --git a/docs/cli/docset/serve.md b/docs/cli/docset/serve.md index c3284eb18..018b995fe 100644 --- a/docs/cli/docset/serve.md +++ b/docs/cli/docset/serve.md @@ -1,7 +1,12 @@ # serve Continuously serve a documentation folder at http://localhost:3000. -File systems changes will be reflected without having to restart the server. + +When running `docs-builder serve`, the documentation is not built in full. +Each page will be build on the fly continuously when requested in the browser. + +The `serve` command is also `live reload` enabled so that file systems changes will be reflected without having to restart the server. +This includes changes to the documentation files, the navigation, or the configuration files. ## Usage diff --git a/src/tooling/docs-builder/Commands/IsolatedBuildCommand.cs b/src/tooling/docs-builder/Commands/IsolatedBuildCommand.cs index 4673fc5d0..0dd4514d6 100644 --- a/src/tooling/docs-builder/Commands/IsolatedBuildCommand.cs +++ b/src/tooling/docs-builder/Commands/IsolatedBuildCommand.cs @@ -23,7 +23,7 @@ IConfigurationContext configurationContext ) { /// - /// Converts a source Markdown folder or file to an output folder + /// Builds a source documentation set folder. /// global options: /// --log-level level /// From 69bf2d2b164e9889e6af837ecdea12d36d5846a6 Mon Sep 17 00:00:00 2001 From: Martijn Laarman Date: Tue, 30 Sep 2025 18:58:32 +0200 Subject: [PATCH 005/171] Cleanup migration --- docs/_docset.yml | 4 --- docs/cli/index.md | 2 +- docs/migration/freeze/gh-action.md | 32 ------------------------ docs/migration/freeze/index.md | 40 ------------------------------ docs/migration/index.md | 4 +-- 5 files changed, 3 insertions(+), 79 deletions(-) delete mode 100644 docs/migration/freeze/gh-action.md delete mode 100644 docs/migration/freeze/index.md diff --git a/docs/_docset.yml b/docs/_docset.yml index 5833e223a..47df630a8 100644 --- a/docs/_docset.yml +++ b/docs/_docset.yml @@ -139,10 +139,6 @@ toc: - folder: migration children: - file: index.md - - folder: freeze - children: - - file: index.md - - file: gh-action.md - file: syntax.md - file: ia.md - file: versioning.md diff --git a/docs/cli/index.md b/docs/cli/index.md index 84db961d4..158c27d56 100644 --- a/docs/cli/index.md +++ b/docs/cli/index.md @@ -1,5 +1,5 @@ --- -navigation_title: docs-builder CLI +navigation_title: CLI (docs-builder) --- # Command line interface diff --git a/docs/migration/freeze/gh-action.md b/docs/migration/freeze/gh-action.md deleted file mode 100644 index 8c32e3f1e..000000000 --- a/docs/migration/freeze/gh-action.md +++ /dev/null @@ -1,32 +0,0 @@ -# GH Action - -## Overview - -The documentation team will use a GitHub Action to enforce the content freeze by adding comments to pull requests that modify `.asciidoc` files. It complements the use of `CODEOWNERS` to ensure changes during a freeze period are reviewed and approved by the `@docs-freeze-team`. - -## How It Works -1. **Trigger**: The Action is triggered on pull request events (`opened`, `reopened`, or `synchronize`). -2. **Check Changes**: It checks the diff between the latest commits to detect modifications to `.asciidoc` files. -3. **Add Comment**: If changes are detected, the Action posts a comment in the pull request, reminding the contributor of the freeze. - -```yaml -name: Comment on PR for .asciidoc changes - -on: - pull_request: - types: - - synchronize - - opened - - reopened - branches: - - main - - master - - "9.0" - -jobs: - comment-on-asciidoc-change: - permissions: - contents: read - pull-requests: write - uses: elastic/docs-builder/.github/workflows/comment-on-asciidoc-changes.yml@main -``` diff --git a/docs/migration/freeze/index.md b/docs/migration/freeze/index.md deleted file mode 100644 index b4d03a011..000000000 --- a/docs/migration/freeze/index.md +++ /dev/null @@ -1,40 +0,0 @@ -# Documentation Freeze - -:::{important} -During the documentation freeze, maintaining consistency and avoiding conflicts is key. To discourage documentation changes from merging during the documentation freeze, a [GH Action](./gh-action.md) is being added to all repositories with public-facing documentation. -::: - -During the documentation freeze, the Docs team will focus almost entirely on migration tasks to ensure all content is successfully migrated and will handle only emergency documentation requests and release-related activities. When the migration is complete, the docs team will assess any documentation requests that were submitted during the documentation freeze and ensure that they're still relevant in the new information architecture and format. - -:::{note} -The documentation freeze does not block you from merging asciidoc changes. However, you are strongly discouraged from merging changes to these files as any changes will not be carried forward in the migration and will be lost forever. -::: - -## Timeline - -* **29-Jan**: Merge all open Docs PRs by 12AM PST -* **30-Jan**: Documentation freeze begins for all public-facing Docs on main/master -* **20-Feb**: Documentation freeze ends -* **25-Mar**: 9.0.0 Docs + Elastic Docs v3 GA - -### Details - -:::{important} -We are freezing only the main/master public-facing Docs. You can continue to make changes to the 8.x Docs by creating PRs in the 8.x branches. -::: - -Before we migrate on 30-Jan, we will close all unmerged Docs PRs targeting main/master. When the freeze ends, you can open a new PR if the changes are still needed. - -We will not close PRs targeting main/master that also include code changes. After the freeze begins, merged PRs targeting main/master branches that include AsciiDoc changes will not be migrated. When the freeze ends, all 9.0.0 Docs changes will be made to the migrated Markdown Docs files. - -### During the freeze: - -* If you make Docs changes to the main/master branches, GitHub Actions will warn against merging the changes. -* You can make 8.x Docs changes by creating PRs in the relevant 8.x branches. -* For 9.0.0 Docs changes, [open an issue](https://github.com/elastic/docs-content/issues/new?template=internal-request.yaml) in [elastic/docs-content](https://github.com/elastic/docs-content/issues) and we’ll incorporate the changes post migration. -* The Docs Team will focus exclusively on migrating content, with the exception of the following: - * Stack-versioned release notes, including 8.18.0, 9.0.0-beta1, -rc1, and -rc2 - * Serverless changelog - * Cloud Hosted and ECE release notes - -For any questions and emergency Docs requests, post in the [#docs](https://elastic.slack.com/archives/C0JF80CJZ) Slack channel. diff --git a/docs/migration/index.md b/docs/migration/index.md index d9749743a..265b53ed5 100644 --- a/docs/migration/index.md +++ b/docs/migration/index.md @@ -1,11 +1,11 @@ --- -navigation_title: Migration +navigation_title: Migration from Asciidoc --- # Migration to docs-builder :::{important} -We are enforcing a [Documentation Freeze](./freeze/index.md) while we migrate docs between our two build systems. +All repositories have been migrated to `docs-builder` however older branches may still be using `asciidoc`. ::: Migrating to Elastic Docs V3 is more than just moving to a new documentation build system. This migration includes: From 1daf6c891012407773b3539d22cb44c52290ca7c Mon Sep 17 00:00:00 2001 From: Martijn Laarman Date: Tue, 30 Sep 2025 19:05:22 +0200 Subject: [PATCH 006/171] touchups --- docs/cli/index.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/cli/index.md b/docs/cli/index.md index 158c27d56..f9d9410a3 100644 --- a/docs/cli/index.md +++ b/docs/cli/index.md @@ -7,7 +7,7 @@ navigation_title: CLI (docs-builder) `docs-builder` is the binary used to invoke various commands. These commands can be roughly grouped into three main categories -- [Isolated commands](#isolated-commands) +- [Documentation Set commands](#documentation-set-commands) - [Link commands](#link-commands) - [Assembler commands](#assembler-commands) @@ -22,9 +22,9 @@ The following options are available for all commands: : Explicitly set the configuration source one of `local`, `remote` or `embedded`. Defaults to `local` if available other wise `embedded` -## Isolated Commands +## Documentation Set Commands -An isolated build means building a single documentation set. +Commands that operate over a single documentation set. A `Documentation Set` is defined as a folder containing a [docset.yml](../configure/content-set/index.md) file. @@ -42,6 +42,6 @@ Inbound links, those going from other sources to the documentation set, are vali ## Assembler Commands -Assembler builds bring together all isolated builds and turn them into the overall documentation that gets published. +Assembler builds bring together all isolated documentation set builds and turn them into the overall documentation that gets published. [See available CLI commands for assembler](assembler/index.md) From a030f8524c39c4ecc6141cb867feb4b0627c7328 Mon Sep 17 00:00:00 2001 From: Martijn Laarman Date: Tue, 30 Sep 2025 20:06:47 +0200 Subject: [PATCH 007/171] stage --- docs/_docset.yml | 3 +++ docs/cli/assembler/assembler-config-init.md | 2 +- docs/migration/index.md | 2 +- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/docs/_docset.yml b/docs/_docset.yml index 47df630a8..775bd804f 100644 --- a/docs/_docset.yml +++ b/docs/_docset.yml @@ -46,6 +46,9 @@ toc: - file: branching-strategy.md - file: add-repo.md - file: release-new-version.md + - folder: building-blocks + children: + - file: index.md - folder: configure children: - file: index.md diff --git a/docs/cli/assembler/assembler-config-init.md b/docs/cli/assembler/assembler-config-init.md index b113be0e4..cc23be77a 100644 --- a/docs/cli/assembler/assembler-config-init.md +++ b/docs/cli/assembler/assembler-config-init.md @@ -10,7 +10,7 @@ By default, the configuration is placed in a special application folder as its m * OSX: `~/Library/Application Support/docs-builder` [NSApplicationSupportDirectory](https://developer.apple.com/documentation/foundation/filemanager/searchpathdirectory/applicationsupportdirectory) * Linux: `~/.config/docs-builder` -* Windows: `%APPDATA%\docs-builder` +* {icon}`logo_windows` Windows: `%APPDATA%\docs-builder` You can also use the `--local` option to save the configuration locally in the current working directory. This exposes a great way to assemble the full documentation locally in an empty directory. diff --git a/docs/migration/index.md b/docs/migration/index.md index 265b53ed5..f385921da 100644 --- a/docs/migration/index.md +++ b/docs/migration/index.md @@ -1,5 +1,5 @@ --- -navigation_title: Migration from Asciidoc +navigation_title: Asciidoc Migration --- # Migration to docs-builder From 8ed7a3bc916d3f93c4c8ce1f92179e0f1cbc3d54 Mon Sep 17 00:00:00 2001 From: Martijn Laarman Date: Tue, 30 Sep 2025 20:15:53 +0200 Subject: [PATCH 008/171] stage --- docs/building-blocks/index.md | 69 +++++++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 docs/building-blocks/index.md diff --git a/docs/building-blocks/index.md b/docs/building-blocks/index.md new file mode 100644 index 000000000..b1be46bf1 --- /dev/null +++ b/docs/building-blocks/index.md @@ -0,0 +1,69 @@ +--- +navigation_title: Building Blocks +--- + +# Building Blocks + +This section explains all the building blocks that are used to build the documentation. + +## Documentation Set + +A single folder containing the documentation of a single repository. At a minimum, this folder should contain: + +* `docset.yml` file +* `index.md` file + +See [docset.yml](../configure/content-set/index.md) for more configuration details. + +## Assembled Documentation + +The product of building many documentation sets and weaving them in to a global navigation producing the fully assembled documentation site. + +See [site configuration](../configure/site/index.md) for more details on the actual configuration. + +## Distributed documentation + +The purpose of separating building documentation sets and assembling them is to allow for distributed builds. + +Each build of documentation set produces a `links.json` (Link Index) file that contains all the linkable resources in the repository. +This `links.json` file is then published to a central location (Link Service) everytime a repository successfully builds on its respective default integration branch. + +For example, [Elasticsearch's links.json](https://elastic-docs-link-index.s3.us-east-2.amazonaws.com/elastic/elasticsearch/main/links.json) represents all linkable resources in the Elasticsearch repository. + +This allows us to: + +* Validate outbound and inbound links ahead of time, even during local `docs-builder` builds. +* Snapshot assembled builds: only building commits that produced a `links.json` + * Documentation errors in one repository won't affect all the others. + * Resilient to repositories having build failures on their integration branches, we fall back to the last known good commit. + +## Link Service + +The central location where all the Link Index files are published. This is a simple S3 bucket fronted by CloudFront. + +## Link Index + +Each repository's branch will get in individual Link Index file in the `Link Serve` representing all linkable resources of that repository's branch. +See, for example: [Elasticsearch's links.json](https://elastic-docs-link-index.s3.us-east-2.amazonaws.com/elastic/elasticsearch/main/links.json). +This file is used to resolve [Outbound Crosslinks](#outbound-crosslinks) and [Inbound Crosslinks](#inbound-crosslinks). + +## Link Registry + +A [single file](https://elastic-docs-link-index.s3.us-east-2.amazonaws.com/link-index.json) that contains all the [Link Index files](#link-index) for all the repositories. +This is file is published everytime [Link Index](#link-index) files are published to the Link Service. The update is handled by an AWS Lambda function that listens to SQS triggers by S3 events. + +## Outbound Crosslinks + +Outbound crosslinks are links from the documentation set that's being built to others. If both repositories publish to the same `Link Service`, they can link to each other using the `://` syntax. + +Read more about general link syntax in the [](../syntax/links.md) section. + +## Inbound Crosslinks + +Inbound crosslinks are links from other documentation sets to the one that's being built. + +If both repositories publish to the same `Link Service`, they can link to each other using the `://` syntax. + +Read more about general link syntax in the [](../syntax/links.md) section. + +Using our [Link Service](#link-service), we can validate if deletions or renames of files in the documentation set break other repositories ahead of time. From 0bfe1344449fdb54083b0cd279a4bfe5d9f352df Mon Sep 17 00:00:00 2001 From: Martijn Laarman Date: Tue, 30 Sep 2025 20:28:48 +0200 Subject: [PATCH 009/171] First pass of claude over building blocks --- docs/_docset.yml | 8 ++ .../assembled-documentation.md | 50 +++++++ .../distributed-documentation.md | 63 +++++++++ docs/building-blocks/documentation-set.md | 49 +++++++ docs/building-blocks/inbound-crosslinks.md | 129 ++++++++++++++++++ docs/building-blocks/index.md | 69 ++++------ docs/building-blocks/link-index.md | 86 ++++++++++++ docs/building-blocks/link-registry.md | 78 +++++++++++ docs/building-blocks/link-service.md | 59 ++++++++ docs/building-blocks/outbound-crosslinks.md | 109 +++++++++++++++ 10 files changed, 658 insertions(+), 42 deletions(-) create mode 100644 docs/building-blocks/assembled-documentation.md create mode 100644 docs/building-blocks/distributed-documentation.md create mode 100644 docs/building-blocks/documentation-set.md create mode 100644 docs/building-blocks/inbound-crosslinks.md create mode 100644 docs/building-blocks/link-index.md create mode 100644 docs/building-blocks/link-registry.md create mode 100644 docs/building-blocks/link-service.md create mode 100644 docs/building-blocks/outbound-crosslinks.md diff --git a/docs/_docset.yml b/docs/_docset.yml index 775bd804f..2e20447fa 100644 --- a/docs/_docset.yml +++ b/docs/_docset.yml @@ -49,6 +49,14 @@ toc: - folder: building-blocks children: - file: index.md + - file: documentation-set.md + - file: assembled-documentation.md + - file: distributed-documentation.md + - file: link-service.md + - file: link-index.md + - file: link-registry.md + - file: outbound-crosslinks.md + - file: inbound-crosslinks.md - folder: configure children: - file: index.md diff --git a/docs/building-blocks/assembled-documentation.md b/docs/building-blocks/assembled-documentation.md new file mode 100644 index 000000000..8505653cf --- /dev/null +++ b/docs/building-blocks/assembled-documentation.md @@ -0,0 +1,50 @@ +--- +navigation_title: Assembled Documentation +--- + +# Assembled Documentation + +**Assembled documentation** is the product of building many [documentation sets](documentation-set.md) and weaving them into a global navigation to produce the fully assembled documentation site. + +## How it works + +The assembler: + +1. Clones multiple documentation repositories +2. Builds each documentation set independently +3. Combines them according to a global navigation configuration +4. Produces a unified documentation website + +## Benefits + +By assembling multiple documentation sets together, you can: + +* **Centralize navigation** - Present a unified experience across multiple repositories +* **Cross-link content** - Link between different documentation sets seamlessly +* **Version coordination** - Control which versions of different repositories appear together +* **Product-level organization** - Organize documentation by product rather than repository + +## Configuration + +Assembled documentation is configured through the site configuration, which defines: + +* Which repositories to include +* What versions of each repository to build +* How to organize the global navigation +* URL structure and routing + +See [Site Configuration](../configure/site/index.md) for complete details on configuring assembled documentation. + +## Build process + +The typical build process for assembled documentation: + +1. **Clone** - Clone all configured repositories using `docs-builder assembler clone` +2. **Build** - Build all documentation sets using `docs-builder assembler build` +3. **Export** - Export the assembled site in various formats (HTML, Elasticsearch index, etc.) + +## Related concepts + +* [Documentation Set](documentation-set.md) - The individual units being assembled +* [Distributed Documentation](distributed-documentation.md) - How documentation sets work independently +* [Link Registry](link-registry.md) - How the assembler knows what to include diff --git a/docs/building-blocks/distributed-documentation.md b/docs/building-blocks/distributed-documentation.md new file mode 100644 index 000000000..b0a65d4fc --- /dev/null +++ b/docs/building-blocks/distributed-documentation.md @@ -0,0 +1,63 @@ +--- +navigation_title: Distributed Documentation +--- + +# Distributed Documentation + +**Distributed documentation** is the architectural approach that allows documentation sets to be built independently while still enabling cross-repository linking and validation. + +## Purpose + +The separation between building individual documentation sets and assembling them enables distributed builds, where: + +* Each repository builds its own documentation independently +* Builds don't block each other +* Teams maintain full autonomy over their documentation +* Cross-repository links are validated without requiring synchronized builds + +## How it works + +### Link Index publication + +Each time a documentation set is built successfully on its default integration branch, it produces and publishes a `links.json` file ([Link Index](link-index.md)) to a central location ([Link Service](link-service.md)). + +This Link Index contains all the linkable resources in that repository at that specific commit. + +### Example + +For instance, [Elasticsearch's links.json](https://elastic-docs-link-index.s3.us-east-2.amazonaws.com/elastic/elasticsearch/main/links.json) represents all linkable resources in the Elasticsearch repository's main branch. + +## Benefits + +This distributed approach provides several key advantages: + +### Link validation + +* **Outbound links** - Validate links to other repositories ahead of time, even during local `docs-builder` builds +* **Inbound links** - Know when changes to your documentation would break links from other repositories + +### Resilient builds + +* **Isolation** - Documentation errors in one repository won't affect builds of other repositories +* **Fallback mechanism** - When a repository has build failures on its integration branch, the assembler falls back to the last known good commit +* **Snapshot builds** - Assembled builds only use commits that successfully produced a Link Index + +### Independent iteration + +* Teams can iterate on their documentation independently +* No coordination required for routine updates +* Faster feedback loops for documentation changes + +## Architecture components + +The distributed documentation system relies on several key components: + +* [Link Index](link-index.md) - Per-repository file of linkable resources +* [Link Service](link-service.md) - Central storage for Link Index files +* [Link Registry](link-registry.md) - Catalog of all available Link Index files +* [Outbound Crosslinks](outbound-crosslinks.md) - Links from one repository to another +* [Inbound Crosslinks](inbound-crosslinks.md) - Links from other repositories to yours + +## Local development + +Even during local development, `docs-builder` can access the Link Service to validate cross-repository links without requiring you to clone and build all related repositories. diff --git a/docs/building-blocks/documentation-set.md b/docs/building-blocks/documentation-set.md new file mode 100644 index 000000000..5ed4dc2a5 --- /dev/null +++ b/docs/building-blocks/documentation-set.md @@ -0,0 +1,49 @@ +--- +navigation_title: Documentation Set +--- + +# Documentation Set + +A **documentation set** is a single folder containing the documentation of a single repository. This is the fundamental unit of documentation in the docs-builder system. + +## Minimum requirements + +At a minimum, a documentation set folder must contain: + +* `docset.yml` - The configuration file that defines the structure and metadata of the documentation set +* `index.md` - The entry point or landing page for the documentation set + +## Purpose + +Documentation sets allow each repository to maintain its own documentation independently. Each set can be: + +* Built independently +* Versioned separately +* Maintained by different teams +* Published to its own schedule + +## Structure + +A typical documentation set might look like: + +``` +my-repo/ +└── docs/ + ├── docset.yml + ├── index.md + ├── getting-started.md + ├── configuration/ + │ ├── index.md + │ └── advanced.md + └── reference/ + └── api.md +``` + +## Configuration + +The `docset.yml` file controls how the documentation set is structured and built. See [Content Set Configuration](../configure/content-set/index.md) for complete configuration details. + +## Related concepts + +* [Assembled Documentation](assembled-documentation.md) - How multiple documentation sets are combined +* [Link Index](link-index.md) - How documentation sets publish their linkable resources diff --git a/docs/building-blocks/inbound-crosslinks.md b/docs/building-blocks/inbound-crosslinks.md new file mode 100644 index 000000000..9a7fe42ff --- /dev/null +++ b/docs/building-blocks/inbound-crosslinks.md @@ -0,0 +1,129 @@ +--- +navigation_title: Inbound Crosslinks +--- + +# Inbound Crosslinks + +**Inbound crosslinks** are links from other documentation sets to yours. Understanding and validating inbound crosslinks helps prevent breaking links in other repositories when you make changes. + +## Purpose + +Inbound crosslink validation allows you to: + +* **Detect breaking changes** - Know when renaming or deleting a file will break links from other repositories +* **Prevent regressions** - Avoid publishing changes that break documentation elsewhere +* **Coordinate changes** - Understand dependencies before making structural changes + +## How it works + +When you build your documentation, `docs-builder` can validate inbound crosslinks by: + +1. **Fetching your published Link Index** - Gets your repository's [Link Index](link-index.md) from the [Link Service](link-service.md) +2. **Comparing with local changes** - Compares your current local state with the published Link Index +3. **Detecting differences** - Identifies files that have been moved, renamed, or deleted +4. **Checking references** - Queries the Link Service to see if other repositories link to the changed files +5. **Reporting warnings** - Alerts you to potential breaking changes + +## Validation commands + +### Validate all inbound links + +Check all inbound crosslinks for your repository: + +```bash +docs-builder inbound-links validate-all +``` + +### Validate specific link reference + +Validate a locally built `links.json` against all published Link Index files: + +```bash +docs-builder inbound-links validate-link-reference --file .artifacts/docs/html/links.json +``` + +### Validate with filters + +Check inbound links from specific repositories or to specific resources: + +```bash +docs-builder inbound-links validate --from elasticsearch --to kibana +``` + +## Common scenarios + +### Moving a file + +If you move a file that other repositories link to: + +1. Create a redirect from the old path to the new path +2. Update your documentation's redirect configuration +3. Run inbound link validation to ensure the redirect works +4. Notify teams that maintain repositories with inbound links + +### Deleting a file + +Before deleting a file: + +1. Run inbound link validation to see if other repositories link to it +2. If there are inbound links, coordinate with those teams first +3. Consider leaving a redirect to related content +4. Update the other repositories to remove or update their links + +### Renaming headings + +Heading anchors are part of the Link Index. If other repositories link to specific headings in your documentation: + +1. Validate inbound links before renaming +2. Consider keeping old heading anchors if heavily linked +3. Document the change if coordination is needed + +## Integration with CI/CD + +You can integrate inbound link validation into your CI/CD pipeline: + +```yaml +- name: Validate inbound links + run: | + docs-builder inbound-links validate-link-reference \ + --file .artifacts/docs/html/links.json +``` + +This will fail the build if you're about to break links from other repositories. + +## Best practices + +### Set up redirects + +When moving or renaming files, always set up redirects: + +```yaml +# In your documentation's configuration +redirects: + - from: /old-path/file.html + to: /new-path/file.html +``` + +### Communicate changes + +If you need to make a breaking change: + +1. Run inbound link validation to identify affected repositories +2. File issues or notify maintainers of affected repositories +3. Coordinate the change timing +4. Provide redirect mappings or alternative URLs + +### Validate before merging + +Make inbound link validation part of your review process: + +* Run validation locally before creating a PR +* Include validation in CI checks +* Review validation results before merging + +## Related concepts + +* [Outbound Crosslinks](outbound-crosslinks.md) - Links from your documentation to others +* [Link Index](link-index.md) - How your linkable resources are tracked +* [Link Service](link-service.md) - Where inbound link information is stored +* [Distributed Documentation](distributed-documentation.md) - The architecture enabling this validation diff --git a/docs/building-blocks/index.md b/docs/building-blocks/index.md index b1be46bf1..2591e3aa6 100644 --- a/docs/building-blocks/index.md +++ b/docs/building-blocks/index.md @@ -4,66 +4,51 @@ navigation_title: Building Blocks # Building Blocks -This section explains all the building blocks that are used to build the documentation. +This section explains the core concepts and building blocks that make up the docs-builder architecture. Understanding these concepts will help you work effectively with distributed documentation and cross-repository linking. -## Documentation Set +## Core concepts -A single folder containing the documentation of a single repository. At a minimum, this folder should contain: +### [Documentation Set](documentation-set.md) -* `docset.yml` file -* `index.md` file +The fundamental unit of documentation - a single folder containing the documentation for one repository. Each documentation set can be built, versioned, and maintained independently. -See [docset.yml](../configure/content-set/index.md) for more configuration details. +### [Assembled Documentation](assembled-documentation.md) -## Assembled Documentation +The product of combining multiple documentation sets into a unified documentation website with global navigation. This enables a seamless user experience across multiple repositories. -The product of building many documentation sets and weaving them in to a global navigation producing the fully assembled documentation site. +### [Distributed Documentation](distributed-documentation.md) -See [site configuration](../configure/site/index.md) for more details on the actual configuration. +The architectural approach that allows documentation sets to be built independently while maintaining link integrity across repositories. This enables teams to work autonomously without blocking each other. -## Distributed documentation +## Link management infrastructure -The purpose of separating building documentation sets and assembling them is to allow for distributed builds. +### [Link Service](link-service.md) -Each build of documentation set produces a `links.json` (Link Index) file that contains all the linkable resources in the repository. -This `links.json` file is then published to a central location (Link Service) everytime a repository successfully builds on its respective default integration branch. +The central S3-backed service where Link Index files are published and stored. This enables distributed validation and build resilience. -For example, [Elasticsearch's links.json](https://elastic-docs-link-index.s3.us-east-2.amazonaws.com/elastic/elasticsearch/main/links.json) represents all linkable resources in the Elasticsearch repository. +### [Link Index](link-index.md) -This allows us to: +A JSON file (`links.json`) containing all linkable resources for a specific repository branch. Published to the Link Service after each successful build. -* Validate outbound and inbound links ahead of time, even during local `docs-builder` builds. -* Snapshot assembled builds: only building commits that produced a `links.json` - * Documentation errors in one repository won't affect all the others. - * Resilient to repositories having build failures on their integration branches, we fall back to the last known good commit. +### [Link Registry](link-registry.md) -## Link Service +A catalog file listing all available Link Index files across all repositories and branches. Used by the assembler to coordinate builds and by documentation builds for validation. -The central location where all the Link Index files are published. This is a simple S3 bucket fronted by CloudFront. +## Cross-repository linking -## Link Index +### [Outbound Crosslinks](outbound-crosslinks.md) -Each repository's branch will get in individual Link Index file in the `Link Serve` representing all linkable resources of that repository's branch. -See, for example: [Elasticsearch's links.json](https://elastic-docs-link-index.s3.us-east-2.amazonaws.com/elastic/elasticsearch/main/links.json). -This file is used to resolve [Outbound Crosslinks](#outbound-crosslinks) and [Inbound Crosslinks](#inbound-crosslinks). +Links from your documentation to other documentation sets. Validated against published Link Index files to ensure they're correct. -## Link Registry +### [Inbound Crosslinks](inbound-crosslinks.md) -A [single file](https://elastic-docs-link-index.s3.us-east-2.amazonaws.com/link-index.json) that contains all the [Link Index files](#link-index) for all the repositories. -This is file is published everytime [Link Index](#link-index) files are published to the Link Service. The update is handled by an AWS Lambda function that listens to SQS triggers by S3 events. +Links from other documentation sets to yours. Validated to prevent breaking changes when you move or delete content. -## Outbound Crosslinks +## How it all works together -Outbound crosslinks are links from the documentation set that's being built to others. If both repositories publish to the same `Link Service`, they can link to each other using the `://` syntax. - -Read more about general link syntax in the [](../syntax/links.md) section. - -## Inbound Crosslinks - -Inbound crosslinks are links from other documentation sets to the one that's being built. - -If both repositories publish to the same `Link Service`, they can link to each other using the `://` syntax. - -Read more about general link syntax in the [](../syntax/links.md) section. - -Using our [Link Service](#link-service), we can validate if deletions or renames of files in the documentation set break other repositories ahead of time. +1. Each repository builds its documentation set independently +2. Successful builds publish a Link Index to the Link Service +3. The Link Registry catalogs all available Link Index files +4. Documentation builds validate crosslinks using these Link Index files +5. The assembler combines documentation sets using the Link Registry +6. Teams can work independently while maintaining link integrity across repositories diff --git a/docs/building-blocks/link-index.md b/docs/building-blocks/link-index.md new file mode 100644 index 000000000..51de5bc1e --- /dev/null +++ b/docs/building-blocks/link-index.md @@ -0,0 +1,86 @@ +--- +navigation_title: Link Index +--- + +# Link Index + +A **Link Index** is a JSON file (`links.json`) that contains all the linkable resources for a specific repository branch. + +## Purpose + +The Link Index enables: + +* **Cross-repository linking** - Other documentation sets can link to your content +* **Link validation** - Validate that links to your content are correct +* **Inbound link detection** - Know when other repositories link to your content +* **Distributed builds** - Build documentation independently while maintaining link integrity + +## Structure + +Each repository branch gets its own Link Index file in the [Link Service](link-service.md), organized by: + +* **Organization** - e.g., `elastic` +* **Repository** - e.g., `elasticsearch` +* **Branch** - e.g., `main`, `8.x`, `7.17` + +## Example + +View [Elasticsearch's main branch Link Index](https://elastic-docs-link-index.s3.us-east-2.amazonaws.com/elastic/elasticsearch/main/links.json) to see a real example. + +The Link Index contains: + +* All documentation pages in the repository +* Headings within those pages +* Anchors and linkable elements +* Version information +* Metadata about the build + +## Generation + +The Link Index is automatically generated during a documentation build: + +1. `docs-builder` builds the documentation set +2. During the build, all linkable resources are tracked +3. After a successful build, a `links.json` file is written to `.artifacts/docs/html/links.json` +4. CI/CD publishes this file to the [Link Service](link-service.md) + +## Usage + +### Resolving outbound crosslinks + +When you use a crosslink like `elasticsearch://reference/api/search.md`, `docs-builder`: + +1. Fetches the Elasticsearch Link Index from the Link Service +2. Looks up the path in the index +3. Validates the link exists +4. Resolves it to the correct URL + +### Validating inbound crosslinks + +When building your documentation, `docs-builder` can: + +1. Fetch your repository's Link Index from previous builds +2. Compare with your current local changes +3. Detect if you've moved or deleted files that other repositories link to +4. Warn about breaking changes + +## File location + +During a build, the Link Index is written to: + +``` +.artifacts/docs/html/links.json +``` + +After publishing, it's available at: + +``` +https://elastic-docs-link-index.s3.us-east-2.amazonaws.com/{org}/{repo}/{branch}/links.json +``` + +## Related concepts + +* [Link Service](link-service.md) - Where Link Index files are stored +* [Link Registry](link-registry.md) - Catalog of all Link Index files +* [Outbound Crosslinks](outbound-crosslinks.md) - Links that use the Link Index +* [Inbound Crosslinks](inbound-crosslinks.md) - Links to resources in the Link Index diff --git a/docs/building-blocks/link-registry.md b/docs/building-blocks/link-registry.md new file mode 100644 index 000000000..4461fe328 --- /dev/null +++ b/docs/building-blocks/link-registry.md @@ -0,0 +1,78 @@ +--- +navigation_title: Link Registry +--- + +# Link Registry + +The **Link Registry** is a single JSON file that serves as a catalog of all available [Link Index](link-index.md) files across all repositories. + +## Purpose + +The Link Registry provides: + +* **Discovery** - A single file to query for all available documentation across all repositories and branches +* **Efficiency** - Avoid scanning the entire [Link Service](link-service.md) to find available Link Index files +* **Assembler coordination** - The assembler uses this to determine which repositories and versions are available to build + +## Location + +The Link Registry is available at: + +``` +https://elastic-docs-link-index.s3.us-east-2.amazonaws.com/link-index.json +``` + +## Structure + +The Link Registry contains: + +* List of all organizations (e.g., `elastic`) +* Repositories within each organization (e.g., `elasticsearch`, `kibana`) +* Branches for each repository (e.g., `main`, `8.x`, `7.17`) +* Metadata about each Link Index: + * Last updated timestamp + * Commit SHA that produced the Link Index + * URL to the Link Index file + +## Maintenance + +The Link Registry is automatically maintained: + +1. A repository's CI/CD pipeline publishes a new `links.json` to the Link Service +2. The S3 bucket triggers an SQS message +3. An AWS Lambda function listens to these SQS messages +4. The Lambda function updates the Link Registry to include or update the entry for the new Link Index + +This process ensures the registry stays in sync with published Link Index files without manual intervention. + +## Usage + +### By the assembler + +When running `docs-builder assembler clone` or `docs-builder assembler build`: + +1. The assembler fetches the Link Registry +2. It determines which repositories and versions to clone/build based on the site configuration +3. It uses the commit SHAs from the registry to clone specific versions +4. It falls back to the last known good commit if a repository's current state has build failures + +### By documentation builds + +During a documentation build: + +1. `docs-builder` fetches the Link Registry +2. It determines which Link Index files to download for cross-repository validation +3. It validates all crosslinks against the appropriate Link Index files + +## Benefits + +* **Single source of truth** - One file to check for all available documentation +* **Performance** - Fast lookup without scanning the entire Link Service +* **Automation** - Maintained automatically via Lambda functions +* **Resilience** - Represents only successful builds with valid Link Indexes + +## Related concepts + +* [Link Service](link-service.md) - Where the Link Registry is stored +* [Link Index](link-index.md) - The files cataloged by the Link Registry +* [Assembled Documentation](assembled-documentation.md) - Uses the Link Registry to coordinate builds diff --git a/docs/building-blocks/link-service.md b/docs/building-blocks/link-service.md new file mode 100644 index 000000000..fbb7800ed --- /dev/null +++ b/docs/building-blocks/link-service.md @@ -0,0 +1,59 @@ +--- +navigation_title: Link Service +--- + +# Link Service + +The **Link Service** is the central location where all [Link Index](link-index.md) files are published and stored. + +## Architecture + +The Link Service is implemented as: + +* **Storage** - An S3 bucket containing all Link Index files +* **CDN** - CloudFront fronting the S3 bucket for fast global access +* **Access** - Publicly accessible for read operations + +## Purpose + +The Link Service enables: + +* **Distributed validation** - Any documentation build can validate cross-repository links without cloning all repositories +* **Link discovery** - Find what resources are available in other repositories +* **Build resilience** - Assembler builds can reference the last known good state of each repository +* **Decentralized publishing** - Each repository publishes its own Link Index independently + +## URL structure + +Link Index files are organized by repository and branch: + +``` +https://elastic-docs-link-index.s3.us-east-2.amazonaws.com/{org}/{repo}/{branch}/links.json +``` + +For example: +* [Elasticsearch main branch](https://elastic-docs-link-index.s3.us-east-2.amazonaws.com/elastic/elasticsearch/main/links.json) +* [Kibana main branch](https://elastic-docs-link-index.s3.us-east-2.amazonaws.com/elastic/kibana/main/links.json) + +## Publishing process + +When a documentation build completes successfully on a default integration branch: + +1. The build generates a `links.json` file +2. The CI/CD pipeline publishes the file to the Link Service +3. An AWS Lambda function triggers on the S3 event +4. The Lambda updates the [Link Registry](link-registry.md) to include the new Link Index + +## Access during builds + +During both local and CI builds, `docs-builder`: + +* Fetches relevant Link Index files from the Link Service +* Validates outbound crosslinks against these indexes +* Validates that inbound crosslinks won't be broken by local changes + +## Related concepts + +* [Link Index](link-index.md) - The files stored in the Link Service +* [Link Registry](link-registry.md) - The catalog of all Link Index files +* [Distributed Documentation](distributed-documentation.md) - Why the Link Service exists diff --git a/docs/building-blocks/outbound-crosslinks.md b/docs/building-blocks/outbound-crosslinks.md new file mode 100644 index 000000000..8d65f2579 --- /dev/null +++ b/docs/building-blocks/outbound-crosslinks.md @@ -0,0 +1,109 @@ +--- +navigation_title: Outbound Crosslinks +--- + +# Outbound Crosslinks + +**Outbound crosslinks** are links from your documentation set to other documentation sets in different repositories. + +## Purpose + +Outbound crosslinks allow you to: + +* Link to documentation in other repositories +* Maintain those links even as the target repository evolves +* Validate links during local builds +* Get warnings if target content is moved or deleted + +## Syntax + +If both repositories publish to the same [Link Service](link-service.md), they can link to each other using the crosslink syntax: + +```markdown +[Link text](repository-name://path/to/file.md) +``` + +For example: + +```markdown +See the [Search API documentation](elasticsearch://reference/api/search.md) +``` + +## How it works + +When `docs-builder` encounters a crosslink: + +1. **Parse** - Extracts the repository name and path from the link +2. **Fetch** - Downloads the target repository's [Link Index](link-index.md) from the Link Service +3. **Resolve** - Looks up the path in the Link Index to get the actual URL +4. **Validate** - Verifies the link exists and generates a warning if not +5. **Transform** - Replaces the crosslink with the resolved URL in the output + +## Validation + +During a build, `docs-builder`: + +* **Validates immediately** - Checks all outbound crosslinks against published Link Index files +* **Reports errors** - Warns about broken links before you publish +* **Suggests fixes** - If a file was moved, the Link Index may include redirect information + +### Local validation + +Even during local development, you can validate outbound crosslinks: + +```bash +docs-builder --path ./docs +``` + +This will: +* Fetch Link Index files from the Link Service +* Validate all crosslinks in your local documentation +* Report any broken links + +## Configuration + +To enable crosslinks to a repository, add it to your `docset.yml`: + +```yaml +cross_links: + - elasticsearch + - kibana + - fleet +``` + +## Best practices + +### Link to files, not URLs + +**Good:** +```markdown +[Search API](elasticsearch://reference/api/search.md) +``` + +**Bad:** +```markdown +[Search API](https://www.elastic.co/guide/en/elasticsearch/reference/current/search.html) +``` + +The crosslink syntax is resilient to: +* URL structure changes +* File moves (if redirects are configured) +* Version differences + +### Link to headings + +You can link to specific headings within a page: + +```markdown +[Query DSL](elasticsearch://reference/query-dsl.md#match-query) +``` + +### Specify versions + +For assembled documentation, the assembler handles version mapping. For local builds, crosslinks resolve to the default branch of the target repository. + +## Related concepts + +* [Inbound Crosslinks](inbound-crosslinks.md) - Links from other repositories to yours +* [Link Index](link-index.md) - How crosslinks are resolved +* [Links syntax](../syntax/links.md) - Complete link syntax documentation From 7887ee2d2840418c2b4de263fb9376d11873550c Mon Sep 17 00:00:00 2001 From: Martijn Laarman Date: Wed, 1 Oct 2025 10:50:10 +0200 Subject: [PATCH 010/171] Expanded building blocks --- docs/_docset.yml | 2 +- .../assembled-documentation.md | 34 +++++++------- .../distributed-documentation.md | 6 +-- docs/building-blocks/inbound-crosslinks.md | 25 ++++------ docs/building-blocks/index.md | 6 +-- .../{link-registry.md => link-catalog.md} | 30 ++++++------ docs/building-blocks/link-index.md | 2 +- docs/building-blocks/link-service.md | 4 +- docs/building-blocks/outbound-crosslinks.md | 46 +++++++++---------- 9 files changed, 71 insertions(+), 84 deletions(-) rename docs/building-blocks/{link-registry.md => link-catalog.md} (70%) diff --git a/docs/_docset.yml b/docs/_docset.yml index 2e20447fa..436f5dade 100644 --- a/docs/_docset.yml +++ b/docs/_docset.yml @@ -54,7 +54,7 @@ toc: - file: distributed-documentation.md - file: link-service.md - file: link-index.md - - file: link-registry.md + - file: link-catalog.md - file: outbound-crosslinks.md - file: inbound-crosslinks.md - folder: configure diff --git a/docs/building-blocks/assembled-documentation.md b/docs/building-blocks/assembled-documentation.md index 8505653cf..33020aaa1 100644 --- a/docs/building-blocks/assembled-documentation.md +++ b/docs/building-blocks/assembled-documentation.md @@ -11,40 +11,40 @@ navigation_title: Assembled Documentation The assembler: 1. Clones multiple documentation repositories -2. Builds each documentation set independently -3. Combines them according to a global navigation configuration +2. Reads its [configuration files](../configure/site/index.md) and builds [a global navigation](../configure/site/navigation.md) +3. Builds each documentation set independently using the global configuration and navigation to inform path prefixes 4. Produces a unified documentation website -## Benefits - -By assembling multiple documentation sets together, you can: - -* **Centralize navigation** - Present a unified experience across multiple repositories -* **Cross-link content** - Link between different documentation sets seamlessly -* **Version coordination** - Control which versions of different repositories appear together -* **Product-level organization** - Organize documentation by product rather than repository - ## Configuration Assembled documentation is configured through the site configuration, which defines: -* Which repositories to include -* What versions of each repository to build -* How to organize the global navigation -* URL structure and routing +* [assembler.yml](../configure/site/index.md) Which repositories to include and [their branching strategy](../contribute/branching-strategy.md) +* [navigation.yml](../configure/site/index.md) Navigation and url prefixes for TOC's. +* [versions.yml](../configure/site/versions.md) Defines the various versioning schemes of products/solutions being documented +* [products.yml](../configure/site/products.md) Defines the product catalog (id, name) and ties it to a specific versioning scheme See [Site Configuration](../configure/site/index.md) for complete details on configuring assembled documentation. +Important to note that `docs-builder` makes no assumptions about how repositories, products, solutions and versions tie into each other. + ## Build process The typical build process for assembled documentation: 1. **Clone** - Clone all configured repositories using `docs-builder assembler clone` 2. **Build** - Build all documentation sets using `docs-builder assembler build` -3. **Export** - Export the assembled site in various formats (HTML, Elasticsearch index, etc.) +3. **Serve** - Serve the documentation on http://localhost:4000 using `docs-builder assembler serve` + +This uses the embedded configuration files inside the `docs-builder` binary. To build a specific configuration: + +1. **Init Config** - Fetch the latest config to `$(pwd)/config` `docs-builder assembler config init --local` +2. **Clone** - Clone all configured repositories using `docs-builder assembler clone --local` +3. **Build** - Build all documentation sets using `docs-builder assembler build --local` +4. **Serve** - Serve the documentation on http://localhost:4000 using `docs-builder assembler serve` ## Related concepts * [Documentation Set](documentation-set.md) - The individual units being assembled * [Distributed Documentation](distributed-documentation.md) - How documentation sets work independently -* [Link Registry](link-registry.md) - How the assembler knows what to include +* [Link Catalog](link-catalog.md) - How the assembler knows what to include diff --git a/docs/building-blocks/distributed-documentation.md b/docs/building-blocks/distributed-documentation.md index b0a65d4fc..8258e004f 100644 --- a/docs/building-blocks/distributed-documentation.md +++ b/docs/building-blocks/distributed-documentation.md @@ -4,7 +4,7 @@ navigation_title: Distributed Documentation # Distributed Documentation -**Distributed documentation** is the architectural approach that allows documentation sets to be built independently while still enabling cross-repository linking and validation. +**Distributed documentation** is the architectural approach that allows repositories to build their own documentation independently. ## Purpose @@ -54,10 +54,10 @@ The distributed documentation system relies on several key components: * [Link Index](link-index.md) - Per-repository file of linkable resources * [Link Service](link-service.md) - Central storage for Link Index files -* [Link Registry](link-registry.md) - Catalog of all available Link Index files +* [Link Catalog](link-catalog.md) - Catalog of all available Link Index files * [Outbound Crosslinks](outbound-crosslinks.md) - Links from one repository to another * [Inbound Crosslinks](inbound-crosslinks.md) - Links from other repositories to yours ## Local development -Even during local development, `docs-builder` can access the Link Service to validate cross-repository links without requiring you to clone and build all related repositories. +Even during local development, `docs-builder` can access the [Link Service](link-service.md) to validate cross-repository links without requiring you to clone and build all related repositories. diff --git a/docs/building-blocks/inbound-crosslinks.md b/docs/building-blocks/inbound-crosslinks.md index 9a7fe42ff..e661c7f65 100644 --- a/docs/building-blocks/inbound-crosslinks.md +++ b/docs/building-blocks/inbound-crosslinks.md @@ -16,19 +16,18 @@ Inbound crosslink validation allows you to: ## How it works -When you build your documentation, `docs-builder` can validate inbound crosslinks by: +A regular [build](../cli/docset/build.md) of a documentation set won't validate inbound links automatically. -1. **Fetching your published Link Index** - Gets your repository's [Link Index](link-index.md) from the [Link Service](link-service.md) -2. **Comparing with local changes** - Compares your current local state with the published Link Index -3. **Detecting differences** - Identifies files that have been moved, renamed, or deleted -4. **Checking references** - Queries the Link Service to see if other repositories link to the changed files -5. **Reporting warnings** - Alerts you to potential breaking changes +You have to use the [inbound-links validate-link-reference](../cli/links/inbound-links-validate-link-reference.md) after a build to validate all inbound links. + +The reason for this is that validating all inbound links has to download all published [Link Index](link-index.md) files +for the current [Content Source](../configure/content-sources.md). ## Validation commands ### Validate all inbound links -Check all inbound crosslinks for your repository: +Check all inbound links for all published [Link Index](link-index.md) files declared in the [Link Catalog](link-catalog.md) ```bash docs-builder inbound-links validate-all @@ -39,6 +38,7 @@ docs-builder inbound-links validate-all Validate a locally built `links.json` against all published Link Index files: ```bash +docs-builder inbound-links validate-link-reference docs-builder inbound-links validate-link-reference --file .artifacts/docs/html/links.json ``` @@ -80,16 +80,7 @@ Heading anchors are part of the Link Index. If other repositories link to specif ## Integration with CI/CD -You can integrate inbound link validation into your CI/CD pipeline: - -```yaml -- name: Validate inbound links - run: | - docs-builder inbound-links validate-link-reference \ - --file .artifacts/docs/html/links.json -``` - -This will fail the build if you're about to break links from other repositories. +Our preview CI job will run inbound link validation automatically. ## Best practices diff --git a/docs/building-blocks/index.md b/docs/building-blocks/index.md index 2591e3aa6..49e6cab38 100644 --- a/docs/building-blocks/index.md +++ b/docs/building-blocks/index.md @@ -30,7 +30,7 @@ The central S3-backed service where Link Index files are published and stored. T A JSON file (`links.json`) containing all linkable resources for a specific repository branch. Published to the Link Service after each successful build. -### [Link Registry](link-registry.md) +### [Link Catalog](link-catalog.md) A catalog file listing all available Link Index files across all repositories and branches. Used by the assembler to coordinate builds and by documentation builds for validation. @@ -48,7 +48,7 @@ Links from other documentation sets to yours. Validated to prevent breaking chan 1. Each repository builds its documentation set independently 2. Successful builds publish a Link Index to the Link Service -3. The Link Registry catalogs all available Link Index files +3. The Link Catalog catalogs all available Link Index files 4. Documentation builds validate crosslinks using these Link Index files -5. The assembler combines documentation sets using the Link Registry +5. The assembler combines documentation sets using the Link Catalog 6. Teams can work independently while maintaining link integrity across repositories diff --git a/docs/building-blocks/link-registry.md b/docs/building-blocks/link-catalog.md similarity index 70% rename from docs/building-blocks/link-registry.md rename to docs/building-blocks/link-catalog.md index 4461fe328..866a5bbb6 100644 --- a/docs/building-blocks/link-registry.md +++ b/docs/building-blocks/link-catalog.md @@ -1,14 +1,14 @@ --- -navigation_title: Link Registry +navigation_title: Link Catalog --- -# Link Registry +# Link Catalog -The **Link Registry** is a single JSON file that serves as a catalog of all available [Link Index](link-index.md) files across all repositories. +The **Link Catalog** is a single JSON file that serves as a catalog of all available [Link Index](link-index.md) files across all repositories. ## Purpose -The Link Registry provides: +The Link Catalog provides: * **Discovery** - A single file to query for all available documentation across all repositories and branches * **Efficiency** - Avoid scanning the entire [Link Service](link-service.md) to find available Link Index files @@ -16,7 +16,7 @@ The Link Registry provides: ## Location -The Link Registry is available at: +The Link Catalog is available at: ``` https://elastic-docs-link-index.s3.us-east-2.amazonaws.com/link-index.json @@ -24,7 +24,7 @@ https://elastic-docs-link-index.s3.us-east-2.amazonaws.com/link-index.json ## Structure -The Link Registry contains: +The Link Catalog contains: * List of all organizations (e.g., `elastic`) * Repositories within each organization (e.g., `elasticsearch`, `kibana`) @@ -36,14 +36,14 @@ The Link Registry contains: ## Maintenance -The Link Registry is automatically maintained: +The Link Catalog is automatically maintained: 1. A repository's CI/CD pipeline publishes a new `links.json` to the Link Service 2. The S3 bucket triggers an SQS message 3. An AWS Lambda function listens to these SQS messages -4. The Lambda function updates the Link Registry to include or update the entry for the new Link Index +4. The Lambda function updates the Link Catalog to include or update the entry for the new Link Index -This process ensures the registry stays in sync with published Link Index files without manual intervention. +This process ensures the catalog stays in sync with published Link Index files without manual intervention. ## Usage @@ -51,16 +51,16 @@ This process ensures the registry stays in sync with published Link Index files When running `docs-builder assembler clone` or `docs-builder assembler build`: -1. The assembler fetches the Link Registry +1. The assembler fetches the Link Catalog 2. It determines which repositories and versions to clone/build based on the site configuration -3. It uses the commit SHAs from the registry to clone specific versions +3. It uses the commit SHAs from the catalog to clone specific versions 4. It falls back to the last known good commit if a repository's current state has build failures ### By documentation builds During a documentation build: -1. `docs-builder` fetches the Link Registry +1. `docs-builder` fetches the Link Catalog 2. It determines which Link Index files to download for cross-repository validation 3. It validates all crosslinks against the appropriate Link Index files @@ -73,6 +73,6 @@ During a documentation build: ## Related concepts -* [Link Service](link-service.md) - Where the Link Registry is stored -* [Link Index](link-index.md) - The files cataloged by the Link Registry -* [Assembled Documentation](assembled-documentation.md) - Uses the Link Registry to coordinate builds +* [Link Service](link-service.md) - Where the Link Catalog is stored +* [Link Index](link-index.md) - The files cataloged by the Link Catalog +* [Assembled Documentation](assembled-documentation.md) - Uses the Link Catalog to coordinate builds diff --git a/docs/building-blocks/link-index.md b/docs/building-blocks/link-index.md index 51de5bc1e..b12bad7da 100644 --- a/docs/building-blocks/link-index.md +++ b/docs/building-blocks/link-index.md @@ -81,6 +81,6 @@ https://elastic-docs-link-index.s3.us-east-2.amazonaws.com/{org}/{repo}/{branch} ## Related concepts * [Link Service](link-service.md) - Where Link Index files are stored -* [Link Registry](link-registry.md) - Catalog of all Link Index files +* [Link Catalog](link-catalog.md) - Catalog of all Link Index files * [Outbound Crosslinks](outbound-crosslinks.md) - Links that use the Link Index * [Inbound Crosslinks](inbound-crosslinks.md) - Links to resources in the Link Index diff --git a/docs/building-blocks/link-service.md b/docs/building-blocks/link-service.md index fbb7800ed..b3129cc43 100644 --- a/docs/building-blocks/link-service.md +++ b/docs/building-blocks/link-service.md @@ -42,7 +42,7 @@ When a documentation build completes successfully on a default integration branc 1. The build generates a `links.json` file 2. The CI/CD pipeline publishes the file to the Link Service 3. An AWS Lambda function triggers on the S3 event -4. The Lambda updates the [Link Registry](link-registry.md) to include the new Link Index +4. The Lambda updates the [Link Catalog](link-catalog.md) to include the new Link Index ## Access during builds @@ -55,5 +55,5 @@ During both local and CI builds, `docs-builder`: ## Related concepts * [Link Index](link-index.md) - The files stored in the Link Service -* [Link Registry](link-registry.md) - The catalog of all Link Index files +* [Link Catalog](link-catalog.md) - The catalog of all Link Index files * [Distributed Documentation](distributed-documentation.md) - Why the Link Service exists diff --git a/docs/building-blocks/outbound-crosslinks.md b/docs/building-blocks/outbound-crosslinks.md index 8d65f2579..0fb913880 100644 --- a/docs/building-blocks/outbound-crosslinks.md +++ b/docs/building-blocks/outbound-crosslinks.md @@ -31,38 +31,31 @@ See the [Search API documentation](elasticsearch://reference/api/search.md) ## How it works +You have to explicitly opt in to another repository's `Link Index` by adding it to your `docset.yml` file: + +```yaml +cross_links: + - docs-content +``` + + When `docs-builder` encounters a crosslink: 1. **Parse** - Extracts the repository name and path from the link -2. **Fetch** - Downloads the target repository's [Link Index](link-index.md) from the Link Service -3. **Resolve** - Looks up the path in the Link Index to get the actual URL -4. **Validate** - Verifies the link exists and generates a warning if not -5. **Transform** - Replaces the crosslink with the resolved URL in the output +3. **Resolve** - Looks up the path in the locally cached [Link Index](link-index.md) to get the actual URL +4. **Validate** - Verifies the link exists and generates an error if not +5. **Transform** - Replaces the crosslink with the fully resolved URL in the output ## Validation During a build, `docs-builder`: -* **Validates immediately** - Checks all outbound crosslinks against published Link Index files -* **Reports errors** - Warns about broken links before you publish -* **Suggests fixes** - If a file was moved, the Link Index may include redirect information - -### Local validation - -Even during local development, you can validate outbound crosslinks: - -```bash -docs-builder --path ./docs -``` - -This will: -* Fetch Link Index files from the Link Service -* Validate all crosslinks in your local documentation -* Report any broken links +* **Validates immediately** - Checks all outbound cross-links against locally fetched [Link Index](link-index.md) files +* **Reports errors** - Reports errors about broken links before you publish ## Configuration -To enable crosslinks to a repository, add it to your `docset.yml`: +To enable cross-links to a repository, add it to your `docset.yml`: ```yaml cross_links: @@ -71,6 +64,13 @@ cross_links: - fleet ``` +This instructs `docs-builder` to fetch the `Link Index` from the [Link Service](link-service.md) during the build process which are then cached locally. +`docs-builder` will validate locally cached `Link Index` files against the remote `Link Index` files on each build fetching updates as needed. + +Now you can create crosslinks e.g `elasticsearch://path/to/file.md` + +The explicit opt-in prevents each repository build having the fetch all the links for all the repositories in the [`Link Catalog`](link-catalog.md) of which there may be many. + ## Best practices ### Link to files, not URLs @@ -98,10 +98,6 @@ You can link to specific headings within a page: [Query DSL](elasticsearch://reference/query-dsl.md#match-query) ``` -### Specify versions - -For assembled documentation, the assembler handles version mapping. For local builds, crosslinks resolve to the default branch of the target repository. - ## Related concepts * [Inbound Crosslinks](inbound-crosslinks.md) - Links from other repositories to yours From 6d7f430450c25dedf852a6a5f45ddbae189d0ebd Mon Sep 17 00:00:00 2001 From: Martijn Laarman Date: Wed, 1 Oct 2025 12:08:23 +0200 Subject: [PATCH 011/171] update building blocks --- docs/building-blocks/index.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/docs/building-blocks/index.md b/docs/building-blocks/index.md index 49e6cab38..a4bdaab89 100644 --- a/docs/building-blocks/index.md +++ b/docs/building-blocks/index.md @@ -44,6 +44,25 @@ Links from your documentation to other documentation sets. Validated against pub Links from other documentation sets to yours. Validated to prevent breaking changes when you move or delete content. +## Navigation + +### Documentation Set Navigation + +A documentation set is responsible for defining how files are organized in the navigation. This is done by defining a `toc` section in the `docset.yml` file. +If the `toc` section becomes to big folders can define a dedicated `toc.yml` file to organize the files and link them in their parent `toc.yml` or `docset.yml` file. + +Read more details in the reference for [docset.yml](../configure/content-set/index.md) + +### Global Navigation + +The global navigation is defined in the [`navigation.yml`](../configure/site/navigation.md) file. +This navigation only concerns itself with `toc` sections defined in either `docset.yml` or `toc.yml` files. +These `toc` sections can be reorganized independently of their position in the documentation set navigation. + +Dangling `toc` sections are **not** allowed and the assembler build will report an error if it finds any. All `toc` sections must be linked in `navigation.yml`. + +Read more details in the reference for [navigation.yml](../configure/site/navigation.md) + ## How it all works together 1. Each repository builds its documentation set independently From 9a9f20b2d377b62faaa9da0ff82b641574cbfa93 Mon Sep 17 00:00:00 2001 From: Martijn Laarman Date: Wed, 1 Oct 2025 12:34:14 +0200 Subject: [PATCH 012/171] update building blocks --- docs/building-blocks/index.md | 72 ++++++++++++++++++++++++++++++++--- 1 file changed, 67 insertions(+), 5 deletions(-) diff --git a/docs/building-blocks/index.md b/docs/building-blocks/index.md index a4bdaab89..983c000f3 100644 --- a/docs/building-blocks/index.md +++ b/docs/building-blocks/index.md @@ -49,17 +49,79 @@ Links from other documentation sets to yours. Validated to prevent breaking chan ### Documentation Set Navigation A documentation set is responsible for defining how files are organized in the navigation. This is done by defining a `toc` section in the `docset.yml` file. -If the `toc` section becomes to big folders can define a dedicated `toc.yml` file to organize the files and link them in their parent `toc.yml` or `docset.yml` file. + +```yaml +toc: + - file: index.md + - folder: contribute + children: + - file: index.md + - file: locally.md + children: + - file: page.md +``` + +If the `toc` section becomes too unwieldy folders can define a dedicated `toc.yml` file to organize their files and link them in their parent `toc.yml` or `docset.yml` file. + +```yaml +toc: + - file: index.md + - folder: contribute + children: + - file: index.md + - file: locally.md + children: + - file: page.md + - toc: development +``` + +Where `development/toc.yml` is defined as: + +```yaml +toc: + - file: index.md + - toc: link-validation +``` + +:::{note} +The folder name `development` is not repeated in the `toc.yml` file this allows for easier renames of the folder itself. +::: Read more details in the reference for [docset.yml](../configure/content-set/index.md) ### Global Navigation -The global navigation is defined in the [`navigation.yml`](../configure/site/navigation.md) file. -This navigation only concerns itself with `toc` sections defined in either `docset.yml` or `toc.yml` files. -These `toc` sections can be reorganized independently of their position in the documentation set navigation. +The global navigation is defined in the [`navigation.yml`](../configure/site/navigation.md) file. It follows a very similar +`toc` configuration structure to the documentation set navigation. + +It comes, however, with the following restrictions: + +* It may only link to `toc.yml` or `docset.yml` files + +```yaml +toc: + - toc: get-started + - toc: elasticsearch-net:// + - toc: extend + children: + - toc: kibana://extend + path_prefix: extend/kibana + - toc: logstash://extend + path_prefix: extend/logstash + - toc: beats://extend +``` + +Some syntactic notes: + +* The toc uses a similar [cross-link syntax to links](../syntax/links.md) +* The `./docset.yml` or `/toc.yml` suffix is implied, assembler will find the correct file for you. +* The narrative repository `elastic/docs-content` is 'special' so omitting `scheme://` implies `docs-content://`. + +These `toc` sections can be reorganized independently of their position in their origin documentation set navigation. +This allows sections from different repositories to be grouped together in the global navigation. -Dangling `toc` sections are **not** allowed and the assembler build will report an error if it finds any. All `toc` sections must be linked in `navigation.yml`. +All `toc` sections must be linked in `navigation.yml`. +Dangling `toc` sections are **not** allowed and the assembler build will report an error if it finds any. Read more details in the reference for [navigation.yml](../configure/site/navigation.md) From 0c617c5f222aee94c84954a2eb8c6a459c2dab60 Mon Sep 17 00:00:00 2001 From: Martijn Laarman Date: Wed, 1 Oct 2025 12:52:18 +0200 Subject: [PATCH 013/171] Include navigation building blocks --- docs/_docset.yml | 2 + .../documentation-set-navigation.md | 236 ++++++++++++++++++ docs/building-blocks/global-navigation.md | 193 ++++++++++++++ docs/building-blocks/index.md | 85 +------ 4 files changed, 438 insertions(+), 78 deletions(-) create mode 100644 docs/building-blocks/documentation-set-navigation.md create mode 100644 docs/building-blocks/global-navigation.md diff --git a/docs/_docset.yml b/docs/_docset.yml index 436f5dade..e2bd86d51 100644 --- a/docs/_docset.yml +++ b/docs/_docset.yml @@ -57,6 +57,8 @@ toc: - file: link-catalog.md - file: outbound-crosslinks.md - file: inbound-crosslinks.md + - file: documentation-set-navigation.md + - file: global-navigation.md - folder: configure children: - file: index.md diff --git a/docs/building-blocks/documentation-set-navigation.md b/docs/building-blocks/documentation-set-navigation.md new file mode 100644 index 000000000..3adf88b50 --- /dev/null +++ b/docs/building-blocks/documentation-set-navigation.md @@ -0,0 +1,236 @@ +--- +navigation_title: Documentation Set Navigation +--- + +# Documentation Set Navigation + +**Documentation set navigation** defines how files within a single documentation set are organized and structured. Each documentation set is responsible for its own internal navigation hierarchy. + +## Purpose + +Documentation set navigation allows repository maintainers to: + +* **Organize content** - Define the logical structure of their documentation +* **Control hierarchy** - Determine which pages are nested under others +* **Create sections** - Group related content together +* **Maintain autonomy** - Structure documentation independently of other repositories + +## Basic structure + +Navigation is defined in the `toc` (table of contents) section of the `docset.yml` file: + +```yaml +toc: + - file: index.md + - folder: contribute + children: + - file: index.md + - file: locally.md + children: + - file: page.md +``` + +## TOC node types + +The `toc` section supports several node types: + +### File nodes + +Reference a single markdown file: + +```yaml +- file: getting-started.md +``` + +Files can also have nested children: + +```yaml +- file: locally.md + children: + - file: page.md +``` + +### Folder nodes + +Group related files together: + +```yaml +- folder: configuration + children: + - file: index.md + - file: basic.md +``` + +`children` is optional for `folder` nodes. This will include all files in the folder. +This is especially useful during development when you are unsure how to structure your documentation. + +Once `children` is defined, it must reference all `.md` files in the folder. The build will fail if +it detects any dangling documentation files. + +### Hidden files + +Hide pages from navigation while keeping them accessible: + +```yaml +- hidden: deprecated-page.md +``` + +### Named TOC sections + +For larger documentation sets, create named TOC sections that can be referenced in [global navigation](global-navigation.md): + +```yaml +toc: + - file: index.md + - toc: development +``` + +This will include the `toc` section defined in `development/toc.yml`:` + +## Dedicated toc.yml files + +When a `toc` section becomes too unwieldy, folders can define a dedicated `toc.yml` file to organize their files and link them in their parent `toc.yml` or `docset.yml` file. + +### Example with nested TOC + +In your `docset.yml`: + +```yaml +toc: + - file: index.md + - folder: contribute + children: + - file: index.md + - file: locally.md + children: + - file: page.md + - toc: development +``` + +Then create `development/toc.yml`: + +```yaml +toc: + - file: index.md + - toc: link-validation +``` + +:::{note} +The folder name `development` is not repeated in the `toc.yml` file. This allows for easier renames of the folder itself. +::: + +### Benefits of separate toc.yml files + +* **Modularity** - Each section can be maintained independently +* **Cleaner docset.yml** - Keep the main file focused and readable +* **Easier refactoring** - Rename folders without updating TOC references +* **Team ownership** - Different teams can manage different TOC sections + +## File paths + +All file paths in the `toc` section are relative to the documentation set root (where `docset.yml` is located): + +```yaml +toc: + - file: index.md # docs/index.md + - folder: api + children: + - file: index.md # docs/api/index.md + - file: authentication.md # docs/api/authentication.md +``` + +## Navigation metadata + +You can customize how items appear in the navigation: + +### Custom titles + +The navigation title defaults to a markdown's page first `h1` heading. + +To present the file differently in the navigation, add a `navigation_title` metadata field. + +```markdown +--- +title: Getting Started with the Documentation Builder +navigation_title: Quick Start +--- +``` + +There is no way to set `title` in the `docset.yml` file. This is by design to keep a page's data +contained in its file. + +## Relationship to global navigation + +When building [assembled documentation](assembled-documentation.md), the documentation set navigation becomes a component of the [global navigation](global-navigation.md): + +* **Documentation set navigation** defines the structure **within** a repository +* **Global navigation** defines **how repositories are organized** relative to each other + +Named `toc` sections in `docset.yml` can be referenced and reorganized in the global `navigation.yml` file without affecting the documentation set's internal structure. + +## Best practices + +### Keep it organized + +* Group related content in folders +* Use descriptive folder and file names +* Maintain a logical hierarchy + +The folder names and hierarchy are reflected directly in the URL structure. + +### Use index files + +Always include an `index.md` in folders: + +```yaml +- folder: api + children: + - file: index.md # Overview of API documentation + - file: endpoints.md + - file: authentication.md +``` + +### Limit nesting depth + +Avoid deeply nested structures (more than three to four levels) to maintain navigation clarity. + +### Use toc.yml for large sections + +When a section contains many files or becomes complex, extract it to a dedicated `toc.yml`: + +``` +docs/ +├── docset.yml +├── index.md +└── development/ + ├── toc.yml # Define development section structure here + ├── index.md + └── link-validation/ + └── toc.yml # Nested TOC section +``` + +### Name TOC sections meaningfully + +Use clear, descriptive names for TOC sections: + +**Good:** +```yaml +- toc: api-reference +- toc: getting-started +- toc: troubleshooting +``` + +**Bad:** +```yaml +- toc: section1 +- toc: misc +- toc: other +``` + +These names will end up in the URL structure of the published documentation + +## Related concepts + +* [Global Navigation](global-navigation.md) - How documentation sets are organized in assembled documentation +* [Content Set Configuration](../configure/content-set/index.md) - Complete `docset.yml` reference +* [Navigation Configuration](../configure/content-set/navigation.md) - Detailed navigation options diff --git a/docs/building-blocks/global-navigation.md b/docs/building-blocks/global-navigation.md new file mode 100644 index 000000000..223bd0ffb --- /dev/null +++ b/docs/building-blocks/global-navigation.md @@ -0,0 +1,193 @@ +--- +navigation_title: Global Navigation +--- + +# Global Navigation + +**Global navigation** defines how multiple documentation sets are organized and presented together in [assembled documentation](assembled-documentation.md). It creates a unified navigation structure across all repositories. + +## Purpose + +Global navigation enables: + +* **Unified experience** - Present documentation from multiple repositories as a cohesive whole +* **Flexible organization** - Arrange documentation by product, feature, or audience rather than by repository +* **Independent evolution** - Reorganize global structure without changing documentation sets +* **Cross-repository grouping** - Combine related content from different repositories + +## Configuration + +Global navigation is defined in the `navigation.yml` file, which is part of the [site configuration](../configure/site/index.md). It follows a very similar `toc` configuration structure to [documentation set navigation](documentation-set-navigation.md). + +## Key differences from documentation set navigation + +Global navigation has specific restrictions: + +* **It may only link to `toc.yml` or `docset.yml` files** - You cannot reference individual markdown files +* **Uses crosslink syntax** - References to other repositories use the `repository://` syntax +* **Requires all TOC sections** - Dangling TOC sections are not allowed + +## Basic structure + +```yaml +toc: + - toc: get-started + - toc: elasticsearch-net:// + - toc: extend + children: + - toc: kibana://extend + path_prefix: extend/kibana + - toc: logstash://extend + path_prefix: extend/logstash + - toc: beats://extend +``` + +## Syntax notes + +### Crosslink syntax + +The TOC uses a similar [cross-link syntax to links](../syntax/links.md): + +```yaml +- toc: elasticsearch-net:// # References entire repository +- toc: kibana://extend # References 'extend' TOC section from kibana +``` + +### Implied suffixes + +The `./docset.yml` or `/toc.yml` suffix is implied - the assembler will find the correct file for you: + +### Special repository handling + +The narrative repository `elastic/docs-content` is 'special', so omitting `scheme://` implies `docs-content://`: + +```yaml +- toc: get-started # Implies docs-content://get-started +- toc: elasticsearch://setup # Explicitly from elasticsearch repository +``` + +## Path prefixes + +You must explicitly provide a URL path prefix when including a `toc`. + +```yaml +- toc: extend + children: + - toc: kibana://extend + path_prefix: extend/kibana # Override default path + - toc: logstash://extend + path_prefix: extend/logstash + - toc: beats://extend + path_prefix: extend/beats +``` + +This allows you to: +* Group content from different repositories under a common path +* Avoid URL conflicts when combining repositories +* Create product-specific URL structures + +## Reorganization independence + +These `toc` sections can be reorganized independently of their position in their origin documentation set navigation. This allows sections from different repositories to be grouped together in the global navigation. + +### Example: Cross-repository organization + +You can create a unified section that combines content from multiple repositories: + +```yaml +toc: + - toc: monitoring + children: + - toc: elasticsearch://monitoring + path_prefix: monitoring/elasticsearch + - toc: kibana://monitoring + path_prefix: monitoring/kibana + - toc: beats://monitoring + path_prefix: monitoring/beats +``` + +Even though each repository defines its own `monitoring` section, the global navigation presents them as a cohesive monitoring guide. + +## Dangling TOC sections + +All `toc` sections must be linked in `navigation.yml`. + +**Dangling `toc` sections are not allowed** and the assembler build will report an error if it finds any. + +This ensures: +* No content is accidentally excluded from the site +* Navigation references are always valid +* Documentation coverage is complete +* Every TOC section defined in a `docset.yml` appears somewhere in the global navigation + +### Example of validation + +If a repository defines: + +```yaml +# my-repo/docs/docset.yml +toc: + - file: index.md + - toc: getting-started + - toc: advanced +``` + +Then `navigation.yml` must reference both `getting-started` and `advanced`: + +```yaml +# navigation.yml +toc: + - toc: my-repo://getting-started + - toc: my-repo://advanced +``` + +If either is missing, the build will fail with an error about dangling TOC sections. + +## Validation + +When building assembled documentation, `docs-builder` validates: + +* All referenced TOC sections exist +* No TOC sections are dangling (unreferenced) +* Path prefixes don't conflict +* Crosslink references resolve correctly + +Validation errors will cause the assembler build to fail. + +## Navigation metadata + +You can customize how sections appear in global navigation: + +### Nested organization + +Create nested navigation structures: + +```yaml +toc: + - toc: getting-started + children: + - toc: elasticsearch://quickstart + - toc: kibana://quickstart + - toc: reference + children: + - toc: elasticsearch://apis + - toc: elasticsearch://settings +``` + +## Build process integration + +During an assembler build: + +1. `docs-builder` reads `navigation.yml` +2. It resolves all TOC section references across repositories +3. It validates that all sections are accounted for (no dangling sections) +4. It builds each documentation set with knowledge of its global path prefix +5. It generates the final site with unified navigation + +## Related concepts + +* [Documentation Set Navigation](documentation-set-navigation.md) - How individual repositories organize their content +* [Assembled Documentation](assembled-documentation.md) - The build process that uses global navigation +* [Site Configuration](../configure/site/index.md) - Complete site configuration reference +* [Navigation Configuration](../configure/site/navigation.md) - Detailed navigation.yml reference +* [Cross-link syntax](../syntax/links.md) - Understanding the repository:// syntax diff --git a/docs/building-blocks/index.md b/docs/building-blocks/index.md index 983c000f3..f571ad0f2 100644 --- a/docs/building-blocks/index.md +++ b/docs/building-blocks/index.md @@ -46,84 +46,13 @@ Links from other documentation sets to yours. Validated to prevent breaking chan ## Navigation -### Documentation Set Navigation - -A documentation set is responsible for defining how files are organized in the navigation. This is done by defining a `toc` section in the `docset.yml` file. - -```yaml -toc: - - file: index.md - - folder: contribute - children: - - file: index.md - - file: locally.md - children: - - file: page.md -``` - -If the `toc` section becomes too unwieldy folders can define a dedicated `toc.yml` file to organize their files and link them in their parent `toc.yml` or `docset.yml` file. - -```yaml -toc: - - file: index.md - - folder: contribute - children: - - file: index.md - - file: locally.md - children: - - file: page.md - - toc: development -``` - -Where `development/toc.yml` is defined as: - -```yaml -toc: - - file: index.md - - toc: link-validation -``` - -:::{note} -The folder name `development` is not repeated in the `toc.yml` file this allows for easier renames of the folder itself. -::: - -Read more details in the reference for [docset.yml](../configure/content-set/index.md) - -### Global Navigation - -The global navigation is defined in the [`navigation.yml`](../configure/site/navigation.md) file. It follows a very similar -`toc` configuration structure to the documentation set navigation. - -It comes, however, with the following restrictions: - -* It may only link to `toc.yml` or `docset.yml` files - -```yaml -toc: - - toc: get-started - - toc: elasticsearch-net:// - - toc: extend - children: - - toc: kibana://extend - path_prefix: extend/kibana - - toc: logstash://extend - path_prefix: extend/logstash - - toc: beats://extend -``` - -Some syntactic notes: - -* The toc uses a similar [cross-link syntax to links](../syntax/links.md) -* The `./docset.yml` or `/toc.yml` suffix is implied, assembler will find the correct file for you. -* The narrative repository `elastic/docs-content` is 'special' so omitting `scheme://` implies `docs-content://`. - -These `toc` sections can be reorganized independently of their position in their origin documentation set navigation. -This allows sections from different repositories to be grouped together in the global navigation. - -All `toc` sections must be linked in `navigation.yml`. -Dangling `toc` sections are **not** allowed and the assembler build will report an error if it finds any. - -Read more details in the reference for [navigation.yml](../configure/site/navigation.md) +### [Documentation Set Navigation](documentation-set-navigation.md) + +How individual documentation sets organize their content through TOC sections in `docset.yml` and `toc.yml` files. Each repository controls its own internal navigation structure, including file and folder organization. + +### [Global Navigation](global-navigation.md) + +How multiple documentation sets are organized together in assembled documentation through `navigation.yml`. Uses crosslink syntax to reference TOC sections from different repositories and enables cross-repository content organization. ## How it all works together From e593ff0f394f0f4ce63da2c961379d86f7dc17ef Mon Sep 17 00:00:00 2001 From: Martijn Laarman Date: Wed, 1 Oct 2025 13:02:35 +0200 Subject: [PATCH 014/171] Small touchups --- docs/building-blocks/link-catalog.md | 6 +++--- docs/building-blocks/link-service.md | 11 +++++++++-- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/docs/building-blocks/link-catalog.md b/docs/building-blocks/link-catalog.md index 866a5bbb6..a8705be16 100644 --- a/docs/building-blocks/link-catalog.md +++ b/docs/building-blocks/link-catalog.md @@ -32,6 +32,7 @@ The Link Catalog contains: * Metadata about each Link Index: * Last updated timestamp * Commit SHA that produced the Link Index + * ETAG of the Link Index file * URL to the Link Index file ## Maintenance @@ -52,9 +53,8 @@ This process ensures the catalog stays in sync with published Link Index files w When running `docs-builder assembler clone` or `docs-builder assembler build`: 1. The assembler fetches the Link Catalog -2. It determines which repositories and versions to clone/build based on the site configuration +2. It determines which repositories and versions to clone/build based on the [site configuration](../configure/site/index.md) 3. It uses the commit SHAs from the catalog to clone specific versions -4. It falls back to the last known good commit if a repository's current state has build failures ### By documentation builds @@ -62,7 +62,7 @@ During a documentation build: 1. `docs-builder` fetches the Link Catalog 2. It determines which Link Index files to download for cross-repository validation -3. It validates all crosslinks against the appropriate Link Index files +3. It validates all cross-links against the appropriate Link Index files ## Benefits diff --git a/docs/building-blocks/link-service.md b/docs/building-blocks/link-service.md index b3129cc43..e45ab097f 100644 --- a/docs/building-blocks/link-service.md +++ b/docs/building-blocks/link-service.md @@ -4,13 +4,20 @@ navigation_title: Link Service # Link Service -The **Link Service** is the central location where all [Link Index](link-index.md) files are published and stored. +The **Link Service** is the central location that stores: + +* All [Link Index](link-index.md) files for all the repositories and branches that are published. +* The [Link Catalog](link-catalog.md), a single JSON file that contains references to all the `Link Index` files. + +We only have one link service today for all public documentation. + +* https://elastic-docs-link-index.s3.us-east-2.amazonaws.com/ ## Architecture The Link Service is implemented as: -* **Storage** - An S3 bucket containing all Link Index files +* **Storage** - An S3 bucket * **CDN** - CloudFront fronting the S3 bucket for fast global access * **Access** - Publicly accessible for read operations From 2e0cfd4d6ca3393c9bef68fb2161c717ea6e47da Mon Sep 17 00:00:00 2001 From: Martijn Laarman Date: Wed, 1 Oct 2025 13:14:53 +0200 Subject: [PATCH 015/171] update redirects --- docs/_redirects.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/_redirects.yml b/docs/_redirects.yml index e3ed6e726..df2983983 100644 --- a/docs/_redirects.yml +++ b/docs/_redirects.yml @@ -1,4 +1,6 @@ redirects: + 'migration/freeze/gh-action.md' : 'index.md' + 'migration/freeze/index.md' : 'index.md' 'testing/redirects/4th-page.md': 'testing/redirects/5th-page.md' 'testing/redirects/9th-page.md': '!testing/redirects/5th-page.md' 'testing/redirects/6th-page.md': From ea397463714854da333f0a2673b539b37dc98884 Mon Sep 17 00:00:00 2001 From: Martijn Laarman Date: Wed, 1 Oct 2025 14:12:02 +0200 Subject: [PATCH 016/171] Apply suggestions from code review Co-authored-by: Fabrizio Ferri-Benedetti --- .../assembled-documentation.md | 50 ++++++++++--------- 1 file changed, 26 insertions(+), 24 deletions(-) diff --git a/docs/building-blocks/assembled-documentation.md b/docs/building-blocks/assembled-documentation.md index 33020aaa1..1ed3e2515 100644 --- a/docs/building-blocks/assembled-documentation.md +++ b/docs/building-blocks/assembled-documentation.md @@ -1,50 +1,52 @@ --- -navigation_title: Assembled Documentation +navigation_title: Assembled documentation --- -# Assembled Documentation +# Assembled documentation -**Assembled documentation** is the product of building many [documentation sets](documentation-set.md) and weaving them into a global navigation to produce the fully assembled documentation site. +Assembled documentation is the product of building many [documentation sets](documentation-set.md) and weaving them into a global navigation to produce the fully assembled documentation site. ## How it works The assembler: -1. Clones multiple documentation repositories -2. Reads its [configuration files](../configure/site/index.md) and builds [a global navigation](../configure/site/navigation.md) -3. Builds each documentation set independently using the global configuration and navigation to inform path prefixes -4. Produces a unified documentation website +1. Clones multiple documentation repositories. +2. Reads the [configuration files](../configure/site/index.md) and builds [a global navigation](../configure/site/navigation.md). +3. Builds each documentation set independently using the global configuration and navigation to inform path prefixes. +4. Produces a unified documentation website. ## Configuration Assembled documentation is configured through the site configuration, which defines: -* [assembler.yml](../configure/site/index.md) Which repositories to include and [their branching strategy](../contribute/branching-strategy.md) -* [navigation.yml](../configure/site/index.md) Navigation and url prefixes for TOC's. -* [versions.yml](../configure/site/versions.md) Defines the various versioning schemes of products/solutions being documented -* [products.yml](../configure/site/products.md) Defines the product catalog (id, name) and ties it to a specific versioning scheme +* [assembler.yml](../configure/site/index.md): Which repositories to include and [their branching strategy](../contribute/branching-strategy.md) +* [navigation.yml](../configure/site/index.md): Navigation and url prefixes for TOC's. +* [versions.yml](../configure/site/versions.md): Defines the various versioning schemes of products/solutions being documented +* [products.yml](../configure/site/products.md): Defines the product catalog (id, name) and ties it to a specific versioning scheme -See [Site Configuration](../configure/site/index.md) for complete details on configuring assembled documentation. +Refer to [Site Configuration](../configure/site/index.md) for details on configuring assembled documentation. -Important to note that `docs-builder` makes no assumptions about how repositories, products, solutions and versions tie into each other. +:::{important} +The `docs-builder` command makes no assumptions about how repositories, products, solutions and versions tie into each other. +::: ## Build process -The typical build process for assembled documentation: +The typical build process for assembled documentation consists of three steps: -1. **Clone** - Clone all configured repositories using `docs-builder assembler clone` -2. **Build** - Build all documentation sets using `docs-builder assembler build` -3. **Serve** - Serve the documentation on http://localhost:4000 using `docs-builder assembler serve` +1. Clone all configured repositories using `docs-builder assembler clone`. +2. Build all documentation sets using `docs-builder assembler build`. +3. Serve the documentation on http://localhost:4000 using `docs-builder assembler serve`. This uses the embedded configuration files inside the `docs-builder` binary. To build a specific configuration: -1. **Init Config** - Fetch the latest config to `$(pwd)/config` `docs-builder assembler config init --local` -2. **Clone** - Clone all configured repositories using `docs-builder assembler clone --local` -3. **Build** - Build all documentation sets using `docs-builder assembler build --local` -4. **Serve** - Serve the documentation on http://localhost:4000 using `docs-builder assembler serve` +1. Fetch the latest config to `$(pwd)/config` `docs-builder assembler config init --local`. +2. Clone all configured repositories using `docs-builder assembler clone --local`. +3. Build all documentation sets using `docs-builder assembler build --local`. +4. Serve the documentation on http://localhost:4000 using `docs-builder assembler serve`. ## Related concepts -* [Documentation Set](documentation-set.md) - The individual units being assembled -* [Distributed Documentation](distributed-documentation.md) - How documentation sets work independently -* [Link Catalog](link-catalog.md) - How the assembler knows what to include +* [Documentation set](documentation-set.md): The individual units being assembled. +* [Distributed documentation](distributed-documentation.md): How documentation sets work independently. +* [Link catalog](link-catalog.md): How the assembler knows what to include. From b3b5309d652e2f7360c610eb16b1c1c582d7cebf Mon Sep 17 00:00:00 2001 From: Martijn Laarman Date: Wed, 1 Oct 2025 14:17:35 +0200 Subject: [PATCH 017/171] Sentence case --- docs/building-blocks/distributed-documentation.md | 4 ++-- docs/building-blocks/documentation-set-navigation.md | 4 ++-- docs/building-blocks/documentation-set.md | 4 ++-- docs/building-blocks/global-navigation.md | 4 ++-- docs/building-blocks/inbound-crosslinks.md | 4 ++-- docs/building-blocks/index.md | 4 ++-- docs/building-blocks/link-catalog.md | 4 ++-- docs/building-blocks/link-index.md | 4 ++-- docs/building-blocks/link-service.md | 4 ++-- docs/building-blocks/outbound-crosslinks.md | 4 ++-- 10 files changed, 20 insertions(+), 20 deletions(-) diff --git a/docs/building-blocks/distributed-documentation.md b/docs/building-blocks/distributed-documentation.md index 8258e004f..dc06f34c6 100644 --- a/docs/building-blocks/distributed-documentation.md +++ b/docs/building-blocks/distributed-documentation.md @@ -1,8 +1,8 @@ --- -navigation_title: Distributed Documentation +navigation_title: Distributed documentation --- -# Distributed Documentation +# Distributed documentation **Distributed documentation** is the architectural approach that allows repositories to build their own documentation independently. diff --git a/docs/building-blocks/documentation-set-navigation.md b/docs/building-blocks/documentation-set-navigation.md index 3adf88b50..40cbac2e3 100644 --- a/docs/building-blocks/documentation-set-navigation.md +++ b/docs/building-blocks/documentation-set-navigation.md @@ -1,8 +1,8 @@ --- -navigation_title: Documentation Set Navigation +navigation_title: Documentation set navigation --- -# Documentation Set Navigation +# Documentation set navigation **Documentation set navigation** defines how files within a single documentation set are organized and structured. Each documentation set is responsible for its own internal navigation hierarchy. diff --git a/docs/building-blocks/documentation-set.md b/docs/building-blocks/documentation-set.md index 5ed4dc2a5..53da16a48 100644 --- a/docs/building-blocks/documentation-set.md +++ b/docs/building-blocks/documentation-set.md @@ -1,8 +1,8 @@ --- -navigation_title: Documentation Set +navigation_title: Documentation set --- -# Documentation Set +# Documentation set A **documentation set** is a single folder containing the documentation of a single repository. This is the fundamental unit of documentation in the docs-builder system. diff --git a/docs/building-blocks/global-navigation.md b/docs/building-blocks/global-navigation.md index 223bd0ffb..cb86efe55 100644 --- a/docs/building-blocks/global-navigation.md +++ b/docs/building-blocks/global-navigation.md @@ -1,8 +1,8 @@ --- -navigation_title: Global Navigation +navigation_title: Global navigation --- -# Global Navigation +# Global navigation **Global navigation** defines how multiple documentation sets are organized and presented together in [assembled documentation](assembled-documentation.md). It creates a unified navigation structure across all repositories. diff --git a/docs/building-blocks/inbound-crosslinks.md b/docs/building-blocks/inbound-crosslinks.md index e661c7f65..c7a489b91 100644 --- a/docs/building-blocks/inbound-crosslinks.md +++ b/docs/building-blocks/inbound-crosslinks.md @@ -1,8 +1,8 @@ --- -navigation_title: Inbound Crosslinks +navigation_title: Inbound cross-links --- -# Inbound Crosslinks +# Inbound cross-links **Inbound crosslinks** are links from other documentation sets to yours. Understanding and validating inbound crosslinks helps prevent breaking links in other repositories when you make changes. diff --git a/docs/building-blocks/index.md b/docs/building-blocks/index.md index f571ad0f2..671ae2df3 100644 --- a/docs/building-blocks/index.md +++ b/docs/building-blocks/index.md @@ -1,8 +1,8 @@ --- -navigation_title: Building Blocks +navigation_title: Building blocks --- -# Building Blocks +# Building blocks This section explains the core concepts and building blocks that make up the docs-builder architecture. Understanding these concepts will help you work effectively with distributed documentation and cross-repository linking. diff --git a/docs/building-blocks/link-catalog.md b/docs/building-blocks/link-catalog.md index a8705be16..a77060173 100644 --- a/docs/building-blocks/link-catalog.md +++ b/docs/building-blocks/link-catalog.md @@ -1,8 +1,8 @@ --- -navigation_title: Link Catalog +navigation_title: Link catalog --- -# Link Catalog +# Link catalog The **Link Catalog** is a single JSON file that serves as a catalog of all available [Link Index](link-index.md) files across all repositories. diff --git a/docs/building-blocks/link-index.md b/docs/building-blocks/link-index.md index b12bad7da..074ab4c49 100644 --- a/docs/building-blocks/link-index.md +++ b/docs/building-blocks/link-index.md @@ -1,8 +1,8 @@ --- -navigation_title: Link Index +navigation_title: Link index --- -# Link Index +# Link index A **Link Index** is a JSON file (`links.json`) that contains all the linkable resources for a specific repository branch. diff --git a/docs/building-blocks/link-service.md b/docs/building-blocks/link-service.md index e45ab097f..a4f2dd337 100644 --- a/docs/building-blocks/link-service.md +++ b/docs/building-blocks/link-service.md @@ -1,8 +1,8 @@ --- -navigation_title: Link Service +navigation_title: Link service --- -# Link Service +# Link service The **Link Service** is the central location that stores: diff --git a/docs/building-blocks/outbound-crosslinks.md b/docs/building-blocks/outbound-crosslinks.md index 0fb913880..09d58fdbd 100644 --- a/docs/building-blocks/outbound-crosslinks.md +++ b/docs/building-blocks/outbound-crosslinks.md @@ -1,8 +1,8 @@ --- -navigation_title: Outbound Crosslinks +navigation_title: Outbound crosslinks --- -# Outbound Crosslinks +# Outbound crosslinks **Outbound crosslinks** are links from your documentation set to other documentation sets in different repositories. From eb5f50d130a54f014b86475b6539caf07135763c Mon Sep 17 00:00:00 2001 From: Martijn Laarman Date: Wed, 1 Oct 2025 14:26:34 +0200 Subject: [PATCH 018/171] fix links --- docs/_docset.yml | 4 ++-- .../distributed-documentation.md | 4 ++-- ...d-crosslinks.md => inbound-cross-links.md} | 6 ++--- docs/building-blocks/index.md | 8 +++---- docs/building-blocks/link-index.md | 10 ++++----- docs/building-blocks/link-service.md | 4 ++-- ...-crosslinks.md => outbound-cross-links.md} | 22 +++++++++---------- docs/cli/assembler/assembler-deploy-apply.md | 2 +- docs/cli/docset/index.md | 12 +++++----- docs/cli/links/index.md | 6 ++--- docs/configure/content-set/index.md | 8 +++---- 11 files changed, 43 insertions(+), 43 deletions(-) rename docs/building-blocks/{inbound-crosslinks.md => inbound-cross-links.md} (91%) rename docs/building-blocks/{outbound-crosslinks.md => outbound-cross-links.md} (77%) diff --git a/docs/_docset.yml b/docs/_docset.yml index e2bd86d51..bad2fe7e4 100644 --- a/docs/_docset.yml +++ b/docs/_docset.yml @@ -55,8 +55,8 @@ toc: - file: link-service.md - file: link-index.md - file: link-catalog.md - - file: outbound-crosslinks.md - - file: inbound-crosslinks.md + - file: outbound-cross-links.md + - file: inbound-cross-links.md - file: documentation-set-navigation.md - file: global-navigation.md - folder: configure diff --git a/docs/building-blocks/distributed-documentation.md b/docs/building-blocks/distributed-documentation.md index dc06f34c6..eeb799e2d 100644 --- a/docs/building-blocks/distributed-documentation.md +++ b/docs/building-blocks/distributed-documentation.md @@ -55,8 +55,8 @@ The distributed documentation system relies on several key components: * [Link Index](link-index.md) - Per-repository file of linkable resources * [Link Service](link-service.md) - Central storage for Link Index files * [Link Catalog](link-catalog.md) - Catalog of all available Link Index files -* [Outbound Crosslinks](outbound-crosslinks.md) - Links from one repository to another -* [Inbound Crosslinks](inbound-crosslinks.md) - Links from other repositories to yours +* [Outbound Cross-links](outbound-cross-links.md) - Links from one repository to another +* [Inbound Cross-links](inbound-cross-links.md) - Links from other repositories to yours ## Local development diff --git a/docs/building-blocks/inbound-crosslinks.md b/docs/building-blocks/inbound-cross-links.md similarity index 91% rename from docs/building-blocks/inbound-crosslinks.md rename to docs/building-blocks/inbound-cross-links.md index c7a489b91..0cedfb17f 100644 --- a/docs/building-blocks/inbound-crosslinks.md +++ b/docs/building-blocks/inbound-cross-links.md @@ -4,11 +4,11 @@ navigation_title: Inbound cross-links # Inbound cross-links -**Inbound crosslinks** are links from other documentation sets to yours. Understanding and validating inbound crosslinks helps prevent breaking links in other repositories when you make changes. +**Inbound cross-links** are links from other documentation sets to yours. Understanding and validating inbound cross-links helps prevent breaking links in other repositories when you make changes. ## Purpose -Inbound crosslink validation allows you to: +Inbound cross-link validation allows you to: * **Detect breaking changes** - Know when renaming or deleting a file will break links from other repositories * **Prevent regressions** - Avoid publishing changes that break documentation elsewhere @@ -114,7 +114,7 @@ Make inbound link validation part of your review process: ## Related concepts -* [Outbound Crosslinks](outbound-crosslinks.md) - Links from your documentation to others +* [Outbound Cross-links](outbound-cross-links.md) - Links from your documentation to others * [Link Index](link-index.md) - How your linkable resources are tracked * [Link Service](link-service.md) - Where inbound link information is stored * [Distributed Documentation](distributed-documentation.md) - The architecture enabling this validation diff --git a/docs/building-blocks/index.md b/docs/building-blocks/index.md index 671ae2df3..087ffc58f 100644 --- a/docs/building-blocks/index.md +++ b/docs/building-blocks/index.md @@ -36,11 +36,11 @@ A catalog file listing all available Link Index files across all repositories an ## Cross-repository linking -### [Outbound Crosslinks](outbound-crosslinks.md) +### [Outbound Cross-links](outbound-cross-links.md) Links from your documentation to other documentation sets. Validated against published Link Index files to ensure they're correct. -### [Inbound Crosslinks](inbound-crosslinks.md) +### [Inbound Cross-links](inbound-cross-links.md) Links from other documentation sets to yours. Validated to prevent breaking changes when you move or delete content. @@ -52,13 +52,13 @@ How individual documentation sets organize their content through TOC sections in ### [Global Navigation](global-navigation.md) -How multiple documentation sets are organized together in assembled documentation through `navigation.yml`. Uses crosslink syntax to reference TOC sections from different repositories and enables cross-repository content organization. +How multiple documentation sets are organized together in assembled documentation through `navigation.yml`. Uses cross-link syntax to reference TOC sections from different repositories and enables cross-repository content organization. ## How it all works together 1. Each repository builds its documentation set independently 2. Successful builds publish a Link Index to the Link Service 3. The Link Catalog catalogs all available Link Index files -4. Documentation builds validate crosslinks using these Link Index files +4. Documentation builds validate cross-links using these Link Index files 5. The assembler combines documentation sets using the Link Catalog 6. Teams can work independently while maintaining link integrity across repositories diff --git a/docs/building-blocks/link-index.md b/docs/building-blocks/link-index.md index 074ab4c49..a0b2a4a96 100644 --- a/docs/building-blocks/link-index.md +++ b/docs/building-blocks/link-index.md @@ -46,16 +46,16 @@ The Link Index is automatically generated during a documentation build: ## Usage -### Resolving outbound crosslinks +### Resolving outbound cross-links -When you use a crosslink like `elasticsearch://reference/api/search.md`, `docs-builder`: +When you use a cross-link like `elasticsearch://reference/api/search.md`, `docs-builder`: 1. Fetches the Elasticsearch Link Index from the Link Service 2. Looks up the path in the index 3. Validates the link exists 4. Resolves it to the correct URL -### Validating inbound crosslinks +### Validating inbound cross-links When building your documentation, `docs-builder` can: @@ -82,5 +82,5 @@ https://elastic-docs-link-index.s3.us-east-2.amazonaws.com/{org}/{repo}/{branch} * [Link Service](link-service.md) - Where Link Index files are stored * [Link Catalog](link-catalog.md) - Catalog of all Link Index files -* [Outbound Crosslinks](outbound-crosslinks.md) - Links that use the Link Index -* [Inbound Crosslinks](inbound-crosslinks.md) - Links to resources in the Link Index +* [Outbound Cross-links](outbound-cross-links.md) - Links that use the Link Index +* [Inbound Cross-links](inbound-cross-links.md) - Links to resources in the Link Index diff --git a/docs/building-blocks/link-service.md b/docs/building-blocks/link-service.md index a4f2dd337..7d173f212 100644 --- a/docs/building-blocks/link-service.md +++ b/docs/building-blocks/link-service.md @@ -56,8 +56,8 @@ When a documentation build completes successfully on a default integration branc During both local and CI builds, `docs-builder`: * Fetches relevant Link Index files from the Link Service -* Validates outbound crosslinks against these indexes -* Validates that inbound crosslinks won't be broken by local changes +* Validates outbound cross-links against these indexes +* Validates that inbound cross-links won't be broken by local changes ## Related concepts diff --git a/docs/building-blocks/outbound-crosslinks.md b/docs/building-blocks/outbound-cross-links.md similarity index 77% rename from docs/building-blocks/outbound-crosslinks.md rename to docs/building-blocks/outbound-cross-links.md index 09d58fdbd..07353b1e3 100644 --- a/docs/building-blocks/outbound-crosslinks.md +++ b/docs/building-blocks/outbound-cross-links.md @@ -1,14 +1,14 @@ --- -navigation_title: Outbound crosslinks +navigation_title: Outbound cross-links --- -# Outbound crosslinks +# Outbound cross-links -**Outbound crosslinks** are links from your documentation set to other documentation sets in different repositories. +**Outbound cross-links** are links from your documentation set to other documentation sets in different repositories. ## Purpose -Outbound crosslinks allow you to: +Outbound cross-links allow you to: * Link to documentation in other repositories * Maintain those links even as the target repository evolves @@ -17,7 +17,7 @@ Outbound crosslinks allow you to: ## Syntax -If both repositories publish to the same [Link Service](link-service.md), they can link to each other using the crosslink syntax: +If both repositories publish to the same [Link Service](link-service.md), they can link to each other using the cross-link syntax: ```markdown [Link text](repository-name://path/to/file.md) @@ -39,12 +39,12 @@ cross_links: ``` -When `docs-builder` encounters a crosslink: +When `docs-builder` encounters a cross-link: 1. **Parse** - Extracts the repository name and path from the link 3. **Resolve** - Looks up the path in the locally cached [Link Index](link-index.md) to get the actual URL 4. **Validate** - Verifies the link exists and generates an error if not -5. **Transform** - Replaces the crosslink with the fully resolved URL in the output +5. **Transform** - Replaces the cross-link with the fully resolved URL in the output ## Validation @@ -67,7 +67,7 @@ cross_links: This instructs `docs-builder` to fetch the `Link Index` from the [Link Service](link-service.md) during the build process which are then cached locally. `docs-builder` will validate locally cached `Link Index` files against the remote `Link Index` files on each build fetching updates as needed. -Now you can create crosslinks e.g `elasticsearch://path/to/file.md` +Now you can create cross-links e.g `elasticsearch://path/to/file.md` The explicit opt-in prevents each repository build having the fetch all the links for all the repositories in the [`Link Catalog`](link-catalog.md) of which there may be many. @@ -85,7 +85,7 @@ The explicit opt-in prevents each repository build having the fetch all the link [Search API](https://www.elastic.co/guide/en/elasticsearch/reference/current/search.html) ``` -The crosslink syntax is resilient to: +The cross-link syntax is resilient to: * URL structure changes * File moves (if redirects are configured) * Version differences @@ -100,6 +100,6 @@ You can link to specific headings within a page: ## Related concepts -* [Inbound Crosslinks](inbound-crosslinks.md) - Links from other repositories to yours -* [Link Index](link-index.md) - How crosslinks are resolved +* [Inbound Cross-links](inbound-cross-links.md) - Links from other repositories to yours +* [Link Index](link-index.md) - How cross-links are resolved * [Links syntax](../syntax/links.md) - Complete link syntax documentation diff --git a/docs/cli/assembler/assembler-deploy-apply.md b/docs/cli/assembler/assembler-deploy-apply.md index ce1c0df27..4e745387a 100644 --- a/docs/cli/assembler/assembler-deploy-apply.md +++ b/docs/cli/assembler/assembler-deploy-apply.md @@ -4,7 +4,7 @@ navigation_title: "deploy apply" # assembler deploy apply -Applies an incremental synchronization plan created by [`docs-builder assembler deploy plan`](./assembler-deploy-plan). +Applies an incremental synchronization plan created by [`docs-builder assembler deploy plan`](./assembler-deploy-plan.md). ## Usage diff --git a/docs/cli/docset/index.md b/docs/cli/docset/index.md index fc11c183c..d3e794c05 100644 --- a/docs/cli/docset/index.md +++ b/docs/cli/docset/index.md @@ -6,7 +6,7 @@ navigation_title: "documentation set" An isolated build means building a single documentation set. -A `Documentation Set` is defined as a folder containing a [docset.yml](../configure/content-set/index.md) file. +A `Documentation Set` is defined as a folder containing a [docset.yml](../../configure/content-set/index.md) file. These commands are typically what you interface with when you are working on the documentation of a single repository locally. @@ -15,12 +15,12 @@ These commands are typically what you interface with when you are working on the `build` is the default command so you can just run `docs-builder` to build a single documentation set. `docs-builder` will locate the `docset.yml` anywhere in the directory tree automatically and build the documentation. -- [build](docset/build.md) - build a single documentation set (incrementally) -- [serve](docset/serve.md) - partial build and serve documentation as needed at http://localhost:3000 -- [index](docset/index-command.md) - ingest a single documentation set to an Elasticsearch index. +- [build](build.md) - build a single documentation set (incrementally) +- [serve](serve.md) - partial build and serve documentation as needed at http://localhost:3000 +- [index](index-command.md) - ingest a single documentation set to an Elasticsearch index. ## Refactor commands -- [mv](docset/mv.md) - move a file or folder to a new location. This will rewrite all links in all files too. -- [diff validate](docset/diff-validate.md) - validate that local changes are reflected in [redirects.yml](../contribute/redirects.md) +- [mv](mv.md) - move a file or folder to a new location. This will rewrite all links in all files too. +- [diff validate](diff-validate.md) - validate that local changes are reflected in [redirects.yml](../../contribute/redirects.md) diff --git a/docs/cli/links/index.md b/docs/cli/links/index.md index 60f838ba3..002dbfbe7 100644 --- a/docs/cli/links/index.md +++ b/docs/cli/links/index.md @@ -10,6 +10,6 @@ Inbound links, those going from other sources to the documentation set, are vali ### Inbound link validation commands -- [inbound-links validate-all](links/inbound-links-validate-all.md) - validate all inbounds links as published to the links registry. -- [inbound-links validate](links/inbound-links-validate.md) - validate inbound links from and to specific repositories -- [inbound-links validate-link-reference](links/inbound-links-validate-link-reference.md) - validate a local link reference artifact from [build](docset/build.md) with the published registry +- [inbound-links validate-all](inbound-links-validate-all.md) - validate all inbounds links as published to the links registry. +- [inbound-links validate](inbound-links-validate.md) - validate inbound links from and to specific repositories +- [inbound-links validate-link-reference](inbound-links-validate-link-reference.md) - validate a local link reference artifact from [build](../docset/build.md) with the published registry diff --git a/docs/configure/content-set/index.md b/docs/configure/content-set/index.md index 30586e711..f813d3de2 100644 --- a/docs/configure/content-set/index.md +++ b/docs/configure/content-set/index.md @@ -8,10 +8,10 @@ Elastic documentation is spread across multiple repositories. Each repository ca A content set in `docs-builder` is equivalent to an AsciiDoc book. At this level, the system consists of: -| System property | Asciidoc | V3 | -| -------------------- | -------------------- | -------------------- | -| **Content source files** --> A whole bunch of markup files as well as any other assets used in the docs (for example, images, videos, and diagrams). | **Markup**: AsciiDoc files **Assets**: Images, videos, and diagrams | **Markup**: MD files **Assets**: Images, videos, and diagrams | -| **Information architecture** --> A way to specify the order in which these text-based files should appear in the information architecture of the book. | `index.asciidoc` file (this can be spread across several AsciiDoc files, but generally starts with the index file specified in the `conf.yaml` file)) | `docset.yml` and/or `toc.yml` file(s) | +| System property | Asciidoc | V3 | +|--------------------------------------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------| +| **Content source files** --> A whole bunch of markup files as well as any other assets used in the docs (for example, images, videos, and diagrams). | **Markup**: AsciiDoc files **Assets**: Images, videos, and diagrams | **Markup**: MD files **Assets**: Images, videos, and diagrams | +| **Information architecture** --> A way to specify the order in which these text-based files should appear in the information architecture of the book. | `index.asciidoc` file (this can be spread across several AsciiDoc files, but generally starts with the index file specified in the `conf.yaml` file)) | `docset.yml` and/or `toc.yml` file(s) | ## Learn more From 73d827f8ca1d7093e6f1eb72cecd1dee67958184 Mon Sep 17 00:00:00 2001 From: Martijn Laarman Date: Wed, 1 Oct 2025 14:42:26 +0200 Subject: [PATCH 019/171] List items full stop --- .../assembled-documentation.md | 12 ++--- .../distributed-documentation.md | 34 +++++++------- .../documentation-set-navigation.md | 32 ++++++------- docs/building-blocks/documentation-set.md | 16 +++---- docs/building-blocks/global-navigation.md | 46 +++++++++---------- docs/building-blocks/inbound-cross-links.md | 28 +++++------ docs/building-blocks/link-catalog.md | 34 +++++++------- docs/building-blocks/link-index.md | 32 ++++++------- docs/building-blocks/link-service.md | 32 ++++++------- docs/building-blocks/outbound-cross-links.md | 24 +++++----- docs/cli/assembler/assemble.md | 2 +- docs/cli/assembler/assembler-config-init.md | 6 +-- .../assembler-content-source-match.md | 6 +-- docs/cli/docset/build.md | 4 +- docs/cli/docset/diff-validate.md | 2 +- docs/configure/content-set/index.md | 6 +-- docs/configure/index.md | 10 ++-- docs/configure/site/content.md | 8 ++-- .../cumulative-docs/badge-placement.md | 10 ++-- .../cumulative-docs/example-scenarios.md | 12 ++--- 20 files changed, 178 insertions(+), 178 deletions(-) diff --git a/docs/building-blocks/assembled-documentation.md b/docs/building-blocks/assembled-documentation.md index 1ed3e2515..171314350 100644 --- a/docs/building-blocks/assembled-documentation.md +++ b/docs/building-blocks/assembled-documentation.md @@ -19,10 +19,10 @@ The assembler: Assembled documentation is configured through the site configuration, which defines: -* [assembler.yml](../configure/site/index.md): Which repositories to include and [their branching strategy](../contribute/branching-strategy.md) +* [assembler.yml](../configure/site/index.md): Which repositories to include and [their branching strategy](../contribute/branching-strategy.md). * [navigation.yml](../configure/site/index.md): Navigation and url prefixes for TOC's. -* [versions.yml](../configure/site/versions.md): Defines the various versioning schemes of products/solutions being documented -* [products.yml](../configure/site/products.md): Defines the product catalog (id, name) and ties it to a specific versioning scheme +* [versions.yml](../configure/site/versions.md): Defines the various versioning schemes of products/solutions being documented. +* [products.yml](../configure/site/products.md): Defines the product catalog (id, name) and ties it to a specific versioning scheme. Refer to [Site Configuration](../configure/site/index.md) for details on configuring assembled documentation. @@ -47,6 +47,6 @@ This uses the embedded configuration files inside the `docs-builder` binary. To ## Related concepts -* [Documentation set](documentation-set.md): The individual units being assembled. -* [Distributed documentation](distributed-documentation.md): How documentation sets work independently. -* [Link catalog](link-catalog.md): How the assembler knows what to include. +* [Documentation Set](documentation-set.md): The individual units being assembled. +* [Distributed Documentation](distributed-documentation.md): How documentation sets work independently. +* [Link Catalog](link-catalog.md): How the assembler knows what to include. diff --git a/docs/building-blocks/distributed-documentation.md b/docs/building-blocks/distributed-documentation.md index eeb799e2d..0f801faa3 100644 --- a/docs/building-blocks/distributed-documentation.md +++ b/docs/building-blocks/distributed-documentation.md @@ -10,10 +10,10 @@ navigation_title: Distributed documentation The separation between building individual documentation sets and assembling them enables distributed builds, where: -* Each repository builds its own documentation independently -* Builds don't block each other -* Teams maintain full autonomy over their documentation -* Cross-repository links are validated without requiring synchronized builds +* Each repository builds its own documentation independently. +* Builds don't block each other. +* Teams maintain full autonomy over their documentation. +* Cross-repository links are validated without requiring synchronized builds. ## How it works @@ -33,30 +33,30 @@ This distributed approach provides several key advantages: ### Link validation -* **Outbound links** - Validate links to other repositories ahead of time, even during local `docs-builder` builds -* **Inbound links** - Know when changes to your documentation would break links from other repositories +* **Outbound links** - Validate links to other repositories ahead of time, even during local `docs-builder` builds. +* **Inbound links** - Know when changes to your documentation would break links from other repositories. ### Resilient builds -* **Isolation** - Documentation errors in one repository won't affect builds of other repositories -* **Fallback mechanism** - When a repository has build failures on its integration branch, the assembler falls back to the last known good commit -* **Snapshot builds** - Assembled builds only use commits that successfully produced a Link Index +* **Isolation** - Documentation errors in one repository won't affect builds of other repositories. +* **Fallback mechanism** - When a repository has build failures on its integration branch, the assembler falls back to the last known good commit. +* **Snapshot builds** - Assembled builds only use commits that successfully produced a Link Index. ### Independent iteration -* Teams can iterate on their documentation independently -* No coordination required for routine updates -* Faster feedback loops for documentation changes +* Teams can iterate on their documentation independently. +* No coordination required for routine updates. +* Faster feedback loops for documentation changes. ## Architecture components The distributed documentation system relies on several key components: -* [Link Index](link-index.md) - Per-repository file of linkable resources -* [Link Service](link-service.md) - Central storage for Link Index files -* [Link Catalog](link-catalog.md) - Catalog of all available Link Index files -* [Outbound Cross-links](outbound-cross-links.md) - Links from one repository to another -* [Inbound Cross-links](inbound-cross-links.md) - Links from other repositories to yours +* [Link Index](link-index.md) - Per-repository file of linkable resources. +* [Link Service](link-service.md) - Central storage for Link Index files. +* [Link Catalog](link-catalog.md) - Catalog of all available Link Index files. +* [Outbound Cross-links](outbound-cross-links.md) - Links from one repository to another. +* [Inbound Cross-links](inbound-cross-links.md) - Links from other repositories to yours. ## Local development diff --git a/docs/building-blocks/documentation-set-navigation.md b/docs/building-blocks/documentation-set-navigation.md index 40cbac2e3..68860584c 100644 --- a/docs/building-blocks/documentation-set-navigation.md +++ b/docs/building-blocks/documentation-set-navigation.md @@ -10,10 +10,10 @@ navigation_title: Documentation set navigation Documentation set navigation allows repository maintainers to: -* **Organize content** - Define the logical structure of their documentation -* **Control hierarchy** - Determine which pages are nested under others -* **Create sections** - Group related content together -* **Maintain autonomy** - Structure documentation independently of other repositories +* **Organize content** - Define the logical structure of their documentation. +* **Control hierarchy** - Determine which pages are nested under others. +* **Create sections** - Group related content together. +* **Maintain autonomy** - Structure documentation independently of other repositories. ## Basic structure @@ -121,10 +121,10 @@ The folder name `development` is not repeated in the `toc.yml` file. This allows ### Benefits of separate toc.yml files -* **Modularity** - Each section can be maintained independently -* **Cleaner docset.yml** - Keep the main file focused and readable -* **Easier refactoring** - Rename folders without updating TOC references -* **Team ownership** - Different teams can manage different TOC sections +* **Modularity** - Each section can be maintained independently. +* **Cleaner docset.yml** - Keep the main file focused and readable. +* **Easier refactoring** - Rename folders without updating TOC references. +* **Team ownership** - Different teams can manage different TOC sections. ## File paths @@ -163,8 +163,8 @@ contained in its file. When building [assembled documentation](assembled-documentation.md), the documentation set navigation becomes a component of the [global navigation](global-navigation.md): -* **Documentation set navigation** defines the structure **within** a repository -* **Global navigation** defines **how repositories are organized** relative to each other +* **Documentation set navigation** defines the structure **within** a repository. +* **Global navigation** defines **how repositories are organized** relative to each other. Named `toc` sections in `docset.yml` can be referenced and reorganized in the global `navigation.yml` file without affecting the documentation set's internal structure. @@ -172,9 +172,9 @@ Named `toc` sections in `docset.yml` can be referenced and reorganized in the gl ### Keep it organized -* Group related content in folders -* Use descriptive folder and file names -* Maintain a logical hierarchy +* Group related content in folders. +* Use descriptive folder and file names. +* Maintain a logical hierarchy. The folder names and hierarchy are reflected directly in the URL structure. @@ -231,6 +231,6 @@ These names will end up in the URL structure of the published documentation ## Related concepts -* [Global Navigation](global-navigation.md) - How documentation sets are organized in assembled documentation -* [Content Set Configuration](../configure/content-set/index.md) - Complete `docset.yml` reference -* [Navigation Configuration](../configure/content-set/navigation.md) - Detailed navigation options +* [Global Navigation](global-navigation.md) - How documentation sets are organized in assembled documentation. +* [Content Set Configuration](../configure/content-set/index.md) - Complete `docset.yml` reference. +* [Navigation Configuration](../configure/content-set/navigation.md) - Detailed navigation options. diff --git a/docs/building-blocks/documentation-set.md b/docs/building-blocks/documentation-set.md index 53da16a48..4446fafe4 100644 --- a/docs/building-blocks/documentation-set.md +++ b/docs/building-blocks/documentation-set.md @@ -10,17 +10,17 @@ A **documentation set** is a single folder containing the documentation of a sin At a minimum, a documentation set folder must contain: -* `docset.yml` - The configuration file that defines the structure and metadata of the documentation set -* `index.md` - The entry point or landing page for the documentation set +* `docset.yml` - The configuration file that defines the structure and metadata of the documentation set. +* `index.md` - The entry point or landing page for the documentation set. ## Purpose Documentation sets allow each repository to maintain its own documentation independently. Each set can be: -* Built independently -* Versioned separately -* Maintained by different teams -* Published to its own schedule +* Built independently. +* Versioned separately. +* Maintained by different teams. +* Published to its own schedule. ## Structure @@ -45,5 +45,5 @@ The `docset.yml` file controls how the documentation set is structured and built ## Related concepts -* [Assembled Documentation](assembled-documentation.md) - How multiple documentation sets are combined -* [Link Index](link-index.md) - How documentation sets publish their linkable resources +* [Assembled Documentation](assembled-documentation.md) - How multiple documentation sets are combined. +* [Link Index](link-index.md) - How documentation sets publish their linkable resources. diff --git a/docs/building-blocks/global-navigation.md b/docs/building-blocks/global-navigation.md index cb86efe55..ffca30ac8 100644 --- a/docs/building-blocks/global-navigation.md +++ b/docs/building-blocks/global-navigation.md @@ -10,10 +10,10 @@ navigation_title: Global navigation Global navigation enables: -* **Unified experience** - Present documentation from multiple repositories as a cohesive whole -* **Flexible organization** - Arrange documentation by product, feature, or audience rather than by repository -* **Independent evolution** - Reorganize global structure without changing documentation sets -* **Cross-repository grouping** - Combine related content from different repositories +* **Unified experience** - Present documentation from multiple repositories as a cohesive whole. +* **Flexible organization** - Arrange documentation by product, feature, or audience rather than by repository. +* **Independent evolution** - Reorganize global structure without changing documentation sets. +* **Cross-repository grouping** - Combine related content from different repositories. ## Configuration @@ -23,9 +23,9 @@ Global navigation is defined in the `navigation.yml` file, which is part of the Global navigation has specific restrictions: -* **It may only link to `toc.yml` or `docset.yml` files** - You cannot reference individual markdown files -* **Uses crosslink syntax** - References to other repositories use the `repository://` syntax -* **Requires all TOC sections** - Dangling TOC sections are not allowed +* **It may only link to `toc.yml` or `docset.yml` files** - You cannot reference individual markdown files. +* **Uses crosslink syntax** - References to other repositories use the `repository://` syntax. +* **Requires all TOC sections** - Dangling TOC sections are not allowed. ## Basic structure @@ -82,9 +82,9 @@ You must explicitly provide a URL path prefix when including a `toc`. ``` This allows you to: -* Group content from different repositories under a common path -* Avoid URL conflicts when combining repositories -* Create product-specific URL structures +* Group content from different repositories under a common path. +* Avoid URL conflicts when combining repositories. +* Create product-specific URL structures. ## Reorganization independence @@ -115,10 +115,10 @@ All `toc` sections must be linked in `navigation.yml`. **Dangling `toc` sections are not allowed** and the assembler build will report an error if it finds any. This ensures: -* No content is accidentally excluded from the site -* Navigation references are always valid -* Documentation coverage is complete -* Every TOC section defined in a `docset.yml` appears somewhere in the global navigation +* No content is accidentally excluded from the site. +* Navigation references are always valid. +* Documentation coverage is complete. +* Every TOC section defined in a `docset.yml` appears somewhere in the global navigation. ### Example of validation @@ -147,10 +147,10 @@ If either is missing, the build will fail with an error about dangling TOC secti When building assembled documentation, `docs-builder` validates: -* All referenced TOC sections exist -* No TOC sections are dangling (unreferenced) -* Path prefixes don't conflict -* Crosslink references resolve correctly +* All referenced TOC sections exist. +* No TOC sections are dangling (unreferenced). +* Path prefixes don't conflict. +* Crosslink references resolve correctly. Validation errors will cause the assembler build to fail. @@ -186,8 +186,8 @@ During an assembler build: ## Related concepts -* [Documentation Set Navigation](documentation-set-navigation.md) - How individual repositories organize their content -* [Assembled Documentation](assembled-documentation.md) - The build process that uses global navigation -* [Site Configuration](../configure/site/index.md) - Complete site configuration reference -* [Navigation Configuration](../configure/site/navigation.md) - Detailed navigation.yml reference -* [Cross-link syntax](../syntax/links.md) - Understanding the repository:// syntax +* [Documentation Set Navigation](documentation-set-navigation.md) - How individual repositories organize their content. +* [Assembled Documentation](assembled-documentation.md) - The build process that uses global navigation. +* [Site Configuration](../configure/site/index.md) - Complete site configuration reference. +* [Navigation Configuration](../configure/site/navigation.md) - Detailed navigation.yml reference. +* [Cross-link syntax](../syntax/links.md) - Understanding the `://` syntax. diff --git a/docs/building-blocks/inbound-cross-links.md b/docs/building-blocks/inbound-cross-links.md index 0cedfb17f..de132fc9b 100644 --- a/docs/building-blocks/inbound-cross-links.md +++ b/docs/building-blocks/inbound-cross-links.md @@ -10,9 +10,9 @@ navigation_title: Inbound cross-links Inbound cross-link validation allows you to: -* **Detect breaking changes** - Know when renaming or deleting a file will break links from other repositories -* **Prevent regressions** - Avoid publishing changes that break documentation elsewhere -* **Coordinate changes** - Understand dependencies before making structural changes +* **Detect breaking changes** - Know when renaming or deleting a file will break links from other repositories. +* **Prevent regressions** - Avoid publishing changes that break documentation elsewhere. +* **Coordinate changes** - Understand dependencies before making structural changes. ## How it works @@ -99,22 +99,22 @@ redirects: If you need to make a breaking change: -1. Run inbound link validation to identify affected repositories -2. File issues or notify maintainers of affected repositories -3. Coordinate the change timing -4. Provide redirect mappings or alternative URLs +1. Run inbound link validation to identify affected repositories. +2. File issues or notify maintainers of affected repositories. +3. Coordinate the change timing, +4. Provide redirect mappings or alternative URLs. ### Validate before merging Make inbound link validation part of your review process: -* Run validation locally before creating a PR -* Include validation in CI checks -* Review validation results before merging +* Run validation locally before creating a PR. +* Include validation in CI checks. +* Review validation results before merging. ## Related concepts -* [Outbound Cross-links](outbound-cross-links.md) - Links from your documentation to others -* [Link Index](link-index.md) - How your linkable resources are tracked -* [Link Service](link-service.md) - Where inbound link information is stored -* [Distributed Documentation](distributed-documentation.md) - The architecture enabling this validation +* [Outbound Cross-links](outbound-cross-links.md) - Links from your documentation to others. +* [Link Index](link-index.md) - How your linkable resources are tracked. +* [Link Service](link-service.md) - Where inbound link information is stored. +* [Distributed Documentation](distributed-documentation.md) - The architecture enabling this validation. diff --git a/docs/building-blocks/link-catalog.md b/docs/building-blocks/link-catalog.md index a77060173..a54a6f526 100644 --- a/docs/building-blocks/link-catalog.md +++ b/docs/building-blocks/link-catalog.md @@ -10,9 +10,9 @@ The **Link Catalog** is a single JSON file that serves as a catalog of all avail The Link Catalog provides: -* **Discovery** - A single file to query for all available documentation across all repositories and branches -* **Efficiency** - Avoid scanning the entire [Link Service](link-service.md) to find available Link Index files -* **Assembler coordination** - The assembler uses this to determine which repositories and versions are available to build +* **Discovery** - A single file to query for all available documentation across all repositories and branches. +* **Efficiency** - Avoid scanning the entire [Link Service](link-service.md) to find available Link Index files. +* **Assembler coordination** - The assembler uses this to determine which repositories and versions are available to build. ## Location @@ -26,14 +26,14 @@ https://elastic-docs-link-index.s3.us-east-2.amazonaws.com/link-index.json The Link Catalog contains: -* List of all organizations (e.g., `elastic`) -* Repositories within each organization (e.g., `elasticsearch`, `kibana`) -* Branches for each repository (e.g., `main`, `8.x`, `7.17`) +* List of all organizations (e.g., `elastic`). +* Repositories within each organization (e.g., `elasticsearch`, `kibana`). +* Branches for each repository (e.g., `main`, `8.x`, `7.17`). * Metadata about each Link Index: - * Last updated timestamp - * Commit SHA that produced the Link Index - * ETAG of the Link Index file - * URL to the Link Index file + * Last updated timestamp. + * Commit SHA that produced the Link Index. + * ETAG of the Link Index file. + * URL to the Link Index file. ## Maintenance @@ -66,13 +66,13 @@ During a documentation build: ## Benefits -* **Single source of truth** - One file to check for all available documentation -* **Performance** - Fast lookup without scanning the entire Link Service -* **Automation** - Maintained automatically via Lambda functions -* **Resilience** - Represents only successful builds with valid Link Indexes +* **Single source of truth** - One file to check for all available documentation. +* **Performance** - Fast lookup without scanning the entire Link Service. +* **Automation** - Maintained automatically via Lambda functions. +* **Resilience** - Represents only successful builds with valid Link Indexes. ## Related concepts -* [Link Service](link-service.md) - Where the Link Catalog is stored -* [Link Index](link-index.md) - The files cataloged by the Link Catalog -* [Assembled Documentation](assembled-documentation.md) - Uses the Link Catalog to coordinate builds +* [Link Service](link-service.md) - Where the Link Catalog is stored. +* [Link Index](link-index.md) - The files cataloged by the Link Catalog. +* [Assembled Documentation](assembled-documentation.md) - Uses the Link Catalog to coordinate builds. diff --git a/docs/building-blocks/link-index.md b/docs/building-blocks/link-index.md index a0b2a4a96..363a672d5 100644 --- a/docs/building-blocks/link-index.md +++ b/docs/building-blocks/link-index.md @@ -10,18 +10,18 @@ A **Link Index** is a JSON file (`links.json`) that contains all the linkable re The Link Index enables: -* **Cross-repository linking** - Other documentation sets can link to your content -* **Link validation** - Validate that links to your content are correct -* **Inbound link detection** - Know when other repositories link to your content -* **Distributed builds** - Build documentation independently while maintaining link integrity +* **Cross-repository linking** - Other documentation sets can link to your content. +* **Link validation** - Validate that links to your content are correct. +* **Inbound link detection** - Know when other repositories link to your content. +* **Distributed builds** - Build documentation independently while maintaining link integrity. ## Structure Each repository branch gets its own Link Index file in the [Link Service](link-service.md), organized by: -* **Organization** - e.g., `elastic` -* **Repository** - e.g., `elasticsearch` -* **Branch** - e.g., `main`, `8.x`, `7.17` +* **Organization** - e.g., `elastic`. +* **Repository** - e.g., `elasticsearch`. +* **Branch** - e.g., `main`, `8.x`, `7.17`. ## Example @@ -29,11 +29,11 @@ View [Elasticsearch's main branch Link Index](https://elastic-docs-link-index.s3 The Link Index contains: -* All documentation pages in the repository -* Headings within those pages -* Anchors and linkable elements -* Version information -* Metadata about the build +* All documentation pages in the repository. +* Headings within those pages. +* Anchors and linkable elements. +* Version information. +* Metadata about the build. ## Generation @@ -80,7 +80,7 @@ https://elastic-docs-link-index.s3.us-east-2.amazonaws.com/{org}/{repo}/{branch} ## Related concepts -* [Link Service](link-service.md) - Where Link Index files are stored -* [Link Catalog](link-catalog.md) - Catalog of all Link Index files -* [Outbound Cross-links](outbound-cross-links.md) - Links that use the Link Index -* [Inbound Cross-links](inbound-cross-links.md) - Links to resources in the Link Index +* [Link Service](link-service.md) - Where Link Index files are stored. +* [Link Catalog](link-catalog.md) - Catalog of all Link Index files. +* [Outbound Cross-links](outbound-cross-links.md) - Links that use the Link Index. +* [Inbound Cross-links](inbound-cross-links.md) - Links to resources in the Link Index. diff --git a/docs/building-blocks/link-service.md b/docs/building-blocks/link-service.md index 7d173f212..691aad532 100644 --- a/docs/building-blocks/link-service.md +++ b/docs/building-blocks/link-service.md @@ -7,7 +7,7 @@ navigation_title: Link service The **Link Service** is the central location that stores: * All [Link Index](link-index.md) files for all the repositories and branches that are published. -* The [Link Catalog](link-catalog.md), a single JSON file that contains references to all the `Link Index` files. +* The [Link Catalog](link-catalog.md), a single JSON file that contains references to all the `Link Index` files. We only have one link service today for all public documentation. @@ -17,18 +17,18 @@ We only have one link service today for all public documentation. The Link Service is implemented as: -* **Storage** - An S3 bucket -* **CDN** - CloudFront fronting the S3 bucket for fast global access -* **Access** - Publicly accessible for read operations +* **Storage** - An S3 bucket. +* **CDN** - CloudFront fronting the S3 bucket for fast global access. +* **Access** - Publicly accessible for read operations. ## Purpose The Link Service enables: -* **Distributed validation** - Any documentation build can validate cross-repository links without cloning all repositories -* **Link discovery** - Find what resources are available in other repositories -* **Build resilience** - Assembler builds can reference the last known good state of each repository -* **Decentralized publishing** - Each repository publishes its own Link Index independently +* **Distributed validation** - Any documentation build can validate cross-repository links without cloning all repositories. +* **Link discovery** - Find what resources are available in other repositories. +* **Build resilience** - Assembler builds can reference the last known good state of each repository. +* **Decentralized publishing** - Each repository publishes its own Link Index independently. ## URL structure @@ -39,8 +39,8 @@ https://elastic-docs-link-index.s3.us-east-2.amazonaws.com/{org}/{repo}/{branch} ``` For example: -* [Elasticsearch main branch](https://elastic-docs-link-index.s3.us-east-2.amazonaws.com/elastic/elasticsearch/main/links.json) -* [Kibana main branch](https://elastic-docs-link-index.s3.us-east-2.amazonaws.com/elastic/kibana/main/links.json) +* [Elasticsearch main branch](https://elastic-docs-link-index.s3.us-east-2.amazonaws.com/elastic/elasticsearch/main/links.json). +* [Kibana main branch](https://elastic-docs-link-index.s3.us-east-2.amazonaws.com/elastic/kibana/main/links.json). ## Publishing process @@ -55,12 +55,12 @@ When a documentation build completes successfully on a default integration branc During both local and CI builds, `docs-builder`: -* Fetches relevant Link Index files from the Link Service -* Validates outbound cross-links against these indexes -* Validates that inbound cross-links won't be broken by local changes +* Fetches relevant Link Index files from the Link Service. +* Validates outbound cross-links against these indexes. +* Validates that inbound cross-links won't be broken by local changes. ## Related concepts -* [Link Index](link-index.md) - The files stored in the Link Service -* [Link Catalog](link-catalog.md) - The catalog of all Link Index files -* [Distributed Documentation](distributed-documentation.md) - Why the Link Service exists +* [Link Index](link-index.md) - The files stored in the Link Service. +* [Link Catalog](link-catalog.md) - The catalog of all Link Index files. +* [Distributed Documentation](distributed-documentation.md) - Why the Link Service exists. diff --git a/docs/building-blocks/outbound-cross-links.md b/docs/building-blocks/outbound-cross-links.md index 07353b1e3..58fe00bfd 100644 --- a/docs/building-blocks/outbound-cross-links.md +++ b/docs/building-blocks/outbound-cross-links.md @@ -10,10 +10,10 @@ navigation_title: Outbound cross-links Outbound cross-links allow you to: -* Link to documentation in other repositories -* Maintain those links even as the target repository evolves -* Validate links during local builds -* Get warnings if target content is moved or deleted +* Link to documentation in other repositories. +* Maintain those links even as the target repository evolves. +* Validate links during local builds. +* Get warnings if target content is moved or deleted. ## Syntax @@ -50,8 +50,8 @@ When `docs-builder` encounters a cross-link: During a build, `docs-builder`: -* **Validates immediately** - Checks all outbound cross-links against locally fetched [Link Index](link-index.md) files -* **Reports errors** - Reports errors about broken links before you publish +* **Validates immediately** - Checks all outbound cross-links against locally fetched [Link Index](link-index.md) files. +* **Reports errors** - Reports errors about broken links before you publish. ## Configuration @@ -86,9 +86,9 @@ The explicit opt-in prevents each repository build having the fetch all the link ``` The cross-link syntax is resilient to: -* URL structure changes -* File moves (if redirects are configured) -* Version differences +* URL structure changes. +* File moves (if redirects are configured). +* Version differences. ### Link to headings @@ -100,6 +100,6 @@ You can link to specific headings within a page: ## Related concepts -* [Inbound Cross-links](inbound-cross-links.md) - Links from other repositories to yours -* [Link Index](link-index.md) - How cross-links are resolved -* [Links syntax](../syntax/links.md) - Complete link syntax documentation +* [Inbound Cross-links](inbound-cross-links.md) - Links from other repositories to yours. +* [Link Index](link-index.md) - How cross-links are resolved. +* [Links syntax](../syntax/links.md) - Complete link syntax documentation. diff --git a/docs/cli/assembler/assemble.md b/docs/cli/assembler/assemble.md index e8c5bcc8a..13058aa0c 100644 --- a/docs/cli/assembler/assemble.md +++ b/docs/cli/assembler/assemble.md @@ -30,7 +30,7 @@ docs-builder assembler serve Where this command really shines is when you want to create a temporary workspace folder to validate: -* changes to [site wide configuration](../../configure/site/index.md) +* changes to [site wide configuration](../../configure/site/index.md). * changes to one or more repositories and their effect on the assembler build. To do that inside an empty folder, call: diff --git a/docs/cli/assembler/assembler-config-init.md b/docs/cli/assembler/assembler-config-init.md index cc23be77a..d1c5e4eb4 100644 --- a/docs/cli/assembler/assembler-config-init.md +++ b/docs/cli/assembler/assembler-config-init.md @@ -8,9 +8,9 @@ Sources the configuration from [The `config` folder on the `main` branch of `doc By default, the configuration is placed in a special application folder as its main usages is to be used by CI environments. -* OSX: `~/Library/Application Support/docs-builder` [NSApplicationSupportDirectory](https://developer.apple.com/documentation/foundation/filemanager/searchpathdirectory/applicationsupportdirectory) -* Linux: `~/.config/docs-builder` -* {icon}`logo_windows` Windows: `%APPDATA%\docs-builder` +* OSX: `~/Library/Application Support/docs-builder` [NSApplicationSupportDirectory](https://developer.apple.com/documentation/foundation/filemanager/searchpathdirectory/applicationsupportdirectory). +* Linux: `~/.config/docs-builder`. +* {icon}`logo_windows` Windows: `%APPDATA%\docs-builder`. You can also use the `--local` option to save the configuration locally in the current working directory. This exposes a great way to assemble the full documentation locally in an empty directory. diff --git a/docs/cli/assembler/assembler-content-source-match.md b/docs/cli/assembler/assembler-content-source-match.md index 67781a42a..ab955ff32 100644 --- a/docs/cli/assembler/assembler-content-source-match.md +++ b/docs/cli/assembler/assembler-content-source-match.md @@ -6,9 +6,9 @@ navigation_title: "content-source match" This command is used to match a repository and branch to a content source it will emit the following `$GITHUB_OUTPUT`: -* `content-source-match` - whether the branch is a configured content source -* `content-source-next` - whether the branch is the next content source -* `content-source-current` - whether the branch is the current content source +* `content-source-match` - whether the branch is a configured content source. +* `content-source-next` - whether the branch is the next content source. +* `content-source-current` - whether the branch is the current content source. * `content-source-speculative` - whether the branch is a speculative content source. #### Speculative builds diff --git a/docs/cli/docset/build.md b/docs/cli/docset/build.md index d300c6ee9..21b29f687 100644 --- a/docs/cli/docset/build.md +++ b/docs/cli/docset/build.md @@ -4,8 +4,8 @@ Builds a local documentation set folder. Repeated invocations will do incremental builds of only the changed files unless: -* The base branch has changed -* The state file in the output folder has been removed +* The base branch has changed. +* The state file in the output folder has been removed. * The `--force` option is specified. ## Usage diff --git a/docs/cli/docset/diff-validate.md b/docs/cli/docset/diff-validate.md index 02c97d84b..59513c5f9 100644 --- a/docs/cli/docset/diff-validate.md +++ b/docs/cli/docset/diff-validate.md @@ -4,7 +4,7 @@ Gathers the local changes by inspecting the git log, stashed and unstashed chang It currently validates the following: -* Ensures that renames and deletions are reflected in [redirects.yml](../../contribute/redirects.md) +* Ensures that renames and deletions are reflected in [redirects.yml](../../contribute/redirects.md). ## Usage diff --git a/docs/configure/content-set/index.md b/docs/configure/content-set/index.md index f813d3de2..1b02e4c17 100644 --- a/docs/configure/content-set/index.md +++ b/docs/configure/content-set/index.md @@ -15,6 +15,6 @@ A content set in `docs-builder` is equivalent to an AsciiDoc book. At this level ## Learn more -* [File structure](./file-structure.md) -* [Navigation](./navigation.md) -* [Attributes](./attributes.md) \ No newline at end of file +* [File structure](./file-structure.md). +* [Navigation](./navigation.md). +* [Attributes](./attributes.md). \ No newline at end of file diff --git a/docs/configure/index.md b/docs/configure/index.md index bdc318f7d..4f7a4b298 100644 --- a/docs/configure/index.md +++ b/docs/configure/index.md @@ -20,8 +20,8 @@ When working with `docs-builder`, there are three levels at which you can config At the site level, you can configure: -* Content sources: where files live -* Global navigation: how navigations are compiled and presented to users +* Content sources: where files live. +* Global navigation: how navigations are compiled and presented to users. [Site configuration](./site/index.md) @@ -29,8 +29,8 @@ At the site level, you can configure: At the content set level, you can configure: -* Content-set-level and sub-content-set-level navigation: how smaller groups of files are organized and presented to users -* Attributes: variables that will be substituted at build-time for pre-defined values +* Content-set-level and sub-content-set-level navigation: how smaller groups of files are organized and presented to users. +* Attributes: variables that will be substituted at build-time for pre-defined values. [Content-set configuration](./content-set/index.md) @@ -38,6 +38,6 @@ At the content set level, you can configure: At the page level, you can configure: -* Frontmatter that influences on-page UX to benefit the user in some way +* Frontmatter that influences on-page UX to benefit the user in some way. [Page configuration](./page.md) \ No newline at end of file diff --git a/docs/configure/site/content.md b/docs/configure/site/content.md index ea344a713..4737cb65f 100644 --- a/docs/configure/site/content.md +++ b/docs/configure/site/content.md @@ -6,10 +6,10 @@ navigation_title: assembler.yml The [`assembler.yml`](https://github.com/elastic/docs-builder/blob/main/config/assembler.yml) file defines the global documentation site: -* `environments` -* `shared_configuration` -* narrative repository configuration -* reference repository configurations +* `environments`. +* `shared_configuration`. +* narrative repository configuration. +* reference repository configurations. ## `environments` diff --git a/docs/contribute/cumulative-docs/badge-placement.md b/docs/contribute/cumulative-docs/badge-placement.md index abd5cdc5e..f9d127e3a 100644 --- a/docs/contribute/cumulative-docs/badge-placement.md +++ b/docs/contribute/cumulative-docs/badge-placement.md @@ -26,11 +26,11 @@ version and deployment type differences in your docs: % Source: Brandon's PR review comment At a high level, you should follow these badge placement principles: -* Place badges where they're most visible but least disruptive to reading flow -* Consider scanning patterns - readers often scan for relevant information -* Ensure badges don't break the natural flow of content -* Use consistent placement patterns within similar content types -* Consider visual grouping - readers must naturally associate the badge with its corresponding content, no more, no less +* Place badges where they're most visible but least disruptive to reading flow. +* Consider scanning patterns - readers often scan for relevant information. +* Ensure badges don't break the natural flow of content. +* Use consistent placement patterns within similar content types. +* Consider visual grouping - readers must naturally associate the badge with its corresponding content, no more, no less. ## Placement in specific elements diff --git a/docs/contribute/cumulative-docs/example-scenarios.md b/docs/contribute/cumulative-docs/example-scenarios.md index c780a0558..31ef9442a 100644 --- a/docs/contribute/cumulative-docs/example-scenarios.md +++ b/docs/contribute/cumulative-docs/example-scenarios.md @@ -34,8 +34,8 @@ page level in the frontmatter, use section-level `applies_to` badges. + + site:// + path_prefix: "docs" + + + + api/ (docs-content://api) + path_prefix: "api" + + + + api/ (elastic-project://api) [re-homed] + path_prefix: "api/elastic-project" + + + + rest/ + + + + overview.md + /docs/api/elastic-project/rest/overview + + + + overview.md + /docs/api/elastic-project/overview + + + + guides/ (docs-content://guides) + path_prefix: "guides" + + + + guides/ (elastic-project://guides) [re-homed] + path_prefix: "guides/elastic-project" + + + + getting-started.md + /docs/guides/elastic-project/getting-started + + + + elastic-client (elastic-client://) [re-homed] + path_prefix: "clients" + + + + elastic-client-node (elastic-client-node://) [re-homed] + path_prefix: "clients/node" + + + + index.md + /docs/clients/node/ + + + + elastic-client-dotnet (elastic-client-dotnet://) [re-homed] + path_prefix: "clients/dotnet" + + + + index.md + /docs/clients/dotnet/ + + + + elastic-client-java (elastic-client-java://) [re-homed] + path_prefix: "clients/java" + + + + index.md + /docs/clients/java/ + + + + index.md + /docs/clients/ + diff --git a/docs/development/navigation/images/assembler-build.png b/docs/development/navigation/images/assembler-build.png new file mode 100644 index 0000000000000000000000000000000000000000..d482870d899d2dd397cd349cdff1d2cde21ef2db GIT binary patch literal 112941 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�e^n88&jgDmewdK5WeYM1U8d;hR)xRDR8>S0~d3 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 literal 0 HcmV?d00001 diff --git a/docs/development/navigation/images/bullet-documentation-set-navigation.svg b/docs/development/navigation/images/bullet-documentation-set-navigation.svg new file mode 100644 index 000000000..5e00bd6d8 --- /dev/null +++ b/docs/development/navigation/images/bullet-documentation-set-navigation.svg @@ -0,0 +1,3 @@ + + + diff --git a/docs/development/navigation/images/bullet-file-navigation-leaf.svg b/docs/development/navigation/images/bullet-file-navigation-leaf.svg new file mode 100644 index 000000000..7d2b68413 --- /dev/null +++ b/docs/development/navigation/images/bullet-file-navigation-leaf.svg @@ -0,0 +1,3 @@ + + + diff --git a/docs/development/navigation/images/bullet-folder-navigation.svg b/docs/development/navigation/images/bullet-folder-navigation.svg new file mode 100644 index 000000000..45182fc72 --- /dev/null +++ b/docs/development/navigation/images/bullet-folder-navigation.svg @@ -0,0 +1,3 @@ + + + diff --git a/docs/development/navigation/images/bullet-site-navigation.svg b/docs/development/navigation/images/bullet-site-navigation.svg new file mode 100644 index 000000000..697d99550 --- /dev/null +++ b/docs/development/navigation/images/bullet-site-navigation.svg @@ -0,0 +1,3 @@ + + + diff --git a/docs/development/navigation/images/bullet-table-of-contents-navigation.svg b/docs/development/navigation/images/bullet-table-of-contents-navigation.svg new file mode 100644 index 000000000..a02f590d0 --- /dev/null +++ b/docs/development/navigation/images/bullet-table-of-contents-navigation.svg @@ -0,0 +1,3 @@ + + + diff --git a/docs/development/navigation/images/isolated-build-tree.svg b/docs/development/navigation/images/isolated-build-tree.svg new file mode 100644 index 000000000..55d0a0b50 --- /dev/null +++ b/docs/development/navigation/images/isolated-build-tree.svg @@ -0,0 +1,49 @@ + + + + + + + + elastic-project:// (docset.yml) + path_prefix: "documentation" + + + + api/ (api/toc.yml) + + + + rest/ + + + + overview.md + /documentation/api/rest/overview + + + + overview.md + /documentation/api/overview + + + + guides/ (guides/toc.yml) + + + + getting-started.md + /documentation/guides/getting-started + + + + index.md + /documentation/ + diff --git a/docs/development/navigation/images/isolated-build.png b/docs/development/navigation/images/isolated-build.png new file mode 100644 index 0000000000000000000000000000000000000000..8d4ae7e4410d30f989de20f8cd5b587b2bd896bf GIT binary patch literal 42909 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�k%8O2fvvPFB;=lx}`*9B0eFG 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¶*^3m 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 Mh0s$gQLza7f6y){vj6}9 literal 0 HcmV?d00001 diff --git a/docs/development/navigation/navigation.md b/docs/development/navigation/navigation.md new file mode 100644 index 000000000..ab139cf8b --- /dev/null +++ b/docs/development/navigation/navigation.md @@ -0,0 +1,65 @@ +# Navigation + +## File Navigation types + +### Isolated builds + +When building a signle repository's documentation, we are building the table of contents defined in `docset.yml` the root of which is represented by ![DocumentationSetNavigation](images/bullet-documentation-set-navigation.svg) `DocumentationSetNavigation` + +The table of contents is composed of a number of `NavigationItem`s. + +- `folder:` ![FolderNavigation](images/bullet-folder-navigation.svg) `FolderNavigation` +- `file:` ![FileNavigationLeaf](images/bullet-file-navigation-leaf.svg) `FileNavigationLeaf` + +`docset.yml` may break up it's table of contents into multiple sub navigation's using nested `toc.yml` files. + +- `toc:` ![TableOfContentsNavigation](images/bullet-table-of-contents-navigation.svg) TableOfContentsNavigation + +### Assembler builds + +The assembler build takes multiple `Isolated Build` navigations and recomposes it into a single ![SiteNavigation](images//bullet-site-navigation.svg) `SiteNavigation` navigation. + +This navigation is defined in [`navigation.yml`](https://github.com/elastic/docs-builder/blob/main/config/navigation.yml) + +An assembler build can only reference: + +- ![DocumentationSetNavigation](images/bullet-documentation-set-navigation.svg) `DocumentationSetNavigation` (using `://` crosslink) +- ![TableOfContentsNavigation](images/bullet-table-of-contents-navigation.svg) `TableOfContentsNavigation` (using `://` crosslink) + +A new special root is created for asssembler builds: + +- ![SiteNavigation](images//bullet-site-navigation.svg) `SiteNavigation` + +## A Visual Example + +### Isolated builds + +Imagine we have the following `docset.yml` that defines two nested `toc.yml` files. + +![Isolated Build](images/isolated-build-tree.svg) + +### Assembler builds + +Now we can break that navigation up into multiple sections of the wider site navigation. + +- ![TableOfContentsNavigation](images/bullet-table-of-contents-navigation.svg)`docs-content://api` + - ![TableOfContentsNavigation](images/bullet-table-of-contents-navigation.svg)**`elastic-project://api`** +- ![TableOfContentsNavigation](images/bullet-table-of-contents-navigation.svg)`docs-content://guides` + - ![TableOfContentsNavigation](images/bullet-table-of-contents-navigation.svg)**`elastic-project://guides`** +- ![DocumentationSetNavigation](images/bullet-documentation-set-navigation.svg) `elastic-client://` + - ![DocumentationSetNavigation](images/bullet-documentation-set-navigation.svg) `elastic-client-node://` + - ![DocumentationSetNavigation](images/bullet-documentation-set-navigation.svg) `elastic-client-dotnet://` + - ![DocumentationSetNavigation](images/bullet-documentation-set-navigation.svg) `elastic-client-java://` + + +![DocumentationSetNavigation](images/bullet-documentation-set-navigation.svg) `DocumentationSetNavigation` may arbitrary nest other +![TableOfContentsNavigation](images/bullet-table-of-contents-navigation.svg) `TableOfContentsNavigation` and visa versa. + +The only requirement is that each node in the navigation **MUST** define a unique `path_prefix`. + +#### A fully resolved navigation. + +When resolving the navigation, all the child navigation items will be included and the url will dynamically be `re-homed` to the root ![DocumentationSetNavigation](images/bullet-documentation-set-navigation.svg) `DocumentationSetNavigation` or ![TableOfContentsNavigation](images/bullet-table-of-contents-navigation.svg) `TableOfContentsNavigation` that defines it. + +![Assembler Build](images/assembler-build-tree.svg) + diff --git a/docs/development/toc.yml b/docs/development/toc.yml index 8e571cd12..de74b4a73 100644 --- a/docs/development/toc.yml +++ b/docs/development/toc.yml @@ -3,4 +3,7 @@ toc: - folder: ingest children: - file: index.md + - folder: navigation + children: + - file: navigation.md - toc: link-validation From 6dcd0728fdd71b3afa8b71dc4dd0ded5ab3c2279 Mon Sep 17 00:00:00 2001 From: Martijn Laarman Date: Mon, 3 Nov 2025 13:14:44 +0100 Subject: [PATCH 153/171] moar docs --- docs/development/navigation/README.md | 331 +++++++++ .../navigation/assembler-process.md | 658 ++++++++++++++++++ .../navigation/first-principles.md | 222 ++++++ .../navigation/home-provider-architecture.md | 517 ++++++++++++++ docs/development/navigation/navigation.md | 89 +-- docs/development/navigation/node-types.md | 640 +++++++++++++++++ .../navigation/two-phase-loading.md | 314 +++++++++ .../navigation/visual-walkthrough.md | 545 +++++++++++++++ docs/development/toc.yml | 6 + 9 files changed, 3279 insertions(+), 43 deletions(-) create mode 100644 docs/development/navigation/README.md create mode 100644 docs/development/navigation/assembler-process.md create mode 100644 docs/development/navigation/first-principles.md create mode 100644 docs/development/navigation/home-provider-architecture.md create mode 100644 docs/development/navigation/node-types.md create mode 100644 docs/development/navigation/two-phase-loading.md create mode 100644 docs/development/navigation/visual-walkthrough.md diff --git a/docs/development/navigation/README.md b/docs/development/navigation/README.md new file mode 100644 index 000000000..68cdf4e64 --- /dev/null +++ b/docs/development/navigation/README.md @@ -0,0 +1,331 @@ +# Navigation Documentation + +Welcome to the documentation for `Elastic.Documentation.Navigation`, the library that powers documentation navigation for Elastic's documentation sites. + +## What This Is + +This library builds hierarchical navigation trees for documentation sites with a unique capability: navigation built for isolated repositories can be **efficiently re-homed** during site assembly without rebuilding the entire tree. + +**Why does this matter?** + +Individual documentation teams can build and test their docs in isolation with URLs like `/api/overview/`, then those same docs can be assembled into a unified site with URLs like `/elasticsearch/api/overview/` - with **zero tree reconstruction**. It's an O(1) operation. + +## Documentation Map + +Start with any document based on what you want to learn: + +### 🎯 [navigation.md](navigation.md) - Start Here +**Overview of the navigation system** + +Read this first to understand: +- The two build modes (isolated vs assembler) +- Core concepts at a high level +- Quick introduction to re-homing +- Links to detailed documentation + +### 🎨 [visual-walkthrough.md](visual-walkthrough.md) - See It In Action +**Visual tour with diagrams showing navigation structures** + +Read this to understand: +- What different node types look like in the tree +- How isolated builds differ from assembler builds visually +- How the same content appears with different URLs +- How to split and reorganize documentation across sites +- Common patterns for multi-repository organization +- Includes actual tree diagrams from this repository + +### 🧭 [first-principles.md](first-principles.md) - Design Philosophy +**Core principles that guide the architecture** + +Read this to understand: +- Why two-phase loading (configuration → navigation) +- Why URLs are calculated dynamically, not stored +- Why navigation roots can be re-homed +- Design patterns used (factory, provider, visitor) +- Performance characteristics and invariants + +### 🔄 [two-phase-loading.md](two-phase-loading.md) - The Loading Process +**Deep dive into Phase 1 (configuration) and Phase 2 (navigation)** + +Read this to understand: +- What happens in Phase 1: Configuration resolution +- What happens in Phase 2: Navigation construction +- Why these phases are separate +- Data flow diagrams +- How to test each phase independently + +### 🏠 [home-provider-architecture.md](home-provider-architecture.md) - The Re-homing Magic +**How O(1) re-homing works** + +Read this to understand: +- The problem: naive re-homing requires O(n) tree traversal +- The solution: HomeProvider pattern with indirection +- How `INavigationHomeProvider` and `INavigationHomeAccessor` work +- Why URLs are lazily calculated and cached +- Detailed examples of re-homing in action +- Performance analysis + +**This is the most important technical concept in the system.** + +### 📦 [node-types.md](node-types.md) - Node Type Reference +**Complete reference for every navigation node type** + +Read this to understand: +- All 7 node types in detail: + - **Leaves**: FileNavigationLeaf, CrossLinkNavigationLeaf + - **Nodes**: FolderNavigation, VirtualFileNavigation + - **Roots**: DocumentationSetNavigation, TableOfContentsNavigation, SiteNavigation +- Constructor signatures +- URL calculation for each type +- Factory methods +- Model types (IDocumentationFile) + +### 🔨 [assembler-process.md](assembler-process.md) - Building Unified Sites +**How multiple repositories become one site** + +Read this to understand: +- The assembler build process step-by-step +- How `SiteNavigation` works +- Re-homing in practice during assembly +- Path prefix requirements +- Phantom nodes +- Nested re-homing +- Error handling + +## Suggested Reading Order + +**If you're new to the codebase:** +1. [navigation.md](navigation.md) - Get the overview +2. [visual-walkthrough.md](visual-walkthrough.md) - See it visually +3. [first-principles.md](first-principles.md) - Understand the why +4. [home-provider-architecture.md](home-provider-architecture.md) - Understand the how +5. [node-types.md](node-types.md) - Reference as needed + +**If you're debugging an issue:** +1. [node-types.md](node-types.md) - Find the node type +2. [home-provider-architecture.md](home-provider-architecture.md) - Understand URL calculation +3. [two-phase-loading.md](two-phase-loading.md) - Check which phase + +**If you're adding a feature:** +1. [first-principles.md](first-principles.md) - Ensure design consistency +2. [node-types.md](node-types.md) - See existing patterns +3. [two-phase-loading.md](two-phase-loading.md) - Determine which phase +4. [assembler-process.md](assembler-process.md) - Consider assembler impact + +**If you're optimizing performance:** +1. [home-provider-architecture.md](home-provider-architecture.md) - Understand caching +2. [first-principles.md](first-principles.md) - See performance characteristics +3. [two-phase-loading.md](two-phase-loading.md) - Find expensive operations + +## Key Concepts Summary + +### Two Build Modes + +1. **Isolated Build** + - Single repository + - URLs relative to `/` + - `DocumentationSetNavigation` is the root + - Fast iteration for doc teams + +2. **Assembler Build** + - Multiple repositories + - Custom URL prefixes + - `SiteNavigation` is the root + - Docsets/TOCs are re-homed + +### Two-Phase Loading + +1. **Phase 1: Configuration** (`Elastic.Documentation.Configuration`) + - Parse YAML files + - Resolve all relative paths to absolute paths from docset root + - Validate structure and file references + - Load nested `toc.yml` files + - Output: Fully resolved configuration + +2. **Phase 2: Navigation** (`Elastic.Documentation.Navigation`) + - Build tree from resolved configuration + - Establish parent-child relationships + - Set up home providers + - Calculate navigation indexes + - Output: Complete navigation tree + +### Home Provider Pattern + +The secret to O(1) re-homing: + +```csharp +// Provider defines URL context +public interface INavigationHomeProvider +{ + string PathPrefix { get; } + IRootNavigationItem<...> NavigationRoot { get; } +} + +// Accessor references provider +public interface INavigationHomeAccessor +{ + INavigationHomeProvider HomeProvider { get; set; } +} + +// Nodes calculate URLs from current provider +public string Url => + $"{_homeAccessor.HomeProvider.PathPrefix}/{_relativePath}/"; +``` + +**Re-homing:** +```csharp +// Change provider → all URLs update instantly! +node.HomeProvider = new NavigationHomeProvider("/new-prefix", newRoot); +``` + +### Node Types + +7 types organized by capabilities: + +**Leaves** (no children): +- `FileNavigationLeaf` - Markdown file +- `CrossLinkNavigationLeaf` - External link + +**Nodes** (have children): +- `FolderNavigation` - Directory +- `VirtualFileNavigation` - File with YAML-defined children + +**Roots** (can be re-homed): +- `DocumentationSetNavigation` - Docset root +- `TableOfContentsNavigation` - Nested TOC +- `SiteNavigation` - Assembled site root + +## Code Organization + +The library is organized into: + +### `Elastic.Documentation.Navigation/` +Root namespace - shared types: +- `IDocumentationFile.cs` - Base interface for documentation files +- `NavigationModels.cs` - Common model types (CrossLinkModel, SiteNavigationNoIndexFile) + +### `Elastic.Documentation.Navigation/Isolated/` +Isolated build navigation: +- `DocumentationSetNavigation.cs` - Docset root +- `TableOfContentsNavigation.cs` - Nested TOC +- `FolderNavigation.cs` - Folder nodes +- `FileNavigationLeaf.cs` - File leaves +- `VirtualFileNavigation.cs` - Virtual file nodes +- `CrossLinkNavigationLeaf.cs` - Crosslink leaves +- `DocumentationNavigationFactory.cs` - Factory for creating nodes +- `NavigationArguments.cs` - Constructor argument records +- `NavigationHomeProvider.cs` - Home provider implementation + +### `Elastic.Documentation.Navigation/Assembler/` +Assembler build navigation: +- `SiteNavigation.cs` - Unified site root + +### Supporting Files +- `README.md` - High-level overview (in src/) +- `url-building.md` - URL building rules (in src/) + +## Testing + +Tests are in `tests/Navigation.Tests/`: + +**Isolated build tests:** +- `Isolation/ConstructorTests.cs` - Basic navigation construction +- `Isolation/FileNavigationTests.cs` - File leaf behavior +- `Isolation/FolderIndexFileRefTests.cs` - Folder navigation +- `Isolation/PhysicalDocsetTests.cs` - Real docset loading + +**Assembler build tests:** +- `Assembler/SiteNavigationTests.cs` - Site assembly +- `Assembler/SiteDocumentationSetsTests.cs` - Multiple docsets +- `Assembler/ComplexSiteNavigationTests.cs` - Complex scenarios + +**Test pattern:** +```csharp +[Fact] +public void FeatureUnderTest_Scenario_ExpectedBehavior() +{ + // Arrange: Create mock file system and configuration + var fileSystem = new MockFileSystem(); + var config = CreateConfig(...); + + // Act: Build navigation + var nav = new DocumentationSetNavigation(...); + + // Assert: Verify behavior + Assert.Equal("/expected/url/", nav.Index.Url); +} +``` + +## Common Tasks + +### Adding a New Node Type + +1. Create class in `Isolated/` namespace +2. Implement appropriate interface (`ILeafNavigationItem` or `INodeNavigationItem`) +3. Add factory method if needed +4. Update `ConvertToNavigationItem` in `DocumentationSetNavigation` +5. Add tests in `Isolation/` +6. Update [node-types.md](node-types.md) + +### Changing URL Calculation + +1. Review [first-principles.md](first-principles.md) - ensure consistency +2. Update `FileNavigationLeaf.Url` property +3. Consider cache invalidation +4. Update tests +5. Update [home-provider-architecture.md](home-provider-architecture.md) + +### Modifying Configuration + +1. Update classes in `Elastic.Documentation.Configuration` +2. Update `LoadAndResolve` methods +3. Update Phase 2 consumption in navigation classes +4. Update tests for both phases +5. Update [two-phase-loading.md](two-phase-loading.md) + +### Debugging Re-homing Issues + +1. Check `HomeProvider` assignments in [assembler-process.md](assembler-process.md) +2. Verify `PathPrefix` values +3. Check `NavigationRoot` points to correct root +4. Look for cache issues (HomeProvider ID changed?) +5. Review [home-provider-architecture.md](home-provider-architecture.md) + +## Related Documentation + +- `Elastic.Documentation.Configuration` - Phase 1 (configuration resolution) +- `Elastic.Documentation.Links` - Cross-link resolution +- `Elastic.Markdown` - Markdown processing + +## Source Reference + +For the actual implementation, see: +- Library: `src/Elastic.Documentation.Navigation/` +- Tests: `tests/Navigation.Tests/` +- Configuration: `src/Elastic.Documentation.Configuration/` + +## Contributing + +When making changes: + +1. **Maintain invariants** from [first-principles.md](first-principles.md) +2. **Keep phases separate** - don't mix configuration and navigation +3. **Preserve O(1) re-homing** - don't add tree traversals +4. **Add tests** for both isolated and assembler scenarios +5. **Update documentation** in this directory +6. **Run all 111+ tests** - they should all pass + +## Questions? + +- **"How do URLs get calculated?"** → [home-provider-architecture.md](home-provider-architecture.md) +- **"Why two phases?"** → [two-phase-loading.md](two-phase-loading.md) +- **"What is re-homing?"** → [navigation.md](navigation.md) then [home-provider-architecture.md](home-provider-architecture.md) +- **"Which node type do I need?"** → [node-types.md](node-types.md) +- **"How does the assembler work?"** → [assembler-process.md](assembler-process.md) +- **"What are the design principles?"** → [first-principles.md](first-principles.md) + +--- + +**Welcome to Elastic.Documentation.Navigation!** + +The library that makes it possible to build documentation in isolation and efficiently assemble it into unified sites with custom URL structures - no rebuilding required. 🚀 diff --git a/docs/development/navigation/assembler-process.md b/docs/development/navigation/assembler-process.md new file mode 100644 index 000000000..cbf57ebe9 --- /dev/null +++ b/docs/development/navigation/assembler-process.md @@ -0,0 +1,658 @@ +# Assembler Process + +This document explains how the assembler builds a unified site navigation from multiple documentation repositories. + +## Overview + +The assembler combines multiple isolated documentation repositories into a single site with: +- Unified navigation structure +- Custom URL prefixes for each repository +- Cross-repository linking +- Phantom node tracking + +## The Challenge + +Given: +- `elastic-docs` repository (guides, API reference, tutorials) +- `elasticsearch` repository (ES-specific docs) +- `kibana` repository (Kibana-specific docs) +- `logstash` repository (Logstash-specific docs) + +Build a site where: +- Each repository maintains its own `docset.yml` +- URLs are organized by product/section (not repository) +- Same TOC can appear in multiple places +- Navigation structure is defined centrally + +**Example:** +``` +elastic-docs has: + - /api/ + - /guides/ + +We want site to have: + - /elasticsearch/api/ (from elastic-docs://api) + - /elasticsearch/guides/ (from elastic-docs://guides) + - /kibana/api/ (from kibana://) + - /logstash/ (from logstash://) +``` + +## The Solution: Site Navigation + +`config/navigation.yml` defines the site structure: + +```yaml +toc: + - toc: elasticsearch + children: + - toc: elastic-docs://api + path_prefix: elasticsearch/api + - toc: elastic-docs://guides + path_prefix: elasticsearch/guides + + - toc: kibana:// + path_prefix: kibana + + - toc: logstash:// + path_prefix: logstash + +phantoms: + - source: plugins:// + # Declared but not included in navigation +``` + +## Assembler Build Process + +### Phase 1: Build Isolated Navigations + +```csharp +// For each repository in assembler.yml +foreach (var repo in repositories) +{ + // Load docset configuration + var docsetYaml = LoadFile($"{repo}/docset.yml"); + var docsetConfig = DocumentationSetFile.LoadAndResolve( + collector, + docsetYaml, + fileSystem.DirectoryInfo.New(repo) + ); + + // Build isolated navigation + var navigation = new DocumentationSetNavigation( + docsetConfig, + CreateContext(repo), + GenericDocumentationFileFactory.Instance + ); + + // Store for later assembly + isolatedNavigations[repo] = navigation; +} +``` + +**Result:** Each repository has its own navigation tree with URLs relative to `/`. + +### Phase 2: Load Site Navigation Configuration + +```csharp +// Load navigation.yml +var navigationYaml = LoadFile("config/navigation.yml"); +var siteConfig = SiteNavigationFile.LoadAndResolve( + collector, + navigationYaml, + fileSystem +); +``` + +**`SiteNavigationFile` contains:** +```csharp +public class SiteNavigationFile +{ + public List TableOfContents { get; set; } + public List Phantoms { get; set; } +} + +public class SiteTableOfContentsRef +{ + public Uri Source { get; set; } // e.g., elastic-docs://api + public string PathPrefix { get; set; } // e.g., "elasticsearch/api" + public List Children { get; set; } +} +``` + +### Phase 3: Assemble Site Navigation + +```csharp +// Create site navigation - this does the re-homing! +var siteNavigation = new SiteNavigation( + siteConfig, + context, + isolatedNavigations.Values, // All isolated navigations + sitePrefix: null // Or custom prefix like "/docs" +); +``` + +**What `SiteNavigation` constructor does:** + +```csharp +public SiteNavigation( + SiteNavigationFile siteNavigationFile, + IDocumentationContext context, + IReadOnlyCollection documentationSetNavigations, + string? sitePrefix) +{ + _sitePrefix = NormalizeSitePrefix(sitePrefix); + + // 1. Initialize root properties + NavigationRoot = this; + Parent = null; + Hidden = false; + Id = ShortId.Create("site"); + Identifier = new Uri("site://"); + + // 2. Collect all root nodes (docsets and TOCs) into _nodes + _nodes = []; + foreach (var setNavigation in documentationSetNavigations) + { + foreach (var (identifier, node) in setNavigation.TableOfContentNodes) + { + if (!_nodes.TryAdd(identifier, node)) + { + context.EmitError( + context.ConfigurationPath, + $"Duplicate navigation identifier: {identifier}" + ); + } + } + } + + // 3. Build site navigation by re-homing nodes + var items = new List(); + var index = 0; + foreach (var tocRef in siteNavigationFile.TableOfContents) + { + var navItem = CreateSiteTableOfContentsNavigation( + tocRef, + index++, + context, + parent: this, + root: null + ); + + if (navItem != null) + items.Add(navItem); + } + + // 4. Set index and navigation items + var indexNavigation = items.QueryIndex( + this, + "/index.md", + out var navigationItems + ); + Index = indexNavigation; + NavigationItems = navigationItems; +} +``` + +### Phase 4: Re-home Individual Nodes + +The magic happens in `CreateSiteTableOfContentsNavigation`: + +```csharp +private INavigationItem? CreateSiteTableOfContentsNavigation( + SiteTableOfContentsRef tocRef, + int index, + IDocumentationContext context, + INodeNavigationItem parent, + IRootNavigationItem? root) +{ + // 1. Calculate path prefix + var pathPrefix = tocRef.PathPrefix; + if (string.IsNullOrWhiteSpace(pathPrefix)) + { + // Handle narrative repository special case + if (tocRef.Source.Scheme != NarrativeRepository.RepositoryName) + { + context.EmitError( + context.ConfigurationPath, + $"path_prefix is required for TOC reference: {tocRef.Source}" + ); + return null; + } + } + + // Normalize path prefix + pathPrefix = pathPrefix.Trim('/'); + pathPrefix = !string.IsNullOrWhiteSpace(_sitePrefix) + ? $"{_sitePrefix}/{pathPrefix}" + : "/" + pathPrefix; + + // 2. Look up the node + if (!_nodes.TryGetValue(tocRef.Source, out var node)) + { + context.EmitError( + context.ConfigurationPath, + $"Could not find navigation node for identifier: {tocRef.Source}" + ); + return null; + } + + if (node is not INavigationHomeAccessor homeAccessor) + { + context.EmitError( + context.ConfigurationPath, + $"Navigation node does not implement INavigationHomeAccessor: {tocRef.Source}" + ); + return null; + } + + root ??= node; + + // 3. RE-HOME THE NODE! ⚡ + node.Parent = parent; + node.NavigationIndex = index; + homeAccessor.HomeProvider = new NavigationHomeProvider(pathPrefix, root); + + // 4. Process children (may include re-homing nested nodes) + var children = new List(); + + // First, add node's existing children + INavigationItem[] nodeChildren = [node.Index, .. node.NavigationItems]; + foreach (var nodeChild in nodeChildren) + { + nodeChild.Parent = node; + if (nodeChild is INavigationHomeAccessor childAccessor) + childAccessor.HomeProvider = homeAccessor.HomeProvider; + + // Don't add root nodes unless explicitly declared + if (nodeChild is IRootNavigationItem) + continue; + + children.Add(nodeChild); + } + + // Then, add any additional children from navigation.yml + if (tocRef.Children.Count > 0) + { + var childIndex = 0; + foreach (var child in tocRef.Children) + { + var childItem = CreateSiteTableOfContentsNavigation( + child, + childIndex++, + context, + node, + root + ); + if (childItem != null) + children.Add(childItem); + } + } + + // 5. Set children on the node + switch (node) + { + case IAssignableChildrenNavigation documentationSetNavigation: + documentationSetNavigation.SetNavigationItems(children); + break; + } + + return node; +} +``` + +## Detailed Example + +### Input: Isolated Navigation + +``` +elastic-docs repository: +DocumentationSetNavigation (elastic-docs://) + PathPrefix: "" + NavigationRoot: self + Index: / + NavigationItems: + - TableOfContentsNavigation (elastic-docs://api) + PathPrefix: "" + NavigationRoot: DocumentationSetNavigation + Index: /api/ + NavigationItems: + - FileNavigationLeaf (api/rest.md) + Url: /api/rest/ + NavigationRoot: DocumentationSetNavigation + + - TableOfContentsNavigation (elastic-docs://guides) + PathPrefix: "" + NavigationRoot: DocumentationSetNavigation + Index: /guides/ + NavigationItems: + - FileNavigationLeaf (guides/getting-started.md) + Url: /guides/getting-started/ + NavigationRoot: DocumentationSetNavigation +``` + +### Configuration: navigation.yml + +```yaml +toc: + - toc: elasticsearch + children: + - toc: elastic-docs://api + path_prefix: elasticsearch/api + + - toc: elastic-docs://guides + path_prefix: elasticsearch/guides +``` + +### Output: Assembled Site Navigation + +``` +SiteNavigation (site://) + PathPrefix: "" + NavigationRoot: self + Identifier: site:// + + Nodes: + [elastic-docs://] = DocumentationSetNavigation + [elastic-docs://api] = TableOfContentsNavigation + [elastic-docs://guides] = TableOfContentsNavigation + + NavigationItems: + - VirtualFolderNode ("elasticsearch") + NavigationItems: + + - TableOfContentsNavigation (elastic-docs://api) RE-HOMED! ⚡ + PathPrefix: "elasticsearch/api" ← Changed! + NavigationRoot: SiteNavigation ← Changed! + Parent: VirtualFolderNode ← Changed! + HomeProvider: new NavigationHomeProvider( + "/elasticsearch/api", + SiteNavigation + ) + Index: /elasticsearch/api/ ← Changed! + NavigationItems: + - FileNavigationLeaf (api/rest.md) + Url: /elasticsearch/api/rest/ ← Changed! + NavigationRoot: SiteNavigation ← Changed! + + - TableOfContentsNavigation (elastic-docs://guides) RE-HOMED! ⚡ + PathPrefix: "elasticsearch/guides" ← Changed! + NavigationRoot: SiteNavigation ← Changed! + Parent: VirtualFolderNode ← Changed! + HomeProvider: new NavigationHomeProvider( + "/elasticsearch/guides", + SiteNavigation + ) + Index: /elasticsearch/guides/ ← Changed! + NavigationItems: + - FileNavigationLeaf (guides/getting-started.md) + Url: /elasticsearch/guides/getting-started/ ← Changed! + NavigationRoot: SiteNavigation ← Changed! +``` + +**Key Changes:** +1. `HomeProvider` replaced → new path prefix and navigation root +2. All URLs automatically updated (lazy calculation) +3. Parent relationships updated +4. NavigationRoot points to SiteNavigation + +## Re-homing Performance + +**Cost of re-homing a subtree with 10,000 nodes:** +```csharp +// This single line re-homes all 10,000 nodes! +homeAccessor.HomeProvider = new NavigationHomeProvider(pathPrefix, root); +``` + +**Time complexity:** O(1) +- No tree traversal +- No URL updates +- Just reference assignment + +**When URLs are accessed later:** +- First access: O(path depth) calculation +- Subsequent: O(1) from cache +- Cache invalidated automatically (HomeProvider ID changed) + +## Phantom Nodes + +Phantoms are nodes declared in navigation.yml but not included in the tree: + +```yaml +phantoms: + - source: plugins:// + - source: cloud://monitoring +``` + +**Purpose:** +- Document intentionally excluded content +- Prevent "undeclared navigation" warnings +- Enable validation of cross-links to phantom content + +**Tracking:** +```csharp +// SiteNavigation tracks phantoms +public IReadOnlyCollection Phantoms { get; } +public HashSet DeclaredPhantoms { get; } + +// After assembly, check for unseen nodes +foreach (var node in UnseenNodes) +{ + if (!DeclaredPhantoms.Contains(node)) + context.EmitHint( + context.ConfigurationPath, + $"Navigation does not explicitly declare: {node} as a phantom" + ); +} +``` + +## Path Prefix Requirements + +In assembler builds, `path_prefix` is **mandatory** (with one exception): + +```yaml +toc: + - toc: elastic-docs://api + path_prefix: elasticsearch/api # Required! + + - toc: docs-content://guides + # path_prefix not required for narrative repository + # Will default to "guides" +``` + +**Why?** +- Prevents URL collisions +- Makes routing explicit +- Enables flexible reorganization + +**Validation:** +```csharp +if (string.IsNullOrWhiteSpace(pathPrefix)) +{ + if (tocRef.Source.Scheme != NarrativeRepository.RepositoryName) + { + context.EmitError( + context.ConfigurationPath, + $"path_prefix is required for TOC reference: {tocRef.Source}" + ); + } +} +``` + +## Nested Re-homing + +Assembler builds can have nested structures where nodes are re-homed multiple times: + +```yaml +toc: + - toc: products + children: + - toc: elasticsearch + path_prefix: products/elasticsearch + children: + - toc: elastic-docs://api + path_prefix: products/elasticsearch/api +``` + +**How it works:** + +1. Create virtual `products` node +2. Re-home `elasticsearch` to `/products/elasticsearch` +3. Re-home `elastic-docs://api` to `/products/elasticsearch/api` + +Each level creates a new scope with its own HomeProvider. + +## Error Handling + +The assembler emits errors for common issues: + +### Duplicate Identifiers +```csharp +// Two docsets with same identifier +if (!_nodes.TryAdd(identifier, node)) +{ + context.EmitError( + context.ConfigurationPath, + $"Duplicate navigation identifier: {identifier}" + ); +} +``` + +### Missing Nodes +```csharp +// Referenced node doesn't exist +if (!_nodes.TryGetValue(tocRef.Source, out var node)) +{ + context.EmitError( + context.ConfigurationPath, + $"Could not find navigation node for identifier: {tocRef.Source}" + ); +} +``` + +### Undeclared Nested TOCs +```csharp +// Found a nested TOC that wasn't declared in navigation.yml +if (!DeclaredTableOfContents.Contains(rootChild.Identifier) && + !DeclaredPhantoms.Contains(rootChild.Identifier)) +{ + context.EmitWarning( + context.ConfigurationPath, + $"Navigation does not explicitly declare: {rootChild.Identifier}" + ); +} +``` + +## Site Prefix + +The entire site can have a global prefix: + +```csharp +var siteNavigation = new SiteNavigation( + siteConfig, + context, + documentationSetNavigations, + sitePrefix: "/docs" // All URLs start with /docs +); +``` + +**Example:** +``` +Without site prefix: + /elasticsearch/api/rest/ + +With sitePrefix="/docs": + /docs/elasticsearch/api/rest/ +``` + +**Normalization:** +```csharp +private static string? NormalizeSitePrefix(string? sitePrefix) +{ + if (string.IsNullOrWhiteSpace(sitePrefix)) + return null; + + var normalized = sitePrefix.Trim(); + + // Ensure leading slash + if (!normalized.StartsWith('/')) + normalized = "/" + normalized; + + // Remove trailing slash + normalized = normalized.TrimEnd('/'); + + return normalized; +} +``` + +## Testing Assembler Builds + +```csharp +[Fact] +public void AssemblerRehomesNavigationUrls() +{ + // Arrange: Build isolated navigation + var docset = DocumentationSetFile.LoadAndResolve( + collector, + yaml, + fileSystem.NewDirInfo("/elastic-docs") + ); + var isolatedNav = new DocumentationSetNavigation( + docset, + isolatedContext, + factory + ); + + // Assert: URLs in isolated build + var apiLeaf = FindLeaf(isolatedNav, "api/rest.md"); + Assert.Equal("/api/rest/", apiLeaf.Url); + + // Act: Assemble site + var siteConfig = new SiteNavigationFile + { + TableOfContents = [ + new SiteTableOfContentsRef + { + Source = new Uri("elastic-docs://api"), + PathPrefix = "elasticsearch/api" + } + ] + }; + + var siteNav = new SiteNavigation( + siteConfig, + assemblerContext, + [isolatedNav], + sitePrefix: null + ); + + // Assert: URLs in assembled site + var apiLeafInSite = FindLeaf(siteNav, "api/rest.md"); + Assert.Equal("/elasticsearch/api/rest/", apiLeafInSite.Url); + + // The same leaf object! Just re-homed! + Assert.Same(apiLeaf, apiLeafInSite); +} +``` + +## Summary + +The assembler process: + +1. **Builds isolated navigations** - Each repository with URLs relative to `/` +2. **Loads site configuration** - `navigation.yml` with path prefixes +3. **Re-homes nodes** - O(1) operation to change URL prefix +4. **Assembles site tree** - Unified navigation with custom structure +5. **Tracks phantoms** - Documents excluded content +6. **Validates** - Catches duplicates, missing nodes, undeclared TOCs + +**Key Innovation:** Re-homing via HomeProvider pattern enables: +- O(1) URL prefix changes +- No tree reconstruction +- Lazy URL calculation +- Same node in multiple contexts + +This makes it possible to: +- Build repositories independently +- Test in isolation +- Assemble flexibly into unified site +- Reorganize without rebuilding diff --git a/docs/development/navigation/first-principles.md b/docs/development/navigation/first-principles.md new file mode 100644 index 000000000..884cd8c6b --- /dev/null +++ b/docs/development/navigation/first-principles.md @@ -0,0 +1,222 @@ +# First Principles + +This document outlines the fundamental principles that guide the design of `Elastic.Documentation.Navigation`. + +## Core Principles + +### 1. Two-Phase Loading + +Navigation construction follows a strict two-phase approach: + +**Phase 1: Configuration Resolution** (`Elastic.Documentation.Configuration`) +- Parse YAML files (`docset.yml`, `toc.yml`, `navigation.yml`) +- Resolve all file references to **full paths** relative to documentation set root +- Validate configuration structure and relationships +- Output: Fully resolved configuration objects with complete file paths + +**Phase 2: Navigation Construction** (`Elastic.Documentation.Navigation`) +- Consume resolved configuration from Phase 1 +- Build navigation tree with **full URLs** +- Create node relationships (parent/child/root) +- Set up home providers for URL calculation +- Output: Complete navigation tree with calculated URLs + +**Why Two Phases?** +- **Separation of Concerns**: Configuration parsing is independent of navigation structure +- **Validation**: Catch file/structure errors before building expensive navigation trees +- **Reusability**: Same configuration can build different navigation structures (isolated vs assembler) +- **Performance**: Resolve file system operations once, reuse for navigation + +### 2. Single Documentation Source + +URLs are always built relative to the documentation set's source directory: +- Files referenced in `docset.yml` are relative to the docset root +- Files referenced in nested `toc.yml` are relative to the toc directory +- During Phase 1, all paths are resolved to be relative to the docset root +- During Phase 2, URLs are calculated from these resolved paths + +**Example:** +``` +docs/ +├── docset.yml # Root +├── index.md +└── api/ + ├── toc.yml # Nested TOC + └── rest.md +``` + +Phase 1 resolves `api/toc.yml` reference to `rest.md` as: `api/rest.md` (relative to docset root) +Phase 2 builds URL as: `/api/rest/` + +### 3. URL Building is Dynamic and Cheap + +URLs are **calculated on-demand**, not stored: +- Nodes don't store their final URL +- URLs are computed from `HomeProvider.PathPrefix` + relative path +- Changing a `HomeProvider` instantly updates all descendant URLs +- No tree traversal needed to update URLs + +**Why Dynamic?** +- **Re-homing**: Same subtree can have different URLs in different contexts +- **Memory Efficient**: Don't store redundant URL strings +- **Consistency**: URLs always reflect current home provider state + +### 4. Navigation Roots Can Be Re-homed + +A key design feature that enables assembler builds: +- **Isolated Build**: Each `DocumentationSetNavigation` is its own root +- **Assembler Build**: `SiteNavigation` becomes the root, docsets are "re-homed" +- **Re-homing**: Replace a subtree's `HomeProvider` to change its URL prefix +- **Cheap Operation**: O(1) - just replace the provider reference + +**Example:** +```csharp +// Isolated: URLs start at / +homeProvider.PathPrefix = ""; +// → /api/rest/ + +// Assembled: Re-home to /guide +homeProvider = new NavigationHomeProvider("/guide", siteNav); +// → /guide/api/rest/ +``` + +### 5. Navigation Scope via HomeProvider + +`INavigationHomeProvider` creates navigation scopes: +- **Provider**: Defines `PathPrefix` and `NavigationRoot` for a scope +- **Accessor**: Children use `INavigationHomeAccessor` to access their scope +- **Inheritance**: Child nodes inherit their parent's accessor +- **Isolation**: Changes to a provider only affect its scope + +**Scope Creators:** +- `DocumentationSetNavigation` - Creates scope for entire docset +- `TableOfContentsNavigation` - Creates scope for TOC subtree (enables re-homing) + +**Scope Consumers:** +- `FileNavigationLeaf` - Uses accessor to calculate URL +- `FolderNavigation` - Passes accessor to children +- `VirtualFileNavigation` - Passes accessor to children + +### 6. Index Files Determine Folder URLs + +Every folder/node navigation has an **Index**: +- Index is either `index.md` or the first file +- The node's URL is the same as its Index's URL +- Children appear "under" the index in navigation +- Index files map to folder paths: `/api/index.md` → `/api/` + +**Why?** +- **Consistent URL Structure**: Folders and their indexes share the same URL +- **Natural Navigation**: Index represents the folder's landing page +- **Hierarchical**: Clear parent-child URL relationships + +### 7. File Structure Should Mirror Navigation + +Best practices for maintainability: +- Navigation structure should follow file system structure +- Avoid deep-linking files from different directories +- Use `folder:` references when possible +- Virtual files should group sibling files, not restructure the tree + +**Rationale:** +- **Discoverability**: Developers can find files by following navigation +- **Predictability**: URL structure matches file structure +- **Maintainability**: Moving files in navigation matches moving them on disk + +### 8. Generic Type System for Covariance + +Navigation classes are generic over `TModel`: +```csharp +public class DocumentationSetNavigation + where TModel : class, IDocumentationFile +``` + +**Why Generic?** +- **Covariance**: Can treat `DocumentationSetNavigation` as `IRootNavigationItem` +- **Type Safety**: Factory pattern ensures correct model types +- **Flexibility**: Same navigation code works with different file models + +### 9. Lazy URL Calculation with Caching + +`FileNavigationLeaf` implements smart URL caching: +```csharp +private string? _homeProviderCache; +private string? _urlCache; + +public string Url +{ + get + { + if (_homeProviderCache == HomeProvider.Id && _urlCache != null) + return _urlCache; + + _urlCache = CalculateUrl(); + _homeProviderCache = HomeProvider.Id; + return _urlCache; + } +} +``` + +**Strategy:** +- Cache URL along with HomeProvider ID +- Invalidate cache when HomeProvider changes +- Recalculate only when needed +- O(1) for repeated access, O(path) for calculation + +### 10. Phantom Nodes for Incomplete Navigation + +`navigation.yml` can declare phantoms: +```yaml +phantoms: + - source: plugins:// +``` + +**Purpose:** +- Reference nodes that exist but aren't included in site navigation +- Prevent "undeclared navigation" warnings +- Document intentionally excluded content +- Enable validation of cross-links + +## Design Patterns + +### Factory Pattern for Node Creation + +`DocumentationNavigationFactory` creates navigation items: +- Encapsulates construction logic +- Ensures consistent initialization +- Centralizes node creation +- Type-safe generic methods + +### Provider Pattern for URL Context + +`INavigationHomeProvider` / `INavigationHomeAccessor`: +- Providers define context (PathPrefix, NavigationRoot) +- Accessors reference providers +- Decouples URL calculation from tree structure +- Enables context switching (re-homing) + +### Visitor Pattern for Tree Operations + +Navigation items implement interfaces for traversal: +- `IRootNavigationItem` - Root nodes +- `INodeNavigationItem` - Nodes with children +- `ILeafNavigationItem` - Leaf nodes +- Common `INavigationItem` base for polymorphic traversal + +## Key Invariants + +1. **Phase Order**: Configuration must be fully resolved before navigation construction +2. **Path Resolution**: All paths in configuration are relative to docset root after Phase 1 +3. **URL Uniqueness**: Every navigation item must have a unique URL within its site +4. **Root Consistency**: All nodes in a subtree point to the same `NavigationRoot` +5. **Provider Validity**: A node's `HomeProvider` must be an ancestor in the tree +6. **Index Requirement**: All node navigations (folder/toc/docset) must have an Index +7. **Path Prefix Uniqueness**: In assembler builds, all `path_prefix` values must be unique + +## Performance Characteristics + +- **Tree Construction**: O(n) where n = number of files +- **URL Calculation**: O(depth) for first access, O(1) with caching +- **Re-homing**: O(1) - just replace HomeProvider reference +- **Tree Traversal**: O(n) for full tree, but rarely needed +- **Memory**: O(n) for nodes, URLs computed on-demand diff --git a/docs/development/navigation/home-provider-architecture.md b/docs/development/navigation/home-provider-architecture.md new file mode 100644 index 000000000..caa51f71d --- /dev/null +++ b/docs/development/navigation/home-provider-architecture.md @@ -0,0 +1,517 @@ +# Home Provider Architecture + +The Home Provider pattern is the secret sauce that makes re-homing navigation subtrees a cheap O(1) operation. + +## The Problem + +When building assembled documentation sites, we need to: +1. Build navigation for individual repositories in isolation +2. Combine them into a single site with custom URL prefixes +3. Update all URLs in a subtree efficiently + +**Naive Approach (doesn't work):** +```csharp +// Bad: Traverse entire subtree to update URLs +void UpdateUrlPrefix(INavigationItem root, string newPrefix) +{ + // O(n) - have to visit every node! + foreach (var item in TraverseTree(root)) + { + item.UrlPrefix = newPrefix; + } +} +``` + +**Problems with naive approach:** +- O(n) traversal for every prefix change +- Have to store URL prefix at every node (memory waste) +- Hard to keep URLs consistent +- Can't lazily calculate URLs + +## The Solution: Home Provider Pattern + +Instead of storing URL information at each node, we use a **provider pattern** with indirection: + +```csharp +// The provider defines the URL context for a scope +public interface INavigationHomeProvider +{ + string PathPrefix { get; } + IRootNavigationItem NavigationRoot { get; } + string Id { get; } // For cache invalidation +} + +// Nodes access their provider through an accessor +public interface INavigationHomeAccessor +{ + INavigationHomeProvider HomeProvider { get; set; } +} +``` + +### Key Insight + +Nodes don't store URL information. Instead they **reference** a provider: + +```csharp +public class FileNavigationLeaf +{ + private readonly INavigationHomeAccessor _homeAccessor; + + public string Url + { + get + { + // Dynamically calculate from current provider! + var prefix = _homeAccessor.HomeProvider.PathPrefix; + return $"{prefix}/{_relativePath}/"; + } + } +} +``` + +Now re-homing is O(1): + +```csharp +// Change the provider → all descendants instantly use new prefix! +docsetNavigation.HomeProvider = new NavigationHomeProvider("/guide", siteNav); +``` + +## How It Works + +### 1. Scope Creation + +Certain navigation types create scopes by implementing `INavigationHomeProvider`: + +```csharp +public class DocumentationSetNavigation + : INavigationHomeProvider, INavigationHomeAccessor +{ + // This node IS a provider (creates scope) + private string _pathPrefix; + + // Properties for being a provider + public string PathPrefix => HomeProvider == this + ? _pathPrefix + : HomeProvider.PathPrefix; + + public IRootNavigationItem<...> NavigationRoot => + HomeProvider == this + ? this + : HomeProvider.NavigationRoot; + + // Property for accessing current provider + public INavigationHomeProvider HomeProvider { get; set; } + + // Initially, it's its own provider + public DocumentationSetNavigation(...) + { + _pathPrefix = pathPrefix ?? ""; + HomeProvider = this; // I am my own provider! + } +} +``` + +### 2. Scope Inheritance + +Child nodes receive their parent's accessor: + +```csharp +// Creating a child node +var fileNav = new FileNavigationLeaf( + model, + fileInfo, + new FileNavigationArgs( + path, + relativePath, + hidden, + index, + parent, + homeAccessor: this // Pass down the accessor! + ) +); +``` + +### 3. URL Calculation + +Leaf nodes use the accessor to calculate URLs: + +```csharp +public class FileNavigationLeaf +{ + private readonly FileNavigationArgs _args; + + public string Url + { + get + { + // Get prefix from current provider + var rootUrl = _args.HomeAccessor.HomeProvider.PathPrefix.TrimEnd('/'); + + // Determine if we're relative to container or docset + var relativeToContainer = + _args.HomeAccessor.HomeProvider.NavigationRoot.Parent is SiteNavigation; + + var relativePath = relativeToContainer + ? _args.RelativePathToTableOfContents + : _args.RelativePathToDocumentationSet; + + // Calculate URL + return BuildUrl(rootUrl, relativePath); + } + } +} +``` + +### 4. Re-homing (The Magic!) + +In assembler builds, `SiteNavigation` re-homes subtrees: + +```csharp +// SiteNavigation.cs:211 +private INavigationItem? CreateSiteTableOfContentsNavigation(...) +{ + // Calculate new path prefix for this subtree + var pathPrefix = $"{_sitePrefix}/{tocRef.PathPrefix}".Trim('/'); + + // Create new provider with custom prefix + var newProvider = new NavigationHomeProvider(pathPrefix, root); + + // Re-home the entire subtree with one assignment! + homeAccessor.HomeProvider = newProvider; + + // All descendants now use the new prefix! ✨ +} +``` + +**What happens:** +1. `homeAccessor.HomeProvider` is set to new provider +2. Provider has `PathPrefix = "/guide"` and `NavigationRoot = SiteNavigation` +3. Every URL calculation in that subtree now uses "/guide" prefix +4. No tree traversal needed! + +## Detailed Example + +### Isolated Build + +``` +DocumentationSetNavigation (elastic-docs) +├─ HomeProvider: self +├─ PathPrefix: "" +├─ NavigationRoot: self +│ +└─ TableOfContentsNavigation (api/) + ├─ HomeProvider: inherited from parent = DocumentationSetNavigation + ├─ PathPrefix: "" (from provider) + ├─ NavigationRoot: DocumentationSetNavigation (from provider) + │ + └─ FileNavigationLeaf (api/rest.md) + ├─ HomeAccessor.HomeProvider: DocumentationSetNavigation + └─ URL calculation: + prefix = HomeProvider.PathPrefix = "" + path = "api/rest.md" + url = "/api/rest/" +``` + +### Assembler Build - Before Re-homing + +``` +SiteNavigation +├─ HomeProvider: self +├─ PathPrefix: "" +│ +└─ (About to add elastic-docs with prefix "/guide") +``` + +### Assembler Build - After Re-homing + +``` +SiteNavigation +├─ HomeProvider: self +├─ PathPrefix: "" +├─ NavigationRoot: self +│ +└─ DocumentationSetNavigation (elastic-docs) + ├─ HomeProvider: NEW NavigationHomeProvider("/guide", SiteNavigation) ⚡ + ├─ PathPrefix: "/guide" (from NEW provider) + ├─ NavigationRoot: SiteNavigation (from NEW provider) + │ + └─ TableOfContentsNavigation (api/) + ├─ HomeProvider: same as parent = NEW provider ⚡ + ├─ PathPrefix: "/guide" (inherited from NEW provider) + ├─ NavigationRoot: SiteNavigation (inherited from NEW provider) + │ + └─ FileNavigationLeaf (api/rest.md) + ├─ HomeAccessor.HomeProvider: NEW provider ⚡ + └─ URL calculation: + prefix = HomeProvider.PathPrefix = "/guide" + path = "api/rest.md" + url = "/guide/api/rest/" ✨ +``` + +**The re-homing happened at line marked with ⚡ - a single assignment!** + +## Why This Is Brilliant + +### 1. O(1) Re-homing + +```csharp +// This is ALL it takes to update thousands of URLs! +node.HomeProvider = new NavigationHomeProvider("/new-prefix", newRoot); +``` + +**Compare to naive approach:** +- Naive: O(n) traversal of entire subtree +- Provider: O(1) single reference assignment + +### 2. Lazy Evaluation + +URLs are only calculated when accessed: +- Don't pay for URLs you never request +- Memory efficient - no stored URL strings +- Always reflects current provider state + +### 3. Smart Caching + +```csharp +private string? _homeProviderCache; +private string? _urlCache; + +public string Url +{ + get + { + // Check if provider changed + if (_homeProviderCache != null && + _homeProviderCache == _args.HomeAccessor.HomeProvider.Id && + _urlCache != null) + { + return _urlCache; // Cache hit! + } + + // Recalculate and cache + _homeProviderCache = _args.HomeAccessor.HomeProvider.Id; + _urlCache = DetermineUrl(); + return _urlCache; + } +} +``` + +**Benefits:** +- First access: O(path depth) calculation +- Subsequent accesses: O(1) cache lookup +- Cache invalidates automatically when provider changes (via Id) +- No manual cache management needed + +### 4. Scope Isolation + +Each provider creates an isolated scope: +- Changes to one scope don't affect others +- Clear ownership of URL context +- Easy to reason about URL calculation +- Enables multiple "views" of same navigation tree + +### 5. Type Safety + +```csharp +// Can't forget to pass the accessor - it's in the constructor! +public FileNavigationLeaf( + TModel model, + IFileInfo fileInfo, + FileNavigationArgs args // Contains HomeAccessor +) +``` + +## Implementation Details + +### Provider Identity + +Each provider has a unique ID for cache invalidation: + +```csharp +public class NavigationHomeProvider : INavigationHomeProvider +{ + public string Id { get; } = Guid.NewGuid().ToString("N"); +} +``` + +When a provider changes, the ID changes, invalidating all cached URLs. + +### Accessor vs Provider + +**Why separate interfaces?** + +- **Provider**: Nodes that CREATE scopes (DocumentationSetNavigation, TableOfContentsNavigation) +- **Accessor**: Nodes that USE scopes (all nodes) + +Some nodes are both: +```csharp +public class DocumentationSetNavigation + : INavigationHomeProvider, INavigationHomeAccessor +{ + // I can be a provider AND access a different provider +} +``` + +This enables re-homing! + +### Passing Accessors Down the Tree + +During construction, accessors flow down: + +```csharp +// Parent creates child, passes its accessor +var childNav = ConvertToNavigationItem( + tocItem, + index, + context, + parent: this, + homeAccessor: this // Pass down the accessor chain +); +``` + +Children inherit their parent's accessor, creating a chain back to the scope provider. + +### Assembler-Specific Provider Behavior + +In assembler builds, TOCs create isolated providers: + +```csharp +var assemblerBuild = context.AssemblerBuild; + +// For assembler builds, TOCs create their own home provider +var isolatedHomeProvider = assemblerBuild + ? new NavigationHomeProvider( + homeAccessor.HomeProvider.PathPrefix, + homeAccessor.HomeProvider.NavigationRoot + ) + : homeAccessor.HomeProvider; +``` + +**Why?** This ensures TOCs can be re-homed independently during site assembly. + +## Performance Analysis + +### Memory Usage + +**Per Node:** +- Provider: ~48 bytes (string, reference, guid) +- Accessor: 8 bytes (reference) +- Cache: ~32 bytes (2 strings) - only on leaf nodes + +**For 10,000 nodes:** +- Without caching: ~560 KB +- With cached URLs: ~880 KB +- Naive approach (stored URLs): ~1.5 MB+ + +### CPU Usage + +**URL Calculation:** +- Cache hit: O(1) - pointer dereference + string return +- Cache miss: O(depth) - string concatenation + path processing +- Re-homing: O(1) - reference assignment + +**Typical Access Pattern:** +- First access: Pay calculation cost +- Subsequent: Free cache lookups +- Re-home: Invalidate caches (cheap), recalculate on next access (lazy) + +### Scalability + +The pattern scales beautifully: +- 100 nodes: Re-home in 1μs +- 10,000 nodes: Re-home in 1μs +- 1,000,000 nodes: Re-home in 1μs + +**Because it's always O(1)!** + +## Common Patterns + +### Pattern 1: Creating a Scope + +```csharp +public class MyNavigation : INavigationHomeProvider, INavigationHomeAccessor +{ + private string _pathPrefix; + + public MyNavigation(string pathPrefix) + { + _pathPrefix = pathPrefix; + HomeProvider = this; // I am my own provider initially + } + + // Provider implementation + public string PathPrefix => HomeProvider == this ? _pathPrefix : HomeProvider.PathPrefix; + public IRootNavigationItem<...> NavigationRoot => /* ... */; + + // Accessor implementation + public INavigationHomeProvider HomeProvider { get; set; } +} +``` + +### Pattern 2: Consuming a Scope + +```csharp +public class MyLeaf +{ + private readonly INavigationHomeAccessor _homeAccessor; + + public MyLeaf(INavigationHomeAccessor homeAccessor) + { + _homeAccessor = homeAccessor; + } + + public string Url => + $"{_homeAccessor.HomeProvider.PathPrefix}/{_path}/"; +} +``` + +### Pattern 3: Re-homing + +```csharp +// In SiteNavigation or other assembler code +void RehomeSubtree( + INavigationHomeAccessor subtree, + string newPrefix, + IRootNavigationItem<...> newRoot) +{ + subtree.HomeProvider = new NavigationHomeProvider(newPrefix, newRoot); + // Done! All URLs updated. +} +``` + +## Testing the Pattern + +### Unit Test Example + +```csharp +[Fact] +public void RehomingUpdatesUrlsDynamically() +{ + // Arrange: Create isolated navigation + var docset = new DocumentationSetNavigation(...); + var leaf = docset.NavigationItems.First() as FileNavigationLeaf; + + // Initial URL + Assert.Equal("/api/rest/", leaf.Url); + + // Act: Re-home the docset + docset.HomeProvider = new NavigationHomeProvider("/guide", siteNav); + + // Assert: URL updated automatically! + Assert.Equal("/guide/api/rest/", leaf.Url); +} +``` + +## Summary + +The Home Provider pattern achieves: + +✅ **O(1) re-homing** - single reference assignment +✅ **Lazy evaluation** - calculate URLs only when needed +✅ **Smart caching** - O(1) repeated access +✅ **Memory efficient** - no stored URLs +✅ **Type safe** - compiler-enforced accessor passing +✅ **Scope isolation** - changes don't leak +✅ **Elegant code** - simple, understandable + +This pattern is what makes it possible to build isolated documentation repositories and then efficiently assemble them into a unified site with custom URL prefixes. Without it, we'd be stuck with expensive tree traversals or rigid, non-rehomable navigation structures. diff --git a/docs/development/navigation/navigation.md b/docs/development/navigation/navigation.md index ab139cf8b..4a5857785 100644 --- a/docs/development/navigation/navigation.md +++ b/docs/development/navigation/navigation.md @@ -1,65 +1,68 @@ # Navigation -## File Navigation types +This document provides an overview of how `Elastic.Documentation.Navigation` works. -### Isolated builds +## Documentation Structure -When building a signle repository's documentation, we are building the table of contents defined in `docset.yml` the root of which is represented by ![DocumentationSetNavigation](images/bullet-documentation-set-navigation.svg) `DocumentationSetNavigation` +For deeper dives into specific topics, see: -The table of contents is composed of a number of `NavigationItem`s. +- **[Visual Walkthrough](visual-walkthrough.md)** - Visual tour with diagrams showing navigation structures in both build modes +- **[First Principles](first-principles.md)** - Core design principles and invariants that guide the navigation architecture +- **[Two-Phase Loading](two-phase-loading.md)** - Why configuration resolution and navigation construction are separate phases +- **[Home Provider Architecture](home-provider-architecture.md)** - The pattern that enables O(1) re-homing of navigation subtrees +- **[Node Types](node-types.md)** - Detailed reference for each navigation node type (leaves, nodes, roots) +- **[Assembler Process](assembler-process.md)** - How multiple repositories are combined into a unified site -- `folder:` ![FolderNavigation](images/bullet-folder-navigation.svg) `FolderNavigation` -- `file:` ![FileNavigationLeaf](images/bullet-file-navigation-leaf.svg) `FileNavigationLeaf` +## Quick Start -`docset.yml` may break up it's table of contents into multiple sub navigation's using nested `toc.yml` files. +### Core Concepts -- `toc:` ![TableOfContentsNavigation](images/bullet-table-of-contents-navigation.svg) TableOfContentsNavigation +The navigation system builds hierarchical trees for documentation sites with these key features: -### Assembler builds +1. **Two Build Modes:** + - **Isolated** - Single repository (e.g., `docs-builder isolated build`) + - **Assembler** - Multi-repository site (e.g., `docs-builder assemble`) -The assembler build takes multiple `Isolated Build` navigations and recomposes it into a single ![SiteNavigation](images//bullet-site-navigation.svg) `SiteNavigation` navigation. +2. **Two-Phase Loading:** + - **Phase 1**: Parse YAML, resolve paths → Configuration + - **Phase 2**: Build tree, calculate URLs → Navigation -This navigation is defined in [`navigation.yml`](https://github.com/elastic/docs-builder/blob/main/config/navigation.yml) +3. **Re-homing:** + - Build navigation in isolation with URLs like `/api/rest/` + - Re-home during assembly to URLs like `/elasticsearch/api/rest/` + - **O(1) operation** - no tree traversal needed! -An assembler build can only reference: +### How Re-homing Works -- ![DocumentationSetNavigation](images/bullet-documentation-set-navigation.svg) `DocumentationSetNavigation` (using `://` crosslink) -- ![TableOfContentsNavigation](images/bullet-table-of-contents-navigation.svg) `TableOfContentsNavigation` (using `://` crosslink) +The key innovation is the [Home Provider pattern](home-provider-architecture.md): -A new special root is created for asssembler builds: +```csharp +// Isolated build +DocumentationSetNavigation +{ + HomeProvider: self, + PathPrefix: "" +} +// Child URL: /api/rest/ -- ![SiteNavigation](images//bullet-site-navigation.svg) `SiteNavigation` +// Re-home for assembler build (ONE LINE!) +docset.HomeProvider = new NavigationHomeProvider("/guide", siteNav); -## A Visual Example +// Child URL: /guide/api/rest/ ✨ All URLs updated! +``` -### Isolated builds +This is possible because URLs are **calculated dynamically** from the HomeProvider, not stored. Changing the provider instantly updates all descendant URLs without any tree traversal. -Imagine we have the following `docset.yml` that defines two nested `toc.yml` files. +See [Home Provider Architecture](home-provider-architecture.md) for the complete explanation. -![Isolated Build](images/isolated-build-tree.svg) +## Visual Examples -### Assembler builds +For a visual tour of navigation structures with diagrams showing both isolated and assembler builds, see the **[Visual Walkthrough](visual-walkthrough.md)**. -Now we can break that navigation up into multiple sections of the wider site navigation. - -- ![TableOfContentsNavigation](images/bullet-table-of-contents-navigation.svg)`docs-content://api` - - ![TableOfContentsNavigation](images/bullet-table-of-contents-navigation.svg)**`elastic-project://api`** -- ![TableOfContentsNavigation](images/bullet-table-of-contents-navigation.svg)`docs-content://guides` - - ![TableOfContentsNavigation](images/bullet-table-of-contents-navigation.svg)**`elastic-project://guides`** -- ![DocumentationSetNavigation](images/bullet-documentation-set-navigation.svg) `elastic-client://` - - ![DocumentationSetNavigation](images/bullet-documentation-set-navigation.svg) `elastic-client-node://` - - ![DocumentationSetNavigation](images/bullet-documentation-set-navigation.svg) `elastic-client-dotnet://` - - ![DocumentationSetNavigation](images/bullet-documentation-set-navigation.svg) `elastic-client-java://` - - -![DocumentationSetNavigation](images/bullet-documentation-set-navigation.svg) `DocumentationSetNavigation` may arbitrary nest other -![TableOfContentsNavigation](images/bullet-table-of-contents-navigation.svg) `TableOfContentsNavigation` and visa versa. - -The only requirement is that each node in the navigation **MUST** define a unique `path_prefix`. - -#### A fully resolved navigation. - -When resolving the navigation, all the child navigation items will be included and the url will dynamically be `re-homed` to the root ![DocumentationSetNavigation](images/bullet-documentation-set-navigation.svg) `DocumentationSetNavigation` or ![TableOfContentsNavigation](images/bullet-table-of-contents-navigation.svg) `TableOfContentsNavigation` that defines it. - -![Assembler Build](images/assembler-build-tree.svg) +The walkthrough covers: +- What nodes look like in isolated vs assembler builds +- How the same content appears with different URLs +- How to split and reorganize documentation across the site +- Common patterns for organizing multi-repository sites +- Examples with the actual tree diagrams from this repository diff --git a/docs/development/navigation/node-types.md b/docs/development/navigation/node-types.md new file mode 100644 index 000000000..090ba39ef --- /dev/null +++ b/docs/development/navigation/node-types.md @@ -0,0 +1,640 @@ +# Navigation Node Types + +This document provides a detailed reference for each navigation node type in `Elastic.Documentation.Navigation`. + +## Type Hierarchy + +``` +INavigationItem +├── ILeafNavigationItem +│ ├── FileNavigationLeaf +│ └── CrossLinkNavigationLeaf +│ +└── INodeNavigationItem + ├── IRootNavigationItem + │ ├── DocumentationSetNavigation + │ ├── TableOfContentsNavigation + │ └── SiteNavigation + │ + ├── FolderNavigation + └── VirtualFileNavigation +``` + +## Common Properties + +All navigation items implement `INavigationItem`: + +```csharp +public interface INavigationItem +{ + /// The URL for this navigation item + string Url { get; } + + /// Title displayed in navigation + string NavigationTitle { get; } + + /// Root of the navigation tree + IRootNavigationItem NavigationRoot { get; } + + /// Parent in the tree, null for roots + INodeNavigationItem? Parent { get; set; } + + /// Whether this item is hidden from navigation + bool Hidden { get; } + + /// Breadth-first index in the tree + int NavigationIndex { get; set; } +} +``` + +--- + +## Leaf Nodes + +Leaf nodes have no children. They represent individual documentation files or external links. + +### FileNavigationLeaf + +Represents an individual markdown file in the documentation. + +**Location:** `src/Elastic.Documentation.Navigation/Isolated/FileNavigationLeaf.cs` + +**YAML Declaration:** +```yaml +toc: + - file: getting-started.md + - file: api/overview.md # Can deep-link + - hidden: 404.md # Hidden from navigation +``` + +**Key Features:** +- URL calculated dynamically from home provider + relative path +- Smart caching (see [Home Provider Architecture](home-provider-architecture.md)) +- Handles index files specially: `folder/index.md` → `/folder/` +- Can be hidden from navigation while remaining accessible + +**URL Calculation:** +```csharp +public string Url +{ + get + { + var rootUrl = _homeAccessor.HomeProvider.PathPrefix.TrimEnd('/'); + var relativePath = DetermineRelativePath(); + + // Remove .md extension + var path = relativePath.EndsWith(".md") ? relativePath[..^3] : relativePath; + + // Handle index files + if (path.EndsWith("/index")) + path = path[..^6]; + else if (path.Equals("index")) + return string.IsNullOrEmpty(rootUrl) ? "/" : $"{rootUrl}/"; + + return $"{rootUrl}/{path.TrimEnd('/')}/"; + } +} +``` + +**Example:** +``` +File: docs/api/rest.md +PathPrefix: "/guide" +URL: /guide/api/rest/ + +File: docs/index.md +PathPrefix: "/guide" +URL: /guide/ +``` + +**Constructor:** +```csharp +public FileNavigationLeaf( + TModel model, // The documentation file model + IFileInfo fileInfo, // File system info + FileNavigationArgs args) // Construction arguments +``` + +**Arguments:** +- `RelativePathToDocumentationSet` - Path from docset root (for URL calculation) +- `RelativePathToTableOfContents` - Path from TOC root (for assembler builds) +- `Hidden` - Whether hidden from navigation +- `NavigationIndex` - Initial index (will be recalculated) +- `Parent` - Parent node +- `HomeAccessor` - For accessing path prefix and navigation root + +--- + +### CrossLinkNavigationLeaf + +Represents a link to external documentation or different documentation set. + +**Location:** `src/Elastic.Documentation.Navigation/Isolated/CrossLinkNavigationLeaf.cs` + +**YAML Declaration:** +```yaml +toc: + - title: "External Guide" + crosslink: https://example.com/guide + - title: "Other Docset" + crosslink: docs-content://guide.md +``` + +**Key Features:** +- URL is the crosslink itself (not calculated) +- Can link to external sites or use crosslink scheme +- Title is required (no auto-title from file) +- Always marked as `IsCrossLink = true` + +**Constructor:** +```csharp +public CrossLinkNavigationLeaf( + CrossLinkModel model, // Contains Uri and title + string url, // The crosslink URL + bool hidden, // Hidden from navigation? + INodeNavigationItem<...>? parent, // Parent node + INavigationHomeAccessor homeAccessor) // For navigation root +``` + +**Example:** +```csharp +new CrossLinkNavigationLeaf( + new CrossLinkModel(new Uri("https://elastic.co"), "Elastic Docs"), + "https://elastic.co", + hidden: false, + parent: this, + homeAccessor: this +) +// URL: https://elastic.co +// NavigationTitle: "Elastic Docs" +``` + +--- + +## Node Types (With Children) + +Node types can have child navigation items. They represent structural elements of the documentation. + +### FolderNavigation + +Represents a directory in the file system with markdown files. + +**Location:** `src/Elastic.Documentation.Navigation/Isolated/FolderNavigation.cs` + +**YAML Declaration:** +```yaml +toc: + - folder: getting-started + # Auto-discovers markdown files in the folder + + - folder: api + children: + - file: index.md + - file: rest.md + # Explicit children, no auto-discovery +``` + +**Key Features:** +- URL is the same as its `Index` property +- Index is either `index.md` or first file +- Can auto-discover markdown files if no children specified +- Children paths are scoped to the folder + +**Properties:** +```csharp +public class FolderNavigation +{ + public string FolderPath { get; } // Relative path to folder + public ILeafNavigationItem Index { get; } // Folder's index file + public IReadOnlyCollection NavigationItems { get; } // Children +} +``` + +**URL:** +```csharp +public string Url => Index.Url; // Same as index file +``` + +**Example:** +``` +Folder: docs/getting-started/ +Files: + - index.md + - install.md + - configure.md + +Navigation: +FolderNavigation + Index: getting-started/index.md → /getting-started/ + NavigationItems: + - install.md → /getting-started/install/ + - configure.md → /getting-started/configure/ +``` + +--- + +### VirtualFileNavigation + +Represents a file with children defined in YAML (not file system structure). + +**Location:** `src/Elastic.Documentation.Navigation/Isolated/VirtualFileNavigation.cs` + +**YAML Declaration:** +```yaml +toc: + - file: getting-started.md + children: + - file: install.md + - file: configure.md + # Children can be anywhere in the file system +``` + +**Key Features:** +- Allows grouping files without matching file system structure +- Index is the file itself +- Children don't have to be in the same directory +- URL is the same as its `Index` property + +**Properties:** +```csharp +public class VirtualFileNavigation +{ + public ILeafNavigationItem Index { get; } // The file itself + public IReadOnlyCollection NavigationItems { get; } // Virtual children +} +``` + +**Example:** +``` +File: docs/getting-started.md +Children (defined in YAML): + - docs/install.md + - docs/setup.md + +Navigation: +VirtualFileNavigation + Index: getting-started.md → /getting-started/ + NavigationItems: + - install.md → /install/ + - setup.md → /setup/ +``` + +**Use Cases:** +- Grouping related files that aren't in the same directory +- Creating navigation structure independent of file structure +- Collecting files under a parent concept + +**Best Practice:** Use sparingly. Prefer `FolderNavigation` when file structure can match navigation structure. + +--- + +## Root Node Types + +Root nodes can be re-homed in assembler builds. They implement `IRootNavigationItem`. + +### DocumentationSetNavigation + +Represents the root navigation for a documentation set (`docset.yml`). + +**Location:** `src/Elastic.Documentation.Navigation/Isolated/DocumentationSetNavigation.cs` + +**Source:** `docset.yml` file + +**Key Features:** +- Root of navigation tree in isolated builds +- Can be re-homed in assembler builds +- Creates home provider scope +- Implements both `INavigationHomeProvider` and `INavigationHomeAccessor` +- Has unique identifier: `{repository}://` + +**Properties:** +```csharp +public class DocumentationSetNavigation + : IRootNavigationItem + , INavigationHomeProvider + , INavigationHomeAccessor +{ + public Uri Identifier { get; } // e.g., elastic-docs:// + public string PathPrefix { get; } // URL prefix for this docset + public GitCheckoutInformation Git { get; } // Repository info + public INavigationHomeProvider HomeProvider { get; set; } // For re-homing! + + public ILeafNavigationItem Index { get; } // Docset index + public IReadOnlyCollection NavigationItems { get; } // Top-level items + public bool IsUsingNavigationDropdown { get; } // From features.primary-nav +} +``` + +**Isolated Build:** +```csharp +// In isolated builds, it's its own home provider +DocumentationSetNavigation +{ + NavigationRoot = this, + HomeProvider = this, + PathPrefix = "", + Identifier = new Uri("elastic-docs://") +} +// Child URL: /api/rest/ +``` + +**Assembler Build (Re-homed):** +```csharp +// In assembler builds, re-homed to site navigation +DocumentationSetNavigation +{ + NavigationRoot = SiteNavigation, // Changed! + HomeProvider = new NavigationHomeProvider( // Changed! + pathPrefix: "/guide", + navigationRoot: SiteNavigation + ), + PathPrefix = "/guide", // From new provider + Identifier = new Uri("elastic-docs://") // Unchanged +} +// Child URL: /guide/api/rest/ +``` + +**Re-homing:** +```csharp +// This is all it takes! +docsetNav.HomeProvider = new NavigationHomeProvider("/guide", siteNav); +``` + +--- + +### TableOfContentsNavigation + +Represents a nested `toc.yml` file within a documentation set. + +**Location:** `src/Elastic.Documentation.Navigation/Isolated/TableOfContentsNavigation.cs` + +**Source:** `toc.yml` file + +**YAML Declaration (in docset.yml or parent toc.yml):** +```yaml +toc: + - toc: api + - toc: guides +``` + +**Key Features:** +- **Creates a scope only in assembler builds** (for independent re-homing) +- In isolated builds, inherits HomeProvider from DocumentationSetNavigation +- Can be re-homed independently in assembler builds +- Implements both `INavigationHomeProvider` and `INavigationHomeAccessor` +- Has unique identifier: `{repository}://{path}` +- Cannot have children defined in YAML (children come from toc.yml file) + +**Properties:** +```csharp +public class TableOfContentsNavigation + : IRootNavigationItem + , INavigationHomeProvider + , INavigationHomeAccessor +{ + public Uri Identifier { get; } // e.g., elastic-docs://api + public string ParentPath { get; } // Path to toc folder + public string PathPrefix { get; } // URL prefix + public IDirectoryInfo TableOfContentsDirectory { get; } // Physical directory + public INavigationHomeProvider HomeProvider { get; set; } // For re-homing! + + public ILeafNavigationItem Index { get; } // TOC index + public IReadOnlyCollection NavigationItems { get; } // TOC items +} +``` + +**Example:** +``` +Docset: elastic-docs +TOC: api/toc.yml + +In isolated build: +TableOfContentsNavigation +{ + Identifier = new Uri("elastic-docs://api"), + ParentPath = "api", + PathPrefix = "", + NavigationRoot = DocumentationSetNavigation, + HomeProvider = DocumentationSetNavigation.HomeProvider // ← Inherited, no new scope +} +// Child URL: /api/rest/ + +In assembler build (creates its own scope for re-homing): +TableOfContentsNavigation +{ + Identifier = new Uri("elastic-docs://api"), + ParentPath = "api", + PathPrefix = "/reference", // Different from docset! + NavigationRoot = SiteNavigation, + HomeProvider = new NavigationHomeProvider( // ← New scope created! + pathPrefix: "/reference", + navigationRoot: SiteNavigation + ) +} +// Child URL: /reference/rest/ +``` + +**Re-homing:** +```csharp +// TOCs can be re-homed independently from their parent docset! +tocNav.HomeProvider = new NavigationHomeProvider("/reference", siteNav); +``` + +**Use Case:** Allows assembler builds to split a docset across multiple site sections. + +--- + +### SiteNavigation + +Represents the root navigation for an assembled documentation site. + +**Location:** `src/Elastic.Documentation.Navigation/Assembler/SiteNavigation.cs` + +**Source:** `config/navigation.yml` + +**Key Features:** +- Only exists in assembler builds +- Ultimate root of the navigation tree +- Re-homes child DocumentationSetNavigation and TableOfContentsNavigation nodes +- Manages `path_prefix` mappings from navigation.yml +- Tracks phantom nodes (declared but not included) +- Has unique identifier: `site://` + +**Properties:** +```csharp +public class SiteNavigation + : IRootNavigationItem +{ + public Uri Identifier { get; } = new Uri("site://"); + public string Url { get; } // Site prefix or "/" + + // All docset/TOC nodes indexed by identifier + public IReadOnlyDictionary> Nodes { get; } + + // Top-level navigation items + public ILeafNavigationItem Index { get; } + public IReadOnlyCollection NavigationItems { get; } + + // Phantom tracking + public IReadOnlyCollection Phantoms { get; } + public HashSet DeclaredPhantoms { get; } + public ImmutableHashSet DeclaredTableOfContents { get; } +} +``` + +**Example:** +```yaml +# config/navigation.yml +toc: + - toc: elastic-docs:// + path_prefix: guide + + - toc: elastic-docs://api + path_prefix: reference + +phantoms: + - source: plugins:// # Not included in navigation +``` + +```csharp +SiteNavigation +{ + Identifier = new Uri("site://"), + NavigationRoot = this, + Nodes = { + [new Uri("elastic-docs://")] = DocumentationSetNavigation { ... }, + [new Uri("elastic-docs://api")] = TableOfContentsNavigation { ... }, + }, + NavigationItems = [ + DocumentationSetNavigation (re-homed to /guide), + TableOfContentsNavigation (re-homed to /reference) + ] +} +``` + +**Re-homing Logic:** +```csharp +// From SiteNavigation.cs:211 +private INavigationItem? CreateSiteTableOfContentsNavigation(...) +{ + var pathPrefix = $"{_sitePrefix}/{tocRef.PathPrefix}".Trim('/'); + + // Look up the node + if (!_nodes.TryGetValue(tocRef.Source, out var node)) + return null; + + // Re-home it! + homeAccessor.HomeProvider = new NavigationHomeProvider(pathPrefix, root); + + // All URLs in subtree now use pathPrefix! + return node; +} +``` + +--- + +## Type Comparison Table + +| Type | Has Children | Is Root | Can Be Re-homed | Creates Scope | URL Source | +|------|-------------|---------|-----------------|---------------|------------| +| **FileNavigationLeaf** | ❌ | ❌ | ❌ | ❌ | Calculated from path + prefix | +| **CrossLinkNavigationLeaf** | ❌ | ❌ | ❌ | ❌ | Crosslink URI itself | +| **FolderNavigation** | ✅ | ❌ | ❌ | ❌ | Same as Index | +| **VirtualFileNavigation** | ✅ | ❌ | ❌ | ❌ | Same as Index | +| **DocumentationSetNavigation** | ✅ | ✅ | ✅ | ✅ (always) | Same as Index | +| **TableOfContentsNavigation** | ✅ | ✅ | ✅ | ✅ (assembler only) | Same as Index | +| **SiteNavigation** | ✅ | ✅ | ❌ | ✅ (always) | Site prefix or "/" | + +**Note:** TableOfContentsNavigation only creates its own scope in assembler builds to enable independent re-homing. In isolated builds, it inherits the HomeProvider from its parent DocumentationSetNavigation. + +## Factory Methods + +Navigation items are created through factory methods in `DocumentationNavigationFactory`: + +```csharp +public static class DocumentationNavigationFactory +{ + // Create a file leaf + public static ILeafNavigationItem CreateFileNavigationLeaf( + TModel model, + IFileInfo fileInfo, + FileNavigationArgs args) + where TModel : IDocumentationFile + => new FileNavigationLeaf(model, fileInfo, args) + { NavigationIndex = args.NavigationIndex }; + + // Create a virtual file node + public static VirtualFileNavigation CreateVirtualFileNavigation( + TModel model, + IFileInfo fileInfo, + VirtualFileNavigationArgs args) + where TModel : IDocumentationFile + => new(model, fileInfo, args) + { NavigationIndex = args.NavigationIndex }; +} +``` + +**Why Factory Methods?** +- Encapsulate creation logic +- Ensure consistent initialization (NavigationIndex) +- Type-safe generic construction +- Centralize instantiation + +## Model Types + +All navigation items work with models that implement `IDocumentationFile`: + +```csharp +public interface IDocumentationFile : INavigationModel +{ + string NavigationTitle { get; } +} +``` + +**Built-in Models:** + +### CrossLinkModel +```csharp +public record CrossLinkModel(Uri CrossLinkUri, string NavigationTitle) + : IDocumentationFile; +``` + +### SiteNavigationNoIndexFile +```csharp +public record SiteNavigationNoIndexFile(string NavigationTitle) + : IDocumentationFile; +``` + +**Custom Models:** + +You can create custom models for specialized documentation types: + +```csharp +public record ApiDocumentationFile( + string NavigationTitle, + string ApiVersion, + ApiType Type +) : IDocumentationFile; + +// Use with generic navigation +var navigation = new DocumentationSetNavigation( + docset, + context, + new ApiDocumentationFileFactory() +); +``` + +## Summary + +The navigation system provides: + +- **7 node types** - 2 leaves, 3 nodes, 3 roots +- **Generic design** - Works with any `IDocumentationFile` model +- **Flexible structure** - Files, folders, TOCs, virtual files +- **Re-homing** - Roots can change URL prefix in O(1) +- **Scope isolation** - Each root creates its own URL scope +- **Type safety** - Factory methods ensure correct construction + +For implementation details, see the source code in: +- `src/Elastic.Documentation.Navigation/Isolated/` - Individual node types +- `src/Elastic.Documentation.Navigation/Assembler/` - Site assembly diff --git a/docs/development/navigation/two-phase-loading.md b/docs/development/navigation/two-phase-loading.md new file mode 100644 index 000000000..055020f46 --- /dev/null +++ b/docs/development/navigation/two-phase-loading.md @@ -0,0 +1,314 @@ +# Two-Phase Loading + +Navigation construction splits into two distinct phases: configuration resolution and navigation building. + +## Why Two Phases? + +Building navigation requires two fundamentally different operations: +1. **Loading configuration** - Parse YAML, check files exist, resolve paths +2. **Building structure** - Create tree, set relationships, calculate URLs + +These operations have different concerns: + +| Aspect | Configuration | Navigation | +|--------|--------------|------------| +| **Input** | File system + YAML | Resolved paths | +| **Validation** | Files exist, YAML valid | Tree structure valid | +| **Errors** | Missing files, bad YAML | Empty TOCs, broken links | +| **Changes when** | YAML format changes | Tree logic changes | +| **Testing needs** | Mock file system | Mock config objects | + +Mixing them creates coupling. Configuration parsing shouldn't know about tree structure. Tree building shouldn't touch the file system. + +**Concrete benefits:** + +**Error messages are clearer:** +``` +Phase 1: File 'api/missing.md' not found at /docs/api/missing.md +Phase 2: Folder 'setup' has children defined but none could be created +``` +You immediately know which layer failed. + +**Testing is simpler:** +```csharp +// Phase 1 test: Does path resolution work? +[Fact] void ResolvesNestedPaths() { /* mock file system */ } + +// Phase 2 test: Does tree structure work? +[Fact] void CreatesNavigationTree() { /* mock config, no files */ } +``` +Each phase tests one thing. + +**Configuration reuses:** +```csharp +// Parse once +var config = DocumentationSetFile.LoadAndResolve(yaml, fileSystem); + +// Build multiple ways +var isolated = new DocumentationSetNavigation(config, isolatedContext, factory); +var assembled = new SiteNavigation(siteConfig, context, [isolated], prefix); +``` +Same configuration, different navigation structures. + +**Separation of concerns:** +- Change YAML format → only Phase 1 changes +- Change URL calculation → only Phase 2 changes +- Swap YAML for JSON → only Phase 1 changes +- Add new node type → only Phase 2 changes + +## Phase 1: Configuration Resolution + +**Package:** `Elastic.Documentation.Configuration` + +**Goal:** Parse YAML → Resolve paths → Validate existence + +``` +Raw YAML + File System → Fully resolved configuration +``` + +**What it does:** +1. Parse YAML files (`docset.yml`, `toc.yml`, `navigation.yml`) +2. Resolve relative paths to absolute paths from docset root +3. Validate files exist on disk +4. Load nested `toc.yml` files recursively +5. Emit configuration errors + +**Example:** +```csharp +// In: Raw YAML +toc: + - toc: api + # api/toc.yml contains: + toc: + - file: rest.md # Relative to api/toc.yml + +// Out: Fully resolved +FileRef { + PathRelativeToDocumentationSet = "api/rest.md" // ✓ From docset root +} +``` + +**Key point:** All paths become relative to docset root. No more file I/O needed. + +## Phase 2: Navigation Construction + +**Package:** `Elastic.Documentation.Navigation` + +**Goal:** Build tree → Calculate URLs → Set relationships + +``` +Resolved Configuration → Navigation tree with URLs +``` + +**What it does:** +1. Create node objects from configuration +2. Set parent-child relationships +3. Set up home providers (for URL calculation) +4. Calculate navigation indexes +5. Emit navigation errors + +**Example:** +```csharp +// In: Resolved configuration +FileRef { PathRelativeToDocumentationSet = "api/rest.md" } + +// Out: Navigation with URL +FileNavigationLeaf { + Url = "/api/rest/", + Parent = TableOfContentsNavigation, + NavigationRoot = DocumentationSetNavigation +} +``` + +**Key point:** URLs calculated dynamically from HomeProvider. No stored paths. + +## The Flow + +``` +┌─────────────────────────────────────┐ +│ Phase 1: Configuration │ +├─────────────────────────────────────┤ +│ YAML files + File system │ +│ ↓ │ +│ Parse & validate │ +│ ↓ │ +│ Resolve all paths │ +│ ↓ │ +│ DocumentationSetFile │ +│ (all paths relative to docset root) │ +└─────────────────────────────────────┘ + ↓ +┌─────────────────────────────────────┐ +│ Phase 2: Navigation │ +├─────────────────────────────────────┤ +│ Resolved configuration │ +│ ↓ │ +│ Build tree │ +│ ↓ │ +│ Set relationships │ +│ ↓ │ +│ Set up URL providers │ +│ ↓ │ +│ DocumentationSetNavigation │ +│ (complete tree with URLs) │ +└─────────────────────────────────────┘ +``` + +## Path Resolution Example + +**Before Phase 1:** +```yaml +# docset.yml +toc: + - toc: api + +# api/toc.yml +toc: + - file: rest.md # ← Relative to api/ + - file: graphql.md # ← Relative to api/ +``` + +**After Phase 1:** +```csharp +IsolatedTableOfContentsRef { + PathRelativeToDocumentationSet = "api", + Children = [ + FileRef { PathRelativeToDocumentationSet = "api/rest.md" }, // ✓ + FileRef { PathRelativeToDocumentationSet = "api/graphql.md" } // ✓ + ] +} +``` + +All paths now relative to docset root. Phase 2 can build without touching filesystem. + +## Error Attribution + +Clear errors because phases are separate: + +**Phase 1 errors (configuration):** +``` +Error: File 'api/missing.md' not found at /docs/api/missing.md +Error: TableOfContents 'api' cannot have children in docset.yml +``` +→ Fix your YAML or add the file. + +**Phase 2 errors (navigation):** +``` +Error: Documentation set has no table of contents defined +Error: Folder 'setup' has children defined but none could be created +``` +→ Fix your navigation structure. + +## Testing Benefits + +**Phase 1 tests:** +```csharp +[Fact] +public void LoadAndResolve_ResolvesNestedPaths() +{ + var yaml = "toc:\n - toc: api"; + var fs = new MockFileSystem(); + fs.AddFile("/docs/api/toc.yml", "toc:\n - file: rest.md"); + + var docset = DocumentationSetFile.LoadAndResolve( + collector, yaml, fs.NewDirInfo("/docs") + ); + + var fileRef = docset.TableOfContents[0].Children[0] as FileRef; + Assert.Equal("api/rest.md", fileRef.PathRelativeToDocumentationSet); +} +``` +Tests YAML parsing and path resolution. + +**Phase 2 tests:** +```csharp +[Fact] +public void Constructor_CreatesNavigationTree() +{ + // Pre-resolved configuration (no file I/O!) + var docset = new DocumentationSetFile { + TableOfContents = [ + new FileRef { PathRelativeToDocumentationSet = "index.md" } + ] + }; + + var nav = new DocumentationSetNavigation( + docset, context, factory + ); + + Assert.Equal("/", nav.Index.Url); +} +``` +Tests tree construction without file system. + +## Reusability + +Same configuration works for both build modes: + +```csharp +// Phase 1: Build configuration once +var docset = DocumentationSetFile.LoadAndResolve( + collector, yaml, fileSystem.NewDirInfo("/docs") +); + +// Phase 2a: Isolated build +var isolatedNav = new DocumentationSetNavigation( + docset, // ← Same config + isolatedContext, + factory +); +// URLs: /api/rest/ + +// Phase 2b: Assembler build +var siteNav = new SiteNavigation( + siteConfig, + assemblerContext, + [isolatedNav], // ← Reuse isolated navigation + sitePrefix: null +); +// Re-home: /api/rest/ → /elasticsearch/api/rest/ +``` + +## Assembler Extension + +Assembler adds two more phases: + +``` +Phase 1a: Load individual docset configs + ↓ +Phase 2a: Build isolated navigations + ↓ +Phase 1b: Load site navigation config + ↓ +Phase 2b: Assemble + re-home +``` + +Each docset goes through Phases 1 & 2 independently, then site navigation assembles them. + +## Key Invariants + +**After Phase 1:** +- ✅ All paths relative to docset root +- ✅ All files validated to exist +- ✅ All nested TOCs loaded +- ✅ Configuration structure validated + +**After Phase 2:** +- ✅ Complete navigation tree +- ✅ All relationships set (parent/child/root) +- ✅ All home providers configured +- ✅ All URLs calculable + +## Summary + +| Aspect | Phase 1 | Phase 2 | +|--------|---------|---------| +| **Package** | `Configuration` | `Navigation` | +| **Input** | YAML + File system | Resolved config | +| **Output** | Resolved config | Navigation tree | +| **Errors** | Config/file issues | Structure issues | +| **File I/O** | Yes | No | +| **Testing** | Mock file system | Mock config | +| **Reusable** | Yes (both builds) | Build-specific | + +**The key insight:** Configuration is about files and YAML. Navigation is about tree structure and URLs. Keep them separate. diff --git a/docs/development/navigation/visual-walkthrough.md b/docs/development/navigation/visual-walkthrough.md new file mode 100644 index 000000000..ba0a22bc5 --- /dev/null +++ b/docs/development/navigation/visual-walkthrough.md @@ -0,0 +1,545 @@ +# Visual Walkthrough + +This document provides a visual tour of navigation structures in both isolated and assembler builds, showing how the same documentation can be represented differently depending on the build mode. + +## Navigation Node Icons + +Throughout this walkthrough, we use these icons to represent different node types: + +- ![DocumentationSetNavigation](images/bullet-documentation-set-navigation.svg) **DocumentationSetNavigation** - Root of a documentation repository +- ![TableOfContentsNavigation](images/bullet-table-of-contents-navigation.svg) **TableOfContentsNavigation** - A nested `toc.yml` section +- ![FolderNavigation](images/bullet-folder-navigation.svg) **FolderNavigation** - A directory with markdown files +- ![FileNavigationLeaf](images/bullet-file-navigation-leaf.svg) **FileNavigationLeaf** - An individual markdown file +- ![SiteNavigation](images/bullet-site-navigation.svg) **SiteNavigation** - Root of an assembled site (assembler builds only) + +## Isolated Builds + +When building a single repository's documentation (e.g., `docs-builder isolated build`), the navigation is rooted at ![DocumentationSetNavigation](images/bullet-documentation-set-navigation.svg) **DocumentationSetNavigation**. + +### What's in an Isolated Build? + +An isolated build processes one `docset.yml` file, which defines the table of contents for that repository. This table of contents can include: + +**Direct Children:** +- ![FileNavigationLeaf](images/bullet-file-navigation-leaf.svg) **file:** - Individual markdown files +- ![FolderNavigation](images/bullet-folder-navigation.svg) **folder:** - Directories containing markdown files +- ![TableOfContentsNavigation](images/bullet-table-of-contents-navigation.svg) **toc:** - Nested `toc.yml` files for subsections + +**Example docset.yml:** +```yaml +project: elastic-project +toc: + - file: index.md # FileNavigationLeaf + - folder: getting-started # FolderNavigation + - toc: api # TableOfContentsNavigation → api/toc.yml + - toc: guides # TableOfContentsNavigation → guides/toc.yml +``` + +### Visual Example: Isolated Build Tree + +Imagine we have an `elastic-project` repository with the following structure: + +![Isolated Build](images/isolated-build-tree.svg) + +**What we see in this diagram:** + +1. **Root:** ![DocumentationSetNavigation](images/bullet-documentation-set-navigation.svg) `elastic-project://` + - This is the root of the entire navigation + - Identifier: `elastic-project://` + - URL: `/` (or custom base path) + +2. **Index File:** ![FileNavigationLeaf](images/bullet-file-navigation-leaf.svg) `index.md` + - The landing page for the documentation + - URL: `/` + +3. **Nested TOCs:** ![TableOfContentsNavigation](images/bullet-table-of-contents-navigation.svg) `api/` and `guides/` + - Each represents a `toc.yml` file + - Creates a scope for child navigation items + - Identifiers: `elastic-project://api`, `elastic-project://guides` + - URLs: `/api/`, `/guides/` + +4. **Child Files:** ![FileNavigationLeaf](images/bullet-file-navigation-leaf.svg) Under each TOC + - Individual markdown files in those sections + - URLs relative to parent TOC: `/api/overview/`, `/guides/getting-started/` + +### Key Characteristics of Isolated Builds + +**Navigation Root:** +- All nodes point to ![DocumentationSetNavigation](images/bullet-documentation-set-navigation.svg) as their `NavigationRoot` +- This is the ultimate parent in the tree + +**URL Structure:** +- All URLs are relative to the documentation set root +- Default: starts at `/` +- Can be customized with `--canonical-base-url` + +**Identifiers:** +- Docset: `{repository}://` (e.g., `elastic-project://`) +- Nested TOCs: `{repository}://{path}` (e.g., `elastic-project://api`) + +**Home Provider:** +- DocumentationSetNavigation is its own home provider +- PathPrefix: `""` (empty, or custom base URL) +- All children inherit this provider + +**Purpose:** +- Fast iteration for documentation teams +- Test documentation in isolation +- No dependencies on other repositories +- Validate links, structure, and content + +--- + +## Assembler Builds + +When building a unified site from multiple repositories (e.g., `docs-builder assemble`), the navigation is rooted at ![SiteNavigation](images/bullet-site-navigation.svg) **SiteNavigation**. + +### What's in an Assembler Build? + +An assembler build combines multiple isolated builds into a single site, defined by `config/navigation.yml`. + +**Key Differences from Isolated:** +- Multiple repositories combined into one navigation tree +- Custom URL prefixes for each section (`path_prefix`) +- ![SiteNavigation](images/bullet-site-navigation.svg) SiteNavigation as the ultimate root +- Navigation items are **re-homed** to use new URL prefixes + +### What Can Be Referenced? + +In `navigation.yml`, you can reference: + +1. **Entire Documentation Sets:** ![DocumentationSetNavigation](images/bullet-documentation-set-navigation.svg) + - Syntax: `{repository}://` + - Example: `elastic-project://`, `kibana://`, `elasticsearch://` + +2. **Nested Table of Contents:** ![TableOfContentsNavigation](images/bullet-table-of-contents-navigation.svg) + - Syntax: `{repository}://{path/to/toc}` + - Example: `elastic-project://api`, `kibana://setup` + +**You cannot directly reference:** +- Individual files +- Folders +- Virtual files + +These are automatically included as children of their parent TOC or docset. + +### Visual Example: Splitting a Docset + +Let's take the same `elastic-project` from the isolated build and split it across the site: + +**Isolated Build Had:** +``` +elastic-project:// +├── /api/ +└── /guides/ +``` + +**Assembler Navigation (navigation.yml):** +```yaml +toc: + - toc: docs-content://elasticsearch + children: + # Pull elastic-project's API section + - toc: elastic-project://api + path_prefix: elasticsearch/api + + # Pull elastic-project's Guides section + - toc: elastic-project://guides + path_prefix: elasticsearch/guides +``` + +**What This Means:** +- The `api` section moves from `/api/` → `/elasticsearch/api/` +- The `guides` section moves from `/guides/` → `/elasticsearch/guides/` +- Same files, same structure, different URLs + +### Visual Example: Composing Multiple Repositories + +Here's a more complex example showing multiple repositories assembled into one site: + +![TableOfContentsNavigation](images/bullet-table-of-contents-navigation.svg) **Site Structure (navigation.yml):** +``` +Site Root +├── Elasticsearch Section +│ ├── elastic-project://api → /elasticsearch/api +│ └── elastic-project://guides → /elasticsearch/guides +│ +├── Kibana Section +│ └── kibana:// → /kibana +│ +├── Logstash Section +│ └── logstash:// → /logstash +│ +└── Client Libraries + ├── elastic-client-node:// → /clients/node + ├── elastic-client-dotnet:// → /clients/dotnet + └── elastic-client-java:// → /clients/java +``` + +**Corresponding navigation.yml:** +```yaml +toc: + # Elasticsearch section + - toc: elasticsearch + children: + - toc: elastic-project://api + path_prefix: elasticsearch/api + - toc: elastic-project://guides + path_prefix: elasticsearch/guides + + # Kibana section + - toc: kibana:// + path_prefix: kibana + + # Logstash section + - toc: logstash:// + path_prefix: logstash + + # Client libraries + - toc: clients + children: + - toc: elastic-client-node:// + path_prefix: clients/node + - toc: elastic-client-dotnet:// + path_prefix: clients/dotnet + - toc: elastic-client-java:// + path_prefix: clients/java +``` + +### Visual Example: Fully Resolved Assembler Build + +When the assembler processes `navigation.yml`, it creates this structure: + +![Assembler Build](images/assembler-build-tree.svg) + +**What we see in this diagram:** + +1. **Ultimate Root:** ![SiteNavigation](images/bullet-site-navigation.svg) `site://` + - Root of the entire assembled site + - Identifier: `site://` + - All nodes ultimately point here as `NavigationRoot` + +2. **Re-homed Documentation Sets:** ![DocumentationSetNavigation](images/bullet-documentation-set-navigation.svg) + - Each repository's docset is re-homed with a custom `path_prefix` + - Example: `elastic-project://` moves from `/` → `/elasticsearch/` + - Home provider updated to use new prefix + +3. **Re-homed Nested TOCs:** ![TableOfContentsNavigation](images/bullet-table-of-contents-navigation.svg) + - Individual TOCs can be pulled out and placed independently + - Example: `elastic-project://api` moves from `/api/` → `/elasticsearch/api/` + - Separate from its parent docset in the site structure + +4. **Child Files:** ![FileNavigationLeaf](images/bullet-file-navigation-leaf.svg) + - Files inherit the new path prefix from their parent + - URLs automatically recalculate based on new home provider + - No code changes needed - it's dynamic! + +### Key Characteristics of Assembler Builds + +**Navigation Root:** +- All nodes point to ![SiteNavigation](images/bullet-site-navigation.svg) as their `NavigationRoot` +- This is the ultimate parent for the entire site + +**URL Structure:** +- URLs use custom `path_prefix` from `navigation.yml` +- Example: `/elasticsearch/api/overview/`, `/kibana/setup/install/` +- Prefixes must be unique across the site + +**Re-homing Process:** +- Each referenced node gets a new home provider +- New provider has custom `PathPrefix` and `NavigationRoot = SiteNavigation` +- One line of code: `node.HomeProvider = new NavigationHomeProvider(pathPrefix, siteNav)` +- All descendant URLs update automatically (O(1) operation!) + +**Identifiers:** +- Site: `site://` +- Docsets: `{repository}://` (unchanged from isolated) +- TOCs: `{repository}://{path}` (unchanged from isolated) +- Identifiers don't change - they're stable across build modes + +**Purpose:** +- Unified navigation across multiple repositories +- Organize docs by product/feature, not repository +- Custom URL structure independent of repository structure +- Single site with consistent navigation + +--- + +## Comparing Build Modes + +### Same Content, Different URLs + +Here's how the same file appears in different builds: + +**File:** `elastic-project/api/overview.md` + +**Isolated Build:** +``` +DocumentationSetNavigation (elastic-project://) + └── TableOfContentsNavigation (elastic-project://api) + └── FileNavigationLeaf (api/overview.md) + Url: /api/overview/ + NavigationRoot: DocumentationSetNavigation + HomeProvider.PathPrefix: "" +``` + +**Assembler Build:** +``` +SiteNavigation (site://) + └── TableOfContentsNavigation (elastic-project://api) RE-HOMED! + └── FileNavigationLeaf (api/overview.md) + Url: /elasticsearch/api/overview/ ← Different! + NavigationRoot: SiteNavigation ← Different! + HomeProvider.PathPrefix: "/elasticsearch/api" ← Different! +``` + +**Key Insight:** Same node objects, different URLs. No tree reconstruction! + +### Flexibility in Assembly + +The assembler gives you complete freedom to reorganize: + +**Scenario 1: Keep Docset Together** +```yaml +- toc: elastic-project:// + path_prefix: elasticsearch +``` +Result: All of `elastic-project` under `/elasticsearch/` + +**Scenario 2: Split Docset Apart** +```yaml +- toc: elastic-project://api + path_prefix: reference/api + +- toc: elastic-project://guides + path_prefix: learn/guides +``` +Result: API under `/reference/api/`, guides under `/learn/guides/` + +**Scenario 3: Nest Docsets** +```yaml +- toc: products + children: + - toc: elasticsearch:// + path_prefix: products/elasticsearch + - toc: kibana:// + path_prefix: products/kibana +``` +Result: Products organized hierarchically + +### Navigation Requirements + +**Both Builds:** +- Every node must have a unique URL +- Navigation must form a tree (no cycles) +- Root nodes must have an index file + +**Isolated Only:** +- Root is always DocumentationSetNavigation +- URLs relative to docset root + +**Assembler Only:** +- Root is always SiteNavigation +- `path_prefix` required for each reference (except narrative repo) +- `path_prefix` values must be unique across the site +- Must declare all referenced docsets/TOCs + +--- + +## Working with the Visual Structure + +### Adding Files to Navigation + +**In isolated build (docset.yml):** +```yaml +toc: + - file: new-guide.md # Adds FileNavigationLeaf +``` +Result: ![FileNavigationLeaf](images/bullet-file-navigation-leaf.svg) at `/new-guide/` + +**In assembler build:** +The file is automatically included as a child of its parent TOC/docset. No changes needed to `navigation.yml`. + +### Adding a Nested TOC + +**In isolated build (docset.yml):** +```yaml +toc: + - toc: new-section # References new-section/toc.yml +``` +Result: ![TableOfContentsNavigation](images/bullet-table-of-contents-navigation.svg) at `/new-section/` + +**In assembler build (navigation.yml):** +```yaml +toc: + - toc: elastic-project://new-section + path_prefix: elasticsearch/new-section +``` +Result: ![TableOfContentsNavigation](images/bullet-table-of-contents-navigation.svg) at `/elasticsearch/new-section/` + +### Moving Content in Assembly + +Want to reorganize the site? Just update `path_prefix` in `navigation.yml`: + +**Before:** +```yaml +- toc: elastic-project://api + path_prefix: api +``` +URLs: `/api/overview/`, `/api/rest/` + +**After:** +```yaml +- toc: elastic-project://api + path_prefix: reference/elasticsearch/api +``` +URLs: `/reference/elasticsearch/api/overview/`, `/reference/elasticsearch/api/rest/` + +No changes to the repository, no code changes, just configuration! + +--- + +## Phantom Nodes + +Sometimes you want to reference a docset/TOC in configuration but not include it in the assembled navigation. These are **phantoms**. + +**Example (navigation.yml):** +```yaml +phantoms: + - source: plugins:// + - source: cloud://monitoring +``` + +**What This Means:** +- `plugins://` and `cloud://monitoring` are acknowledged to exist +- They're not included in the site navigation tree +- Cross-links to them won't trigger "undeclared navigation" warnings +- Useful for: + - Work-in-progress sections + - Legacy content being phased out + - External content that's referenced but not hosted + +**Visual Representation:** +``` +SiteNavigation (site://) +├── elasticsearch:// (included, solid line) +├── kibana:// (included, solid line) +└── plugins:// (phantom, dotted line - not in tree) +``` + +--- + +## Common Patterns + +### Pattern 1: Product-Centric Organization + +```yaml +toc: + - toc: elasticsearch + children: + - toc: es-guide:// + path_prefix: elasticsearch/guide + - toc: es-reference:// + path_prefix: elasticsearch/reference + + - toc: kibana + children: + - toc: kibana-guide:// + path_prefix: kibana/guide + - toc: kibana-reference:// + path_prefix: kibana/reference +``` + +Each product has guide and reference sections under a common prefix. + +### Pattern 2: Audience-Centric Organization + +```yaml +toc: + - toc: getting-started + children: + - toc: es-guide://getting-started + path_prefix: getting-started/elasticsearch + - toc: kibana-guide://getting-started + path_prefix: getting-started/kibana + + - toc: advanced + children: + - toc: es-reference://advanced + path_prefix: advanced/elasticsearch + - toc: kibana-reference://advanced + path_prefix: advanced/kibana +``` + +Content organized by user journey, pulling from multiple repositories. + +### Pattern 3: Client Libraries Collection + +```yaml +toc: + - toc: clients + children: + - toc: java-client:// + path_prefix: clients/java + - toc: dotnet-client:// + path_prefix: clients/dotnet + - toc: python-client:// + path_prefix: clients/python +``` + +All client libraries under one section, each with its own prefix. + +--- + +## Troubleshooting Visual Issues + +### "Why isn't my TOC showing up?" + +Check if: +1. It's referenced in `navigation.yml` with a `path_prefix` +2. The identifier matches exactly (case-sensitive!) +3. It's not declared as a phantom +4. The parent docset is built and available + +### "Why are URLs wrong?" + +Check: +1. `path_prefix` in `navigation.yml` is correct +2. No duplicate `path_prefix` values +3. HomeProvider was set correctly during re-homing +4. Cache hasn't gone stale (HomeProvider ID changed?) + +### "Why can't I reference a file directly?" + +You can't! The assembler only works with: +- ![DocumentationSetNavigation](images/bullet-documentation-set-navigation.svg) Entire docsets (`repo://`) +- ![TableOfContentsNavigation](images/bullet-table-of-contents-navigation.svg) Nested TOCs (`repo://path`) + +Files are automatically included as children of their parent. + +--- + +## Summary + +**Isolated Builds:** +- Single repository +- ![DocumentationSetNavigation](images/bullet-documentation-set-navigation.svg) DocumentationSetNavigation as root +- URLs relative to `/` +- Fast iteration, no dependencies + +**Assembler Builds:** +- Multiple repositories +- ![SiteNavigation](images/bullet-site-navigation.svg) SiteNavigation as root +- Custom `path_prefix` for each section +- Flexible organization across repository boundaries + +**The Magic:** +- Same node objects in both builds +- URLs calculated dynamically from HomeProvider +- Re-homing is O(1) - just change the provider reference +- No tree reconstruction needed + +For implementation details, see: +- [Assembler Process](assembler-process.md) - How assembly works +- [Home Provider Architecture](home-provider-architecture.md) - How re-homing works +- [Node Types](node-types.md) - Details on each node type diff --git a/docs/development/toc.yml b/docs/development/toc.yml index de74b4a73..f54f28324 100644 --- a/docs/development/toc.yml +++ b/docs/development/toc.yml @@ -6,4 +6,10 @@ toc: - folder: navigation children: - file: navigation.md + - file: visual-walkthrough.md + - file: first-principles.md + - file: two-phase-loading.md + - file: home-provider-architecture.md + - file: node-types.md + - file: assembler-process.md - toc: link-validation From dcd2c670e0db66334035bc3ea970326e1333788c Mon Sep 17 00:00:00 2001 From: Martijn Laarman Date: Mon, 3 Nov 2025 14:06:51 +0100 Subject: [PATCH 154/171] moar docs --- .../navigation/assembler-process.md | 728 +++++------------- .../navigation/first-principles.md | 235 +----- .../navigation/home-provider-architecture.md | 233 +++--- docs/development/navigation/navigation.md | 23 +- docs/development/navigation/node-types.md | 2 + .../navigation/two-phase-loading.md | 2 + .../navigation/visual-walkthrough.md | 532 +++---------- docs/development/toc.yml | 5 +- 8 files changed, 437 insertions(+), 1323 deletions(-) diff --git a/docs/development/navigation/assembler-process.md b/docs/development/navigation/assembler-process.md index cbf57ebe9..bae1b9c16 100644 --- a/docs/development/navigation/assembler-process.md +++ b/docs/development/navigation/assembler-process.md @@ -1,137 +1,84 @@ # Assembler Process -This document explains how the assembler builds a unified site navigation from multiple documentation repositories. +The assembler combines multiple documentation repositories into a unified site with custom URL prefixes. -## Overview - -The assembler combines multiple isolated documentation repositories into a single site with: -- Unified navigation structure -- Custom URL prefixes for each repository -- Cross-repository linking -- Phantom node tracking +> **Prerequisites:** Read [Functional Principles #4](functional-principles.md#4-navigation-roots-can-be-re-homed) and [Home Provider Architecture](home-provider-architecture.md) first to understand re-homing. ## The Challenge -Given: -- `elastic-docs` repository (guides, API reference, tutorials) -- `elasticsearch` repository (ES-specific docs) -- `kibana` repository (Kibana-specific docs) -- `logstash` repository (Logstash-specific docs) - -Build a site where: -- Each repository maintains its own `docset.yml` -- URLs are organized by product/section (not repository) -- Same TOC can appear in multiple places -- Navigation structure is defined centrally - -**Example:** -``` -elastic-docs has: - - /api/ - - /guides/ - -We want site to have: - - /elasticsearch/api/ (from elastic-docs://api) - - /elasticsearch/guides/ (from elastic-docs://guides) - - /kibana/api/ (from kibana://) - - /logstash/ (from logstash://) -``` +Multiple repositories need to appear as one site: +- `elastic-docs` with `/api/` and `/guides/` +- Assembled site needs `/elasticsearch/api/` and `/elasticsearch/guides/` +- Same content, different URLs, no rebuilding -## The Solution: Site Navigation - -`config/navigation.yml` defines the site structure: - -```yaml -toc: - - toc: elasticsearch - children: - - toc: elastic-docs://api - path_prefix: elasticsearch/api - - toc: elastic-docs://guides - path_prefix: elasticsearch/guides +## The Solution - - toc: kibana:// - path_prefix: kibana +Four-phase process: - - toc: logstash:// - path_prefix: logstash - -phantoms: - - source: plugins:// - # Declared but not included in navigation -``` - -## Assembler Build Process - -### Phase 1: Build Isolated Navigations +### Phase 1: Build with AssemblerBuild Flag ```csharp -// For each repository in assembler.yml -foreach (var repo in repositories) +public AssemblerDocumentationSet( + ILoggerFactory logFactory, + AssembleContext context, + Checkout checkout, + ICrossLinkResolver crossLinkResolver, + IConfigurationContext configurationContext, + IReadOnlySet availableExporters) { - // Load docset configuration - var docsetYaml = LoadFile($"{repo}/docset.yml"); - var docsetConfig = DocumentationSetFile.LoadAndResolve( - collector, - docsetYaml, - fileSystem.DirectoryInfo.New(repo) - ); - - // Build isolated navigation - var navigation = new DocumentationSetNavigation( - docsetConfig, - CreateContext(repo), - GenericDocumentationFileFactory.Instance - ); - - // Store for later assembly - isolatedNavigations[repo] = navigation; + // For each repository: + // 1. Load and resolve docset.yml + var buildContext = new BuildContext(...) + { + AssemblerBuild = true // ← CRITICAL! + }; + + // 2. Build DocumentationSetNavigation with assembler context + DocumentationSet = new DocumentationSet(buildContext, logFactory, crossLinkResolver); } ``` -**Result:** Each repository has its own navigation tree with URLs relative to `/`. +**Why `AssemblerBuild = true` matters:** +```csharp +// DocumentationSetNavigation constructor when creating TOCs: +var assemblerBuild = context.AssemblerBuild; -### Phase 2: Load Site Navigation Configuration +var tocHomeProvider = assemblerBuild + ? new NavigationHomeProvider(...) // Create NEW scope + : parentHomeProvider; // Inherit parent's scope -```csharp -// Load navigation.yml -var navigationYaml = LoadFile("config/navigation.yml"); -var siteConfig = SiteNavigationFile.LoadAndResolve( - collector, - navigationYaml, - fileSystem -); +// Result: Each TOC gets its own HomeProvider instance ``` -**`SiteNavigationFile` contains:** -```csharp -public class SiteNavigationFile -{ - public List TableOfContents { get; set; } - public List Phantoms { get; set; } -} +**Without this flag:** +``` +DocumentationSetNavigation + └─ TableOfContentsNavigation (api) + HomeProvider: INHERITED ← shares parent's provider +``` +Can't re-home independently. -public class SiteTableOfContentsRef -{ - public Uri Source { get; set; } // e.g., elastic-docs://api - public string PathPrefix { get; set; } // e.g., "elasticsearch/api" - public List Children { get; set; } -} +**With this flag:** ``` +DocumentationSetNavigation + └─ TableOfContentsNavigation (api) + HomeProvider: NEW INSTANCE ← own provider! +``` +Can re-home independently! -### Phase 3: Assemble Site Navigation +### Phase 2: Load navigation.yml -```csharp -// Create site navigation - this does the re-homing! -var siteNavigation = new SiteNavigation( - siteConfig, - context, - isolatedNavigations.Values, // All isolated navigations - sitePrefix: null // Or custom prefix like "/docs" -); +```yaml +toc: + - toc: elastic-docs://api + path_prefix: elasticsearch/api + - toc: elastic-docs://guides + path_prefix: elasticsearch/guides ``` -**What `SiteNavigation` constructor does:** +Defines where each TOC appears in the site. + +### Phase 3: Create SiteNavigation ```csharp public SiteNavigation( @@ -140,62 +87,29 @@ public SiteNavigation( IReadOnlyCollection documentationSetNavigations, string? sitePrefix) { - _sitePrefix = NormalizeSitePrefix(sitePrefix); - - // 1. Initialize root properties + // 1. Initialize SiteNavigation as root NavigationRoot = this; - Parent = null; - Hidden = false; - Id = ShortId.Create("site"); - Identifier = new Uri("site://"); - // 2. Collect all root nodes (docsets and TOCs) into _nodes - _nodes = []; + // 2. Collect all docset/TOC nodes into dictionary foreach (var setNavigation in documentationSetNavigations) { foreach (var (identifier, node) in setNavigation.TableOfContentNodes) - { - if (!_nodes.TryAdd(identifier, node)) - { - context.EmitError( - context.ConfigurationPath, - $"Duplicate navigation identifier: {identifier}" - ); - } - } + _nodes.TryAdd(identifier, node); } - // 3. Build site navigation by re-homing nodes - var items = new List(); - var index = 0; + // 3. Process each navigation.yml reference foreach (var tocRef in siteNavigationFile.TableOfContents) { - var navItem = CreateSiteTableOfContentsNavigation( - tocRef, - index++, - context, - parent: this, - root: null - ); - + var navItem = CreateSiteTableOfContentsNavigation(tocRef, index++, context, this, null); if (navItem != null) items.Add(navItem); } - - // 4. Set index and navigation items - var indexNavigation = items.QueryIndex( - this, - "/index.md", - out var navigationItems - ); - Index = indexNavigation; - NavigationItems = navigationItems; } ``` -### Phase 4: Re-home Individual Nodes +### Phase 4: Re-home Each Reference -The magic happens in `CreateSiteTableOfContentsNavigation`: +For each entry in `navigation.yml`: ```csharp private INavigationItem? CreateSiteTableOfContentsNavigation( @@ -205,454 +119,186 @@ private INavigationItem? CreateSiteTableOfContentsNavigation( INodeNavigationItem parent, IRootNavigationItem? root) { - // 1. Calculate path prefix - var pathPrefix = tocRef.PathPrefix; - if (string.IsNullOrWhiteSpace(pathPrefix)) - { - // Handle narrative repository special case - if (tocRef.Source.Scheme != NarrativeRepository.RepositoryName) - { - context.EmitError( - context.ConfigurationPath, - $"path_prefix is required for TOC reference: {tocRef.Source}" - ); - return null; - } - } - - // Normalize path prefix - pathPrefix = pathPrefix.Trim('/'); - pathPrefix = !string.IsNullOrWhiteSpace(_sitePrefix) - ? $"{_sitePrefix}/{pathPrefix}" - : "/" + pathPrefix; - - // 2. Look up the node - if (!_nodes.TryGetValue(tocRef.Source, out var node)) - { - context.EmitError( - context.ConfigurationPath, - $"Could not find navigation node for identifier: {tocRef.Source}" - ); - return null; - } - - if (node is not INavigationHomeAccessor homeAccessor) - { - context.EmitError( - context.ConfigurationPath, - $"Navigation node does not implement INavigationHomeAccessor: {tocRef.Source}" - ); - return null; - } - - root ??= node; - - // 3. RE-HOME THE NODE! ⚡ - node.Parent = parent; - node.NavigationIndex = index; - homeAccessor.HomeProvider = new NavigationHomeProvider(pathPrefix, root); - - // 4. Process children (may include re-homing nested nodes) - var children = new List(); - - // First, add node's existing children - INavigationItem[] nodeChildren = [node.Index, .. node.NavigationItems]; - foreach (var nodeChild in nodeChildren) - { - nodeChild.Parent = node; - if (nodeChild is INavigationHomeAccessor childAccessor) - childAccessor.HomeProvider = homeAccessor.HomeProvider; - - // Don't add root nodes unless explicitly declared - if (nodeChild is IRootNavigationItem) - continue; - - children.Add(nodeChild); - } - - // Then, add any additional children from navigation.yml - if (tocRef.Children.Count > 0) - { - var childIndex = 0; - foreach (var child in tocRef.Children) - { - var childItem = CreateSiteTableOfContentsNavigation( - child, - childIndex++, - context, - node, - root - ); - if (childItem != null) - children.Add(childItem); - } - } - - // 5. Set children on the node - switch (node) - { - case IAssignableChildrenNavigation documentationSetNavigation: - documentationSetNavigation.SetNavigationItems(children); - break; - } + // 1. Calculate final path_prefix + // 2. Look up node by identifier (elastic-docs://api) + // 3. Replace node's HomeProvider ← THE MAGIC! ⚡ + // 4. Update parent/index + // 5. Process children (skip nested root nodes) +} +``` - return node; +**The critical line:** +```csharp +private INavigationItem? CreateSiteTableOfContentsNavigation(...) +{ + // ... + homeAccessor.HomeProvider = new NavigationHomeProvider(pathPrefix, siteRoot); } ``` -## Detailed Example +This single assignment updates all descendant URLs instantly (O(1)). -### Input: Isolated Navigation +## How It Works: Example +**Input: Built with AssemblerBuild = true** ``` -elastic-docs repository: -DocumentationSetNavigation (elastic-docs://) - PathPrefix: "" - NavigationRoot: self - Index: / - NavigationItems: - - TableOfContentsNavigation (elastic-docs://api) - PathPrefix: "" - NavigationRoot: DocumentationSetNavigation - Index: /api/ - NavigationItems: - - FileNavigationLeaf (api/rest.md) - Url: /api/rest/ - NavigationRoot: DocumentationSetNavigation - - - TableOfContentsNavigation (elastic-docs://guides) - PathPrefix: "" - NavigationRoot: DocumentationSetNavigation - Index: /guides/ - NavigationItems: - - FileNavigationLeaf (guides/getting-started.md) - Url: /guides/getting-started/ - NavigationRoot: DocumentationSetNavigation +elastic-docs:// + HomeProvider: self + └─ elastic-docs://api + HomeProvider: Instance A (PathPrefix = "") + └─ api/rest.md → URL: /api/rest/ + └─ elastic-docs://guides + HomeProvider: Instance B (PathPrefix = "") + └─ guides/start.md → URL: /guides/start/ ``` -### Configuration: navigation.yml - +**navigation.yml:** ```yaml -toc: - - toc: elasticsearch - children: - - toc: elastic-docs://api - path_prefix: elasticsearch/api - - - toc: elastic-docs://guides - path_prefix: elasticsearch/guides +- toc: elastic-docs://api + path_prefix: elasticsearch/api +- toc: elastic-docs://guides + path_prefix: elasticsearch/guides ``` -### Output: Assembled Site Navigation - +**After Re-homing:** ``` -SiteNavigation (site://) - PathPrefix: "" - NavigationRoot: self - Identifier: site:// - - Nodes: - [elastic-docs://] = DocumentationSetNavigation - [elastic-docs://api] = TableOfContentsNavigation - [elastic-docs://guides] = TableOfContentsNavigation - - NavigationItems: - - VirtualFolderNode ("elasticsearch") - NavigationItems: - - - TableOfContentsNavigation (elastic-docs://api) RE-HOMED! ⚡ - PathPrefix: "elasticsearch/api" ← Changed! - NavigationRoot: SiteNavigation ← Changed! - Parent: VirtualFolderNode ← Changed! - HomeProvider: new NavigationHomeProvider( - "/elasticsearch/api", - SiteNavigation - ) - Index: /elasticsearch/api/ ← Changed! - NavigationItems: - - FileNavigationLeaf (api/rest.md) - Url: /elasticsearch/api/rest/ ← Changed! - NavigationRoot: SiteNavigation ← Changed! - - - TableOfContentsNavigation (elastic-docs://guides) RE-HOMED! ⚡ - PathPrefix: "elasticsearch/guides" ← Changed! - NavigationRoot: SiteNavigation ← Changed! - Parent: VirtualFolderNode ← Changed! - HomeProvider: new NavigationHomeProvider( - "/elasticsearch/guides", - SiteNavigation - ) - Index: /elasticsearch/guides/ ← Changed! - NavigationItems: - - FileNavigationLeaf (guides/getting-started.md) - Url: /elasticsearch/guides/getting-started/ ← Changed! - NavigationRoot: SiteNavigation ← Changed! +SiteNavigation + └─ elastic-docs://api + HomeProvider: NEW (PathPrefix = "elasticsearch/api") ← Replaced! + └─ api/rest.md → URL: /elasticsearch/api/rest/ ✓ + └─ elastic-docs://guides + HomeProvider: NEW (PathPrefix = "elasticsearch/guides") ← Replaced! + └─ guides/start.md → URL: /elasticsearch/guides/start/ ✓ ``` -**Key Changes:** -1. `HomeProvider` replaced → new path prefix and navigation root -2. All URLs automatically updated (lazy calculation) -3. Parent relationships updated -4. NavigationRoot points to SiteNavigation +## Why Separate Scopes Matter -## Re-homing Performance +**Scenario:** Split a docset across the site. -**Cost of re-homing a subtree with 10,000 nodes:** -```csharp -// This single line re-homes all 10,000 nodes! -homeAccessor.HomeProvider = new NavigationHomeProvider(pathPrefix, root); +```yaml +# elastic-docs has both api/ and guides/ TOCs +toc: + - toc: elastic-docs://api + path_prefix: reference/api # Goes here + - toc: elastic-docs://guides + path_prefix: learn/guides # Goes there ``` -**Time complexity:** O(1) -- No tree traversal -- No URL updates -- Just reference assignment +If TOCs shared their parent's provider, both would get the same prefix. Separate providers enable different prefixes from the same repository. -**When URLs are accessed later:** -- First access: O(path depth) calculation -- Subsequent: O(1) from cache -- Cache invalidated automatically (HomeProvider ID changed) +## Key Architecture Points -## Phantom Nodes +**1. AssemblerBuild Flag Controls Scope Creation** +- True: TOCs create own HomeProvider +- False: TOCs inherit parent's HomeProvider -Phantoms are nodes declared in navigation.yml but not included in the tree: +**2. HomeProvider is the Re-homing Mechanism** +- URLs calculated from `HomeProvider.PathPrefix` +- Changing provider changes all descendant URLs +- No tree traversal needed -```yaml -phantoms: - - source: plugins:// - - source: cloud://monitoring -``` - -**Purpose:** -- Document intentionally excluded content -- Prevent "undeclared navigation" warnings -- Enable validation of cross-links to phantom content - -**Tracking:** -```csharp -// SiteNavigation tracks phantoms -public IReadOnlyCollection Phantoms { get; } -public HashSet DeclaredPhantoms { get; } +**3. Root Nodes Can Be Re-homed** +- `DocumentationSetNavigation` - Entire docset +- `TableOfContentsNavigation` - Individual TOC +- Must have own provider (not inherited) -// After assembly, check for unseen nodes -foreach (var node in UnseenNodes) -{ - if (!DeclaredPhantoms.Contains(node)) - context.EmitHint( - context.ConfigurationPath, - $"Navigation does not explicitly declare: {node} as a phantom" - ); -} -``` +**4. Non-Root Nodes Inherit** +- `FileNavigationLeaf`, `FolderNavigation`, etc. +- Use parent's HomeProvider +- Re-home automatically when parent re-homed ## Path Prefix Requirements -In assembler builds, `path_prefix` is **mandatory** (with one exception): - ```yaml -toc: - - toc: elastic-docs://api - path_prefix: elasticsearch/api # Required! +# Required +- toc: elastic-docs://api + path_prefix: elasticsearch/api # Must be unique! - - toc: docs-content://guides - # path_prefix not required for narrative repository - # Will default to "guides" +# Exception: narrative repository +- toc: docs-content://guides + # path_prefix defaults to "guides" ``` -**Why?** -- Prevents URL collisions -- Makes routing explicit -- Enables flexible reorganization - -**Validation:** -```csharp -if (string.IsNullOrWhiteSpace(pathPrefix)) -{ - if (tocRef.Source.Scheme != NarrativeRepository.RepositoryName) - { - context.EmitError( - context.ConfigurationPath, - $"path_prefix is required for TOC reference: {tocRef.Source}" - ); - } -} -``` +All `path_prefix` values must be unique across the site. -## Nested Re-homing +## Phantom Nodes -Assembler builds can have nested structures where nodes are re-homed multiple times: +Declared but not included: ```yaml -toc: - - toc: products - children: - - toc: elasticsearch - path_prefix: products/elasticsearch - children: - - toc: elastic-docs://api - path_prefix: products/elasticsearch/api +phantoms: + - source: plugins:// ``` -**How it works:** +Prevents "undeclared navigation" warnings for excluded content. -1. Create virtual `products` node -2. Re-home `elasticsearch` to `/products/elasticsearch` -3. Re-home `elastic-docs://api` to `/products/elasticsearch/api` +## The Re-homing Flow -Each level creates a new scope with its own HomeProvider. +``` +1. Build with AssemblerBuild = true + → TOCs get own HomeProvider -## Error Handling +2. Collect all nodes into dictionary + → Indexed by identifier (elastic-docs://api) -The assembler emits errors for common issues: +3. For each navigation.yml entry: + → Look up node + → Replace HomeProvider ← O(1) operation + → All URLs update automatically -### Duplicate Identifiers -```csharp -// Two docsets with same identifier -if (!_nodes.TryAdd(identifier, node)) -{ - context.EmitError( - context.ConfigurationPath, - $"Duplicate navigation identifier: {identifier}" - ); -} +4. Result: Unified site with custom structure ``` -### Missing Nodes -```csharp -// Referenced node doesn't exist -if (!_nodes.TryGetValue(tocRef.Source, out var node)) -{ - context.EmitError( - context.ConfigurationPath, - $"Could not find navigation node for identifier: {tocRef.Source}" - ); -} -``` +## What Makes This Fast -### Undeclared Nested TOCs +**O(1) Re-homing:** ```csharp -// Found a nested TOC that wasn't declared in navigation.yml -if (!DeclaredTableOfContents.Contains(rootChild.Identifier) && - !DeclaredPhantoms.Contains(rootChild.Identifier)) -{ - context.EmitWarning( - context.ConfigurationPath, - $"Navigation does not explicitly declare: {rootChild.Identifier}" - ); -} +// This updates 10,000 URLs instantly: +node.HomeProvider = new NavigationHomeProvider(newPrefix, newRoot); ``` -## Site Prefix - -The entire site can have a global prefix: +**Why?** +- URLs calculated on-demand from HomeProvider +- Not stored in nodes +- Changing provider = all URLs recalculate next access +- Smart caching invalidates on provider change -```csharp -var siteNavigation = new SiteNavigation( - siteConfig, - context, - documentationSetNavigations, - sitePrefix: "/docs" // All URLs start with /docs -); -``` +## Common Patterns -**Example:** +**Pattern 1: Keep docset together** +```yaml +- toc: elastic-docs:// + path_prefix: elasticsearch ``` -Without site prefix: - /elasticsearch/api/rest/ -With sitePrefix="/docs": - /docs/elasticsearch/api/rest/ +**Pattern 2: Split docset apart** +```yaml +- toc: elastic-docs://api + path_prefix: reference/api +- toc: elastic-docs://guides + path_prefix: learn/guides ``` -**Normalization:** -```csharp -private static string? NormalizeSitePrefix(string? sitePrefix) -{ - if (string.IsNullOrWhiteSpace(sitePrefix)) - return null; - - var normalized = sitePrefix.Trim(); - - // Ensure leading slash - if (!normalized.StartsWith('/')) - normalized = "/" + normalized; - - // Remove trailing slash - normalized = normalized.TrimEnd('/'); - - return normalized; -} +**Pattern 3: Nest docsets** +```yaml +- toc: products + children: + - toc: elasticsearch:// + path_prefix: products/elasticsearch + - toc: kibana:// + path_prefix: products/kibana ``` -## Testing Assembler Builds - -```csharp -[Fact] -public void AssemblerRehomesNavigationUrls() -{ - // Arrange: Build isolated navigation - var docset = DocumentationSetFile.LoadAndResolve( - collector, - yaml, - fileSystem.NewDirInfo("/elastic-docs") - ); - var isolatedNav = new DocumentationSetNavigation( - docset, - isolatedContext, - factory - ); - - // Assert: URLs in isolated build - var apiLeaf = FindLeaf(isolatedNav, "api/rest.md"); - Assert.Equal("/api/rest/", apiLeaf.Url); - - // Act: Assemble site - var siteConfig = new SiteNavigationFile - { - TableOfContents = [ - new SiteTableOfContentsRef - { - Source = new Uri("elastic-docs://api"), - PathPrefix = "elasticsearch/api" - } - ] - }; - - var siteNav = new SiteNavigation( - siteConfig, - assemblerContext, - [isolatedNav], - sitePrefix: null - ); - - // Assert: URLs in assembled site - var apiLeafInSite = FindLeaf(siteNav, "api/rest.md"); - Assert.Equal("/elasticsearch/api/rest/", apiLeafInSite.Url); +## Summary - // The same leaf object! Just re-homed! - Assert.Same(apiLeaf, apiLeafInSite); -} -``` +**The assembler enables:** +- Build repositories independently (isolated) +- Combine into unified site (assembled) +- Custom URL structure per site +- Split single docset across multiple sections +- O(1) re-homing (no tree reconstruction) -## Summary +**The critical piece:** +`AssemblerBuild = true` causes `TableOfContentsNavigation` to create own `HomeProvider`, enabling independent re-homing of TOCs within a docset. -The assembler process: - -1. **Builds isolated navigations** - Each repository with URLs relative to `/` -2. **Loads site configuration** - `navigation.yml` with path prefixes -3. **Re-homes nodes** - O(1) operation to change URL prefix -4. **Assembles site tree** - Unified navigation with custom structure -5. **Tracks phantoms** - Documents excluded content -6. **Validates** - Catches duplicates, missing nodes, undeclared TOCs - -**Key Innovation:** Re-homing via HomeProvider pattern enables: -- O(1) URL prefix changes -- No tree reconstruction -- Lazy URL calculation -- Same node in multiple contexts - -This makes it possible to: -- Build repositories independently -- Test in isolation -- Assemble flexibly into unified site -- Reorganize without rebuilding +Without this, you can only re-home entire docsets. With it, you can split a docset anywhere. diff --git a/docs/development/navigation/first-principles.md b/docs/development/navigation/first-principles.md index 884cd8c6b..fb5face31 100644 --- a/docs/development/navigation/first-principles.md +++ b/docs/development/navigation/first-principles.md @@ -1,222 +1,45 @@ # First Principles -This document outlines the fundamental principles that guide the design of `Elastic.Documentation.Navigation`. +The navigation system is built on two types of principles: -## Core Principles +## [Functional Principles](functional-principles.md) -### 1. Two-Phase Loading +These define **what** the navigation system does and **why**: -Navigation construction follows a strict two-phase approach: +1. **Two-Phase Loading** - Separate configuration resolution from navigation construction +2. **Single Documentation Source** - All paths relative to docset root +3. **URL Building is Dynamic** - URLs calculated on-demand, not stored +4. **Navigation Roots Can Be Re-homed** - O(1) URL prefix changes +5. **Navigation Scope via HomeProvider** - Scoped URL calculation contexts +6. **Index Files Determine URLs** - Folders and indexes share URLs +7. **File Structure Mirrors Navigation** - Predictable, maintainable structure +8. **Acyclic Graph Structure** - Tree with no cycles, unique URLs +9. **Phantom Nodes** - Acknowledge content without including it -**Phase 1: Configuration Resolution** (`Elastic.Documentation.Configuration`) -- Parse YAML files (`docset.yml`, `toc.yml`, `navigation.yml`) -- Resolve all file references to **full paths** relative to documentation set root -- Validate configuration structure and relationships -- Output: Fully resolved configuration objects with complete file paths +[Read Functional Principles →](functional-principles.md) -**Phase 2: Navigation Construction** (`Elastic.Documentation.Navigation`) -- Consume resolved configuration from Phase 1 -- Build navigation tree with **full URLs** -- Create node relationships (parent/child/root) -- Set up home providers for URL calculation -- Output: Complete navigation tree with calculated URLs +## [Technical Principles](technical-principles.md) -**Why Two Phases?** -- **Separation of Concerns**: Configuration parsing is independent of navigation structure -- **Validation**: Catch file/structure errors before building expensive navigation trees -- **Reusability**: Same configuration can build different navigation structures (isolated vs assembler) -- **Performance**: Resolve file system operations once, reuse for navigation +These define **how** the navigation system is implemented: -### 2. Single Documentation Source +1. **Generic Type System** - Covariance enables static typing without runtime casts +2. **Provider Pattern** - Decouples URL calculation from tree structure +3. **Lazy URL Calculation** - Smart caching with automatic invalidation -URLs are always built relative to the documentation set's source directory: -- Files referenced in `docset.yml` are relative to the docset root -- Files referenced in nested `toc.yml` are relative to the toc directory -- During Phase 1, all paths are resolved to be relative to the docset root -- During Phase 2, URLs are calculated from these resolved paths +[Read Technical Principles →](technical-principles.md) -**Example:** -``` -docs/ -├── docset.yml # Root -├── index.md -└── api/ - ├── toc.yml # Nested TOC - └── rest.md -``` +--- -Phase 1 resolves `api/toc.yml` reference to `rest.md` as: `api/rest.md` (relative to docset root) -Phase 2 builds URL as: `/api/rest/` +## Quick Reference -### 3. URL Building is Dynamic and Cheap +**For understanding architecture:** Start with [Functional Principles](functional-principles.md) -URLs are **calculated on-demand**, not stored: -- Nodes don't store their final URL -- URLs are computed from `HomeProvider.PathPrefix` + relative path -- Changing a `HomeProvider` instantly updates all descendant URLs -- No tree traversal needed to update URLs +**For implementation details:** See [Technical Principles](technical-principles.md) -**Why Dynamic?** -- **Re-homing**: Same subtree can have different URLs in different contexts -- **Memory Efficient**: Don't store redundant URL strings -- **Consistency**: URLs always reflect current home provider state +**For visual examples:** See [Visual Walkthrough](visual-walkthrough.md) -### 4. Navigation Roots Can Be Re-homed - -A key design feature that enables assembler builds: -- **Isolated Build**: Each `DocumentationSetNavigation` is its own root -- **Assembler Build**: `SiteNavigation` becomes the root, docsets are "re-homed" -- **Re-homing**: Replace a subtree's `HomeProvider` to change its URL prefix -- **Cheap Operation**: O(1) - just replace the provider reference - -**Example:** -```csharp -// Isolated: URLs start at / -homeProvider.PathPrefix = ""; -// → /api/rest/ - -// Assembled: Re-home to /guide -homeProvider = new NavigationHomeProvider("/guide", siteNav); -// → /guide/api/rest/ -``` - -### 5. Navigation Scope via HomeProvider - -`INavigationHomeProvider` creates navigation scopes: -- **Provider**: Defines `PathPrefix` and `NavigationRoot` for a scope -- **Accessor**: Children use `INavigationHomeAccessor` to access their scope -- **Inheritance**: Child nodes inherit their parent's accessor -- **Isolation**: Changes to a provider only affect its scope - -**Scope Creators:** -- `DocumentationSetNavigation` - Creates scope for entire docset -- `TableOfContentsNavigation` - Creates scope for TOC subtree (enables re-homing) - -**Scope Consumers:** -- `FileNavigationLeaf` - Uses accessor to calculate URL -- `FolderNavigation` - Passes accessor to children -- `VirtualFileNavigation` - Passes accessor to children - -### 6. Index Files Determine Folder URLs - -Every folder/node navigation has an **Index**: -- Index is either `index.md` or the first file -- The node's URL is the same as its Index's URL -- Children appear "under" the index in navigation -- Index files map to folder paths: `/api/index.md` → `/api/` - -**Why?** -- **Consistent URL Structure**: Folders and their indexes share the same URL -- **Natural Navigation**: Index represents the folder's landing page -- **Hierarchical**: Clear parent-child URL relationships - -### 7. File Structure Should Mirror Navigation - -Best practices for maintainability: -- Navigation structure should follow file system structure -- Avoid deep-linking files from different directories -- Use `folder:` references when possible -- Virtual files should group sibling files, not restructure the tree - -**Rationale:** -- **Discoverability**: Developers can find files by following navigation -- **Predictability**: URL structure matches file structure -- **Maintainability**: Moving files in navigation matches moving them on disk - -### 8. Generic Type System for Covariance - -Navigation classes are generic over `TModel`: -```csharp -public class DocumentationSetNavigation - where TModel : class, IDocumentationFile -``` - -**Why Generic?** -- **Covariance**: Can treat `DocumentationSetNavigation` as `IRootNavigationItem` -- **Type Safety**: Factory pattern ensures correct model types -- **Flexibility**: Same navigation code works with different file models - -### 9. Lazy URL Calculation with Caching - -`FileNavigationLeaf` implements smart URL caching: -```csharp -private string? _homeProviderCache; -private string? _urlCache; - -public string Url -{ - get - { - if (_homeProviderCache == HomeProvider.Id && _urlCache != null) - return _urlCache; - - _urlCache = CalculateUrl(); - _homeProviderCache = HomeProvider.Id; - return _urlCache; - } -} -``` - -**Strategy:** -- Cache URL along with HomeProvider ID -- Invalidate cache when HomeProvider changes -- Recalculate only when needed -- O(1) for repeated access, O(path) for calculation - -### 10. Phantom Nodes for Incomplete Navigation - -`navigation.yml` can declare phantoms: -```yaml -phantoms: - - source: plugins:// -``` - -**Purpose:** -- Reference nodes that exist but aren't included in site navigation -- Prevent "undeclared navigation" warnings -- Document intentionally excluded content -- Enable validation of cross-links - -## Design Patterns - -### Factory Pattern for Node Creation - -`DocumentationNavigationFactory` creates navigation items: -- Encapsulates construction logic -- Ensures consistent initialization -- Centralizes node creation -- Type-safe generic methods - -### Provider Pattern for URL Context - -`INavigationHomeProvider` / `INavigationHomeAccessor`: -- Providers define context (PathPrefix, NavigationRoot) -- Accessors reference providers -- Decouples URL calculation from tree structure -- Enables context switching (re-homing) - -### Visitor Pattern for Tree Operations - -Navigation items implement interfaces for traversal: -- `IRootNavigationItem` - Root nodes -- `INodeNavigationItem` - Nodes with children -- `ILeafNavigationItem` - Leaf nodes -- Common `INavigationItem` base for polymorphic traversal - -## Key Invariants - -1. **Phase Order**: Configuration must be fully resolved before navigation construction -2. **Path Resolution**: All paths in configuration are relative to docset root after Phase 1 -3. **URL Uniqueness**: Every navigation item must have a unique URL within its site -4. **Root Consistency**: All nodes in a subtree point to the same `NavigationRoot` -5. **Provider Validity**: A node's `HomeProvider` must be an ancestor in the tree -6. **Index Requirement**: All node navigations (folder/toc/docset) must have an Index -7. **Path Prefix Uniqueness**: In assembler builds, all `path_prefix` values must be unique - -## Performance Characteristics - -- **Tree Construction**: O(n) where n = number of files -- **URL Calculation**: O(depth) for first access, O(1) with caching -- **Re-homing**: O(1) - just replace HomeProvider reference -- **Tree Traversal**: O(n) for full tree, but rarely needed -- **Memory**: O(n) for nodes, URLs computed on-demand +**For specific topics:** +- How assembly works: [Assembler Process](assembler-process.md) +- How re-homing works: [Home Provider Architecture](home-provider-architecture.md) +- Two-phase approach: [Two-Phase Loading](two-phase-loading.md) +- Node reference: [Node Types](node-types.md) diff --git a/docs/development/navigation/home-provider-architecture.md b/docs/development/navigation/home-provider-architecture.md index caa51f71d..f94eae025 100644 --- a/docs/development/navigation/home-provider-architecture.md +++ b/docs/development/navigation/home-provider-architecture.md @@ -1,6 +1,8 @@ # Home Provider Architecture -The Home Provider pattern is the secret sauce that makes re-homing navigation subtrees a cheap O(1) operation. +The Home Provider pattern enables O(1) re-homing of navigation subtrees through indirection. + +> **Overview:** For high-level concepts, see [Functional Principles #3-5](functional-principles.md#3-url-building-is-dynamic-and-cheap). This document explains the implementation. ## The Problem @@ -9,12 +11,12 @@ When building assembled documentation sites, we need to: 2. Combine them into a single site with custom URL prefixes 3. Update all URLs in a subtree efficiently -**Naive Approach (doesn't work):** +**Naive approach:** ```csharp -// Bad: Traverse entire subtree to update URLs +// Traverse entire subtree to update URLs void UpdateUrlPrefix(INavigationItem root, string newPrefix) { - // O(n) - have to visit every node! + // O(n) - visit every node foreach (var item in TraverseTree(root)) { item.UrlPrefix = newPrefix; @@ -22,15 +24,15 @@ void UpdateUrlPrefix(INavigationItem root, string newPrefix) } ``` -**Problems with naive approach:** +**Issues:** - O(n) traversal for every prefix change -- Have to store URL prefix at every node (memory waste) -- Hard to keep URLs consistent -- Can't lazily calculate URLs +- URL prefix stored at every node +- URLs calculated at construction time +- Changes require tree reconstruction -## The Solution: Home Provider Pattern +## The Solution: Provider Pattern -Instead of storing URL information at each node, we use a **provider pattern** with indirection: +Instead of storing URL information at each node, use indirection through a provider: ```csharp // The provider defines the URL context for a scope @@ -48,9 +50,7 @@ public interface INavigationHomeAccessor } ``` -### Key Insight - -Nodes don't store URL information. Instead they **reference** a provider: +Nodes reference a provider instead of storing URL information: ```csharp public class FileNavigationLeaf @@ -61,7 +61,7 @@ public class FileNavigationLeaf { get { - // Dynamically calculate from current provider! + // Calculate from current provider var prefix = _homeAccessor.HomeProvider.PathPrefix; return $"{prefix}/{_relativePath}/"; } @@ -69,10 +69,10 @@ public class FileNavigationLeaf } ``` -Now re-homing is O(1): +Re-homing becomes a single assignment: ```csharp -// Change the provider → all descendants instantly use new prefix! +// Change the provider → all descendants use new prefix docsetNavigation.HomeProvider = new NavigationHomeProvider("/guide", siteNav); ``` @@ -80,16 +80,15 @@ docsetNavigation.HomeProvider = new NavigationHomeProvider("/guide", siteNav); ### 1. Scope Creation -Certain navigation types create scopes by implementing `INavigationHomeProvider`: +Navigation types that can be re-homed implement `INavigationHomeProvider`: ```csharp public class DocumentationSetNavigation : INavigationHomeProvider, INavigationHomeAccessor { - // This node IS a provider (creates scope) private string _pathPrefix; - // Properties for being a provider + // Provider properties public string PathPrefix => HomeProvider == this ? _pathPrefix : HomeProvider.PathPrefix; @@ -99,14 +98,14 @@ public class DocumentationSetNavigation ? this : HomeProvider.NavigationRoot; - // Property for accessing current provider + // Accessor property public INavigationHomeProvider HomeProvider { get; set; } - // Initially, it's its own provider + // Initially self-referential public DocumentationSetNavigation(...) { _pathPrefix = pathPrefix ?? ""; - HomeProvider = this; // I am my own provider! + HomeProvider = this; } } ``` @@ -126,7 +125,7 @@ var fileNav = new FileNavigationLeaf( hidden, index, parent, - homeAccessor: this // Pass down the accessor! + homeAccessor: this // Pass down the accessor ) ); ``` @@ -147,7 +146,7 @@ public class FileNavigationLeaf // Get prefix from current provider var rootUrl = _args.HomeAccessor.HomeProvider.PathPrefix.TrimEnd('/'); - // Determine if we're relative to container or docset + // Determine path based on context var relativeToContainer = _args.HomeAccessor.HomeProvider.NavigationRoot.Parent is SiteNavigation; @@ -155,41 +154,37 @@ public class FileNavigationLeaf ? _args.RelativePathToTableOfContents : _args.RelativePathToDocumentationSet; - // Calculate URL return BuildUrl(rootUrl, relativePath); } } } ``` -### 4. Re-homing (The Magic!) +### 4. Re-homing -In assembler builds, `SiteNavigation` re-homes subtrees: +In assembler builds, `SiteNavigation` replaces the provider: ```csharp -// SiteNavigation.cs:211 -private INavigationItem? CreateSiteTableOfContentsNavigation(...) -{ - // Calculate new path prefix for this subtree - var pathPrefix = $"{_sitePrefix}/{tocRef.PathPrefix}".Trim('/'); +// CreateSiteTableOfContentsNavigation(...): +// Calculate new path prefix for this subtree +var pathPrefix = $"{_sitePrefix}/{tocRef.PathPrefix}".Trim('/'); - // Create new provider with custom prefix - var newProvider = new NavigationHomeProvider(pathPrefix, root); +// Create new provider with custom prefix +var newProvider = new NavigationHomeProvider(pathPrefix, root); - // Re-home the entire subtree with one assignment! - homeAccessor.HomeProvider = newProvider; +// Replace provider - this is the magic! ⚡ +homeAccessor.HomeProvider = newProvider; - // All descendants now use the new prefix! ✨ -} +// All descendants now use the new prefix ``` **What happens:** -1. `homeAccessor.HomeProvider` is set to new provider +1. `homeAccessor.HomeProvider` is assigned a new provider 2. Provider has `PathPrefix = "/guide"` and `NavigationRoot = SiteNavigation` -3. Every URL calculation in that subtree now uses "/guide" prefix -4. No tree traversal needed! +3. Every URL calculation in that subtree now uses the "/guide" prefix +4. No tree traversal needed -## Detailed Example +## Example: Isolated to Assembled ### Isolated Build @@ -212,16 +207,6 @@ DocumentationSetNavigation (elastic-docs) url = "/api/rest/" ``` -### Assembler Build - Before Re-homing - -``` -SiteNavigation -├─ HomeProvider: self -├─ PathPrefix: "" -│ -└─ (About to add elastic-docs with prefix "/guide") -``` - ### Assembler Build - After Re-homing ``` @@ -232,45 +217,49 @@ SiteNavigation │ └─ DocumentationSetNavigation (elastic-docs) ├─ HomeProvider: NEW NavigationHomeProvider("/guide", SiteNavigation) ⚡ - ├─ PathPrefix: "/guide" (from NEW provider) - ├─ NavigationRoot: SiteNavigation (from NEW provider) + ├─ PathPrefix: "/guide" (from new provider) + ├─ NavigationRoot: SiteNavigation (from new provider) │ └─ TableOfContentsNavigation (api/) - ├─ HomeProvider: same as parent = NEW provider ⚡ - ├─ PathPrefix: "/guide" (inherited from NEW provider) - ├─ NavigationRoot: SiteNavigation (inherited from NEW provider) + ├─ HomeProvider: inherited = new provider ⚡ + ├─ PathPrefix: "/guide" (from new provider) + ├─ NavigationRoot: SiteNavigation (from new provider) │ └─ FileNavigationLeaf (api/rest.md) - ├─ HomeAccessor.HomeProvider: NEW provider ⚡ + ├─ HomeAccessor.HomeProvider: new provider ⚡ └─ URL calculation: prefix = HomeProvider.PathPrefix = "/guide" path = "api/rest.md" url = "/guide/api/rest/" ✨ ``` -**The re-homing happened at line marked with ⚡ - a single assignment!** +**The re-homing happened at lines marked with ⚡ - a single assignment!** -## Why This Is Brilliant +## Key Characteristics -### 1. O(1) Re-homing +### O(1) Re-homing ```csharp -// This is ALL it takes to update thousands of URLs! +// This updates ALL URLs in the subtree - regardless of size! node.HomeProvider = new NavigationHomeProvider("/new-prefix", newRoot); ``` +**Time complexity: O(1)** + +This isn't marketing - it's a fact. Whether the subtree has 10 nodes or 10,000 nodes, re-homing takes the same amount of time because it's a single reference assignment. + **Compare to naive approach:** -- Naive: O(n) traversal of entire subtree -- Provider: O(1) single reference assignment +- Naive: O(n) - must visit every node +- Provider: O(1) - single assignment -### 2. Lazy Evaluation +### Lazy Evaluation -URLs are only calculated when accessed: -- Don't pay for URLs you never request -- Memory efficient - no stored URL strings +URLs calculated on-demand: +- Not calculated until accessed - Always reflects current provider state +- Memory efficient - no stored URL strings -### 3. Smart Caching +### Smart Caching ```csharp private string? _homeProviderCache; @@ -285,7 +274,7 @@ public string Url _homeProviderCache == _args.HomeAccessor.HomeProvider.Id && _urlCache != null) { - return _urlCache; // Cache hit! + return _urlCache; } // Recalculate and cache @@ -296,30 +285,17 @@ public string Url } ``` -**Benefits:** -- First access: O(path depth) calculation +Caching strategy: +- First access: O(depth) calculation - Subsequent accesses: O(1) cache lookup -- Cache invalidates automatically when provider changes (via Id) -- No manual cache management needed +- Cache invalidates automatically when provider changes (via Id comparison) -### 4. Scope Isolation +### Scope Isolation Each provider creates an isolated scope: - Changes to one scope don't affect others - Clear ownership of URL context -- Easy to reason about URL calculation -- Enables multiple "views" of same navigation tree - -### 5. Type Safety - -```csharp -// Can't forget to pass the accessor - it's in the constructor! -public FileNavigationLeaf( - TModel model, - IFileInfo fileInfo, - FileNavigationArgs args // Contains HomeAccessor -) -``` +- Enables independent re-homing of subtrees ## Implementation Details @@ -334,29 +310,28 @@ public class NavigationHomeProvider : INavigationHomeProvider } ``` -When a provider changes, the ID changes, invalidating all cached URLs. +When a provider changes, the ID changes, invalidating cached URLs. ### Accessor vs Provider -**Why separate interfaces?** +**Provider:** Nodes that create scopes (`DocumentationSetNavigation`, `TableOfContentsNavigation`) -- **Provider**: Nodes that CREATE scopes (DocumentationSetNavigation, TableOfContentsNavigation) -- **Accessor**: Nodes that USE scopes (all nodes) +**Accessor:** All nodes that need to calculate URLs -Some nodes are both: +Some nodes implement both: ```csharp public class DocumentationSetNavigation : INavigationHomeProvider, INavigationHomeAccessor { - // I can be a provider AND access a different provider + // Can be a provider AND access a different provider } ``` -This enables re-homing! +This dual implementation is what enables re-homing. -### Passing Accessors Down the Tree +### Passing Accessors Down -During construction, accessors flow down: +During construction, accessors flow down the tree: ```csharp // Parent creates child, passes its accessor @@ -365,11 +340,11 @@ var childNav = ConvertToNavigationItem( index, context, parent: this, - homeAccessor: this // Pass down the accessor chain + homeAccessor: this // Pass down accessor ); ``` -Children inherit their parent's accessor, creating a chain back to the scope provider. +Children inherit their parent's accessor, creating a reference chain back to the scope provider. ### Assembler-Specific Provider Behavior @@ -378,7 +353,6 @@ In assembler builds, TOCs create isolated providers: ```csharp var assemblerBuild = context.AssemblerBuild; -// For assembler builds, TOCs create their own home provider var isolatedHomeProvider = assemblerBuild ? new NavigationHomeProvider( homeAccessor.HomeProvider.PathPrefix, @@ -387,7 +361,9 @@ var isolatedHomeProvider = assemblerBuild : homeAccessor.HomeProvider; ``` -**Why?** This ensures TOCs can be re-homed independently during site assembly. +This ensures TOCs can be re-homed independently during site assembly. + +> See [Assembler Process](assembler-process.md) for details on how this flag controls scope creation. ## Performance Analysis @@ -396,7 +372,7 @@ var isolatedHomeProvider = assemblerBuild **Per Node:** - Provider: ~48 bytes (string, reference, guid) - Accessor: 8 bytes (reference) -- Cache: ~32 bytes (2 strings) - only on leaf nodes +- Cache: ~32 bytes (2 strings) - leaf nodes only **For 10,000 nodes:** - Without caching: ~560 KB @@ -410,19 +386,22 @@ var isolatedHomeProvider = assemblerBuild - Cache miss: O(depth) - string concatenation + path processing - Re-homing: O(1) - reference assignment -**Typical Access Pattern:** -- First access: Pay calculation cost -- Subsequent: Free cache lookups -- Re-home: Invalidate caches (cheap), recalculate on next access (lazy) +**Access Pattern:** +- First access: Calculate and cache +- Subsequent: Return cached value +- After re-homing: Recalculate on next access ### Scalability -The pattern scales beautifully: -- 100 nodes: Re-home in 1μs -- 10,000 nodes: Re-home in 1μs -- 1,000,000 nodes: Re-home in 1μs +Re-homing time is constant regardless of subtree size: -**Because it's always O(1)!** +| Subtree Size | Re-homing Time | +|--------------|----------------| +| 100 nodes | O(1) | +| 10,000 nodes | O(1) | +| 1,000,000 nodes | O(1) | + +This is O(1) because re-homing is a single reference assignment, regardless of how many nodes reference that provider. ## Common Patterns @@ -436,14 +415,11 @@ public class MyNavigation : INavigationHomeProvider, INavigationHomeAccessor public MyNavigation(string pathPrefix) { _pathPrefix = pathPrefix; - HomeProvider = this; // I am my own provider initially + HomeProvider = this; // Self-referential initially } - // Provider implementation public string PathPrefix => HomeProvider == this ? _pathPrefix : HomeProvider.PathPrefix; public IRootNavigationItem<...> NavigationRoot => /* ... */; - - // Accessor implementation public INavigationHomeProvider HomeProvider { get; set; } } ``` @@ -468,18 +444,17 @@ public class MyLeaf ### Pattern 3: Re-homing ```csharp -// In SiteNavigation or other assembler code void RehomeSubtree( INavigationHomeAccessor subtree, string newPrefix, IRootNavigationItem<...> newRoot) { subtree.HomeProvider = new NavigationHomeProvider(newPrefix, newRoot); - // Done! All URLs updated. + // ✅ All URLs updated } ``` -## Testing the Pattern +## Testing ### Unit Test Example @@ -487,31 +462,29 @@ void RehomeSubtree( [Fact] public void RehomingUpdatesUrlsDynamically() { - // Arrange: Create isolated navigation + // Create isolated navigation var docset = new DocumentationSetNavigation(...); var leaf = docset.NavigationItems.First() as FileNavigationLeaf; // Initial URL Assert.Equal("/api/rest/", leaf.Url); - // Act: Re-home the docset + // Re-home the docset docset.HomeProvider = new NavigationHomeProvider("/guide", siteNav); - // Assert: URL updated automatically! + // URL updated ✨ Assert.Equal("/guide/api/rest/", leaf.Url); } ``` ## Summary -The Home Provider pattern achieves: +The Home Provider pattern provides: -✅ **O(1) re-homing** - single reference assignment -✅ **Lazy evaluation** - calculate URLs only when needed -✅ **Smart caching** - O(1) repeated access -✅ **Memory efficient** - no stored URLs -✅ **Type safe** - compiler-enforced accessor passing -✅ **Scope isolation** - changes don't leak -✅ **Elegant code** - simple, understandable +✅ **O(1) re-homing** - Single reference assignment updates entire subtree +✅ **Lazy URL evaluation** - URLs calculated on-demand +✅ **Automatic cache invalidation** - Via provider ID comparison +✅ **Memory efficiency** - No stored URL strings +✅ **Scope isolation** - Changes don't leak between scopes -This pattern is what makes it possible to build isolated documentation repositories and then efficiently assemble them into a unified site with custom URL prefixes. Without it, we'd be stuck with expensive tree traversals or rigid, non-rehomable navigation structures. +This enables building isolated documentation repositories and efficiently assembling them into a unified site with custom URL prefixes. The O(1) re-homing is what makes the assembler build practical - without it, combining large documentation sites would require expensive tree traversal for every URL prefix change. diff --git a/docs/development/navigation/navigation.md b/docs/development/navigation/navigation.md index 4a5857785..b2b458654 100644 --- a/docs/development/navigation/navigation.md +++ b/docs/development/navigation/navigation.md @@ -4,14 +4,21 @@ This document provides an overview of how `Elastic.Documentation.Navigation` wor ## Documentation Structure -For deeper dives into specific topics, see: - -- **[Visual Walkthrough](visual-walkthrough.md)** - Visual tour with diagrams showing navigation structures in both build modes -- **[First Principles](first-principles.md)** - Core design principles and invariants that guide the navigation architecture -- **[Two-Phase Loading](two-phase-loading.md)** - Why configuration resolution and navigation construction are separate phases -- **[Home Provider Architecture](home-provider-architecture.md)** - The pattern that enables O(1) re-homing of navigation subtrees -- **[Node Types](node-types.md)** - Detailed reference for each navigation node type (leaves, nodes, roots) -- **[Assembler Process](assembler-process.md)** - How multiple repositories are combined into a unified site +**Start here:** +- **[Visual Walkthrough](visual-walkthrough.md)** - Visual tour with diagrams (best for first-time readers) +- **[First Principles](first-principles.md)** - Core design principles (functional and technical) + +**Learn the architecture:** +- **[Functional Principles](functional-principles.md)** - What the system does and why +- **[Two-Phase Loading](two-phase-loading.md)** - Configuration resolution vs navigation construction +- **[Node Types](node-types.md)** - Reference for each navigation node type + +**Understand key mechanisms:** +- **[Home Provider Architecture](home-provider-architecture.md)** - How O(1) re-homing works +- **[Assembler Process](assembler-process.md)** - How multiple repositories combine into one site + +**Implementation details:** +- **[Technical Principles](technical-principles.md)** - How the system is implemented ## Quick Start diff --git a/docs/development/navigation/node-types.md b/docs/development/navigation/node-types.md index 090ba39ef..8db44087a 100644 --- a/docs/development/navigation/node-types.md +++ b/docs/development/navigation/node-types.md @@ -2,6 +2,8 @@ This document provides a detailed reference for each navigation node type in `Elastic.Documentation.Navigation`. +> **Context:** For the acyclic graph structure that these nodes form, see [Functional Principles #8](functional-principles.md#8-acyclic-graph-structure). + ## Type Hierarchy ``` diff --git a/docs/development/navigation/two-phase-loading.md b/docs/development/navigation/two-phase-loading.md index 055020f46..134a42f0a 100644 --- a/docs/development/navigation/two-phase-loading.md +++ b/docs/development/navigation/two-phase-loading.md @@ -2,6 +2,8 @@ Navigation construction splits into two distinct phases: configuration resolution and navigation building. +> **Overview:** For a high-level understanding, see [Functional Principles #1](functional-principles.md#1-two-phase-loading). This document provides detailed implementation information. + ## Why Two Phases? Building navigation requires two fundamentally different operations: diff --git a/docs/development/navigation/visual-walkthrough.md b/docs/development/navigation/visual-walkthrough.md index ba0a22bc5..aaf9f1eba 100644 --- a/docs/development/navigation/visual-walkthrough.md +++ b/docs/development/navigation/visual-walkthrough.md @@ -1,545 +1,203 @@ # Visual Walkthrough -This document provides a visual tour of navigation structures in both isolated and assembler builds, showing how the same documentation can be represented differently depending on the build mode. +This visual guide shows how documentation navigation works in practice. We'll use diagrams to show how the same content appears differently in isolated vs assembler builds. + +> **Best for:** First-time readers who want to understand navigation visually before diving into details. ## Navigation Node Icons -Throughout this walkthrough, we use these icons to represent different node types: +These icons represent different parts of the navigation tree: -- ![DocumentationSetNavigation](images/bullet-documentation-set-navigation.svg) **DocumentationSetNavigation** - Root of a documentation repository -- ![TableOfContentsNavigation](images/bullet-table-of-contents-navigation.svg) **TableOfContentsNavigation** - A nested `toc.yml` section -- ![FolderNavigation](images/bullet-folder-navigation.svg) **FolderNavigation** - A directory with markdown files -- ![FileNavigationLeaf](images/bullet-file-navigation-leaf.svg) **FileNavigationLeaf** - An individual markdown file -- ![SiteNavigation](images/bullet-site-navigation.svg) **SiteNavigation** - Root of an assembled site (assembler builds only) +- ![DocumentationSetNavigation](images/bullet-documentation-set-navigation.svg) **DocumentationSetNavigation** - Root of a repository +- ![TableOfContentsNavigation](images/bullet-table-of-contents-navigation.svg) **TableOfContentsNavigation** - A `toc.yml` section +- ![FolderNavigation](images/bullet-folder-navigation.svg) **FolderNavigation** - A directory +- ![FileNavigationLeaf](images/bullet-file-navigation-leaf.svg) **FileNavigationLeaf** - A markdown file +- ![SiteNavigation](images/bullet-site-navigation.svg) **SiteNavigation** - Root of assembled site ## Isolated Builds -When building a single repository's documentation (e.g., `docs-builder isolated build`), the navigation is rooted at ![DocumentationSetNavigation](images/bullet-documentation-set-navigation.svg) **DocumentationSetNavigation**. - -### What's in an Isolated Build? - -An isolated build processes one `docset.yml` file, which defines the table of contents for that repository. This table of contents can include: - -**Direct Children:** -- ![FileNavigationLeaf](images/bullet-file-navigation-leaf.svg) **file:** - Individual markdown files -- ![FolderNavigation](images/bullet-folder-navigation.svg) **folder:** - Directories containing markdown files -- ![TableOfContentsNavigation](images/bullet-table-of-contents-navigation.svg) **toc:** - Nested `toc.yml` files for subsections +Building a single repository (e.g., `docs-builder isolated build`): **Example docset.yml:** ```yaml project: elastic-project toc: - - file: index.md # FileNavigationLeaf - - folder: getting-started # FolderNavigation - - toc: api # TableOfContentsNavigation → api/toc.yml - - toc: guides # TableOfContentsNavigation → guides/toc.yml + - file: index.md + - toc: api # api/toc.yml + - toc: guides # guides/toc.yml ``` -### Visual Example: Isolated Build Tree - -Imagine we have an `elastic-project` repository with the following structure: +### Visual: Isolated Build Tree ![Isolated Build](images/isolated-build-tree.svg) -**What we see in this diagram:** - -1. **Root:** ![DocumentationSetNavigation](images/bullet-documentation-set-navigation.svg) `elastic-project://` - - This is the root of the entire navigation - - Identifier: `elastic-project://` - - URL: `/` (or custom base path) - -2. **Index File:** ![FileNavigationLeaf](images/bullet-file-navigation-leaf.svg) `index.md` - - The landing page for the documentation - - URL: `/` - -3. **Nested TOCs:** ![TableOfContentsNavigation](images/bullet-table-of-contents-navigation.svg) `api/` and `guides/` - - Each represents a `toc.yml` file - - Creates a scope for child navigation items - - Identifiers: `elastic-project://api`, `elastic-project://guides` - - URLs: `/api/`, `/guides/` - -4. **Child Files:** ![FileNavigationLeaf](images/bullet-file-navigation-leaf.svg) Under each TOC - - Individual markdown files in those sections - - URLs relative to parent TOC: `/api/overview/`, `/guides/getting-started/` - -### Key Characteristics of Isolated Builds - -**Navigation Root:** -- All nodes point to ![DocumentationSetNavigation](images/bullet-documentation-set-navigation.svg) as their `NavigationRoot` -- This is the ultimate parent in the tree - -**URL Structure:** -- All URLs are relative to the documentation set root -- Default: starts at `/` -- Can be customized with `--canonical-base-url` +**What the diagram shows:** -**Identifiers:** -- Docset: `{repository}://` (e.g., `elastic-project://`) -- Nested TOCs: `{repository}://{path}` (e.g., `elastic-project://api`) +- ![DocumentationSetNavigation](images/bullet-documentation-set-navigation.svg) `elastic-project://` - Repository root +- ![TableOfContentsNavigation](images/bullet-table-of-contents-navigation.svg) `api/` and `guides/` - Sections from `toc.yml` files +- ![FileNavigationLeaf](images/bullet-file-navigation-leaf.svg) Individual files under each section +- URLs: `/api/overview/`, `/guides/getting-started/` -**Home Provider:** -- DocumentationSetNavigation is its own home provider -- PathPrefix: `""` (empty, or custom base URL) -- All children inherit this provider - -**Purpose:** -- Fast iteration for documentation teams -- Test documentation in isolation -- No dependencies on other repositories -- Validate links, structure, and content +**Key points:** +- One repository = one navigation tree +- URLs default to `/` (configurable with `--url-path-prefix`) +- Fast for testing and iteration --- ## Assembler Builds -When building a unified site from multiple repositories (e.g., `docs-builder assemble`), the navigation is rooted at ![SiteNavigation](images/bullet-site-navigation.svg) **SiteNavigation**. - -### What's in an Assembler Build? - -An assembler build combines multiple isolated builds into a single site, defined by `config/navigation.yml`. - -**Key Differences from Isolated:** -- Multiple repositories combined into one navigation tree -- Custom URL prefixes for each section (`path_prefix`) -- ![SiteNavigation](images/bullet-site-navigation.svg) SiteNavigation as the ultimate root -- Navigation items are **re-homed** to use new URL prefixes - -### What Can Be Referenced? - -In `navigation.yml`, you can reference: - -1. **Entire Documentation Sets:** ![DocumentationSetNavigation](images/bullet-documentation-set-navigation.svg) - - Syntax: `{repository}://` - - Example: `elastic-project://`, `kibana://`, `elasticsearch://` - -2. **Nested Table of Contents:** ![TableOfContentsNavigation](images/bullet-table-of-contents-navigation.svg) - - Syntax: `{repository}://{path/to/toc}` - - Example: `elastic-project://api`, `kibana://setup` - -**You cannot directly reference:** -- Individual files -- Folders -- Virtual files - -These are automatically included as children of their parent TOC or docset. - -### Visual Example: Splitting a Docset +Combining multiple repositories into one site (e.g., `docs-builder assemble`): -Let's take the same `elastic-project` from the isolated build and split it across the site: +**Example: Split One Repository Across Site** -**Isolated Build Had:** -``` -elastic-project:// -├── /api/ -└── /guides/ -``` +Take the same `elastic-project` from above and split it: -**Assembler Navigation (navigation.yml):** ```yaml +# navigation.yml toc: - - toc: docs-content://elasticsearch - children: - # Pull elastic-project's API section - - toc: elastic-project://api - path_prefix: elasticsearch/api - - # Pull elastic-project's Guides section - - toc: elastic-project://guides - path_prefix: elasticsearch/guides -``` - -**What This Means:** -- The `api` section moves from `/api/` → `/elasticsearch/api/` -- The `guides` section moves from `/guides/` → `/elasticsearch/guides/` -- Same files, same structure, different URLs + - toc: elastic-project://api + path_prefix: elasticsearch/api -### Visual Example: Composing Multiple Repositories - -Here's a more complex example showing multiple repositories assembled into one site: - -![TableOfContentsNavigation](images/bullet-table-of-contents-navigation.svg) **Site Structure (navigation.yml):** -``` -Site Root -├── Elasticsearch Section -│ ├── elastic-project://api → /elasticsearch/api -│ └── elastic-project://guides → /elasticsearch/guides -│ -├── Kibana Section -│ └── kibana:// → /kibana -│ -├── Logstash Section -│ └── logstash:// → /logstash -│ -└── Client Libraries - ├── elastic-client-node:// → /clients/node - ├── elastic-client-dotnet:// → /clients/dotnet - └── elastic-client-java:// → /clients/java + - toc: elastic-project://guides + path_prefix: elasticsearch/guides ``` -**Corresponding navigation.yml:** -```yaml -toc: - # Elasticsearch section - - toc: elasticsearch - children: - - toc: elastic-project://api - path_prefix: elasticsearch/api - - toc: elastic-project://guides - path_prefix: elasticsearch/guides - - # Kibana section - - toc: kibana:// - path_prefix: kibana - - # Logstash section - - toc: logstash:// - path_prefix: logstash - - # Client libraries - - toc: clients - children: - - toc: elastic-client-node:// - path_prefix: clients/node - - toc: elastic-client-dotnet:// - path_prefix: clients/dotnet - - toc: elastic-client-java:// - path_prefix: clients/java -``` +**Result:** +- `/api/` → `/elasticsearch/api/` +- `/guides/` → `/elasticsearch/guides/` -### Visual Example: Fully Resolved Assembler Build +Same content, different URLs! -When the assembler processes `navigation.yml`, it creates this structure: +### Visual: Assembler Build Tree ![Assembler Build](images/assembler-build-tree.svg) -**What we see in this diagram:** - -1. **Ultimate Root:** ![SiteNavigation](images/bullet-site-navigation.svg) `site://` - - Root of the entire assembled site - - Identifier: `site://` - - All nodes ultimately point here as `NavigationRoot` - -2. **Re-homed Documentation Sets:** ![DocumentationSetNavigation](images/bullet-documentation-set-navigation.svg) - - Each repository's docset is re-homed with a custom `path_prefix` - - Example: `elastic-project://` moves from `/` → `/elasticsearch/` - - Home provider updated to use new prefix - -3. **Re-homed Nested TOCs:** ![TableOfContentsNavigation](images/bullet-table-of-contents-navigation.svg) - - Individual TOCs can be pulled out and placed independently - - Example: `elastic-project://api` moves from `/api/` → `/elasticsearch/api/` - - Separate from its parent docset in the site structure - -4. **Child Files:** ![FileNavigationLeaf](images/bullet-file-navigation-leaf.svg) - - Files inherit the new path prefix from their parent - - URLs automatically recalculate based on new home provider - - No code changes needed - it's dynamic! - -### Key Characteristics of Assembler Builds - -**Navigation Root:** -- All nodes point to ![SiteNavigation](images/bullet-site-navigation.svg) as their `NavigationRoot` -- This is the ultimate parent for the entire site - -**URL Structure:** -- URLs use custom `path_prefix` from `navigation.yml` -- Example: `/elasticsearch/api/overview/`, `/kibana/setup/install/` -- Prefixes must be unique across the site - -**Re-homing Process:** -- Each referenced node gets a new home provider -- New provider has custom `PathPrefix` and `NavigationRoot = SiteNavigation` -- One line of code: `node.HomeProvider = new NavigationHomeProvider(pathPrefix, siteNav)` -- All descendant URLs update automatically (O(1) operation!) - -**Identifiers:** -- Site: `site://` -- Docsets: `{repository}://` (unchanged from isolated) -- TOCs: `{repository}://{path}` (unchanged from isolated) -- Identifiers don't change - they're stable across build modes - -**Purpose:** -- Unified navigation across multiple repositories -- Organize docs by product/feature, not repository -- Custom URL structure independent of repository structure -- Single site with consistent navigation +**What the diagram shows:** ---- +- ![SiteNavigation](images/bullet-site-navigation.svg) `site://` - Root of entire assembled site +- ![TableOfContentsNavigation](images/bullet-table-of-contents-navigation.svg) `elastic-project://api` - Re-homed from `/api/` to `/elasticsearch/api/` +- ![TableOfContentsNavigation](images/bullet-table-of-contents-navigation.svg) `elastic-project://guides` - Re-homed from `/guides/` to `/elasticsearch/guides/` +- ![FileNavigationLeaf](images/bullet-file-navigation-leaf.svg) Files automatically use new prefixes + +**Key points:** +- Multiple repositories → one site +- Custom URL prefixes via `path_prefix` +- Re-homing changes URLs without rebuilding -## Comparing Build Modes +> **How re-homing works:** See [Assembler Process](assembler-process.md) for the four-phase assembly process and [Home Provider Architecture](home-provider-architecture.md) for how URLs update instantly (O(1)). -### Same Content, Different URLs +--- -Here's how the same file appears in different builds: +## Same File, Different URLs **File:** `elastic-project/api/overview.md` **Isolated Build:** ``` -DocumentationSetNavigation (elastic-project://) - └── TableOfContentsNavigation (elastic-project://api) - └── FileNavigationLeaf (api/overview.md) - Url: /api/overview/ - NavigationRoot: DocumentationSetNavigation - HomeProvider.PathPrefix: "" +URL: /api/overview/ ``` **Assembler Build:** ``` -SiteNavigation (site://) - └── TableOfContentsNavigation (elastic-project://api) RE-HOMED! - └── FileNavigationLeaf (api/overview.md) - Url: /elasticsearch/api/overview/ ← Different! - NavigationRoot: SiteNavigation ← Different! - HomeProvider.PathPrefix: "/elasticsearch/api" ← Different! +URL: /elasticsearch/api/overview/ ``` -**Key Insight:** Same node objects, different URLs. No tree reconstruction! +Same file, same tree structure, different URL prefix. The assembler re-homes the navigation subtree without rebuilding it. -### Flexibility in Assembly +--- -The assembler gives you complete freedom to reorganize: +## Common Assembly Patterns -**Scenario 1: Keep Docset Together** +**Keep repository together:** ```yaml - toc: elastic-project:// path_prefix: elasticsearch ``` -Result: All of `elastic-project` under `/elasticsearch/` +→ Everything under `/elasticsearch/` -**Scenario 2: Split Docset Apart** +**Split repository apart:** ```yaml - toc: elastic-project://api path_prefix: reference/api - - toc: elastic-project://guides path_prefix: learn/guides ``` -Result: API under `/reference/api/`, guides under `/learn/guides/` +→ API under `/reference/api/`, guides under `/learn/guides/` -**Scenario 3: Nest Docsets** +**Combine multiple repositories:** ```yaml -- toc: products +- toc: clients children: - - toc: elasticsearch:// - path_prefix: products/elasticsearch - - toc: kibana:// - path_prefix: products/kibana + - toc: java-client:// + path_prefix: clients/java + - toc: dotnet-client:// + path_prefix: clients/dotnet ``` -Result: Products organized hierarchically - -### Navigation Requirements - -**Both Builds:** -- Every node must have a unique URL -- Navigation must form a tree (no cycles) -- Root nodes must have an index file - -**Isolated Only:** -- Root is always DocumentationSetNavigation -- URLs relative to docset root - -**Assembler Only:** -- Root is always SiteNavigation -- `path_prefix` required for each reference (except narrative repo) -- `path_prefix` values must be unique across the site -- Must declare all referenced docsets/TOCs +→ All clients under `/clients/` --- -## Working with the Visual Structure - -### Adding Files to Navigation - -**In isolated build (docset.yml):** -```yaml -toc: - - file: new-guide.md # Adds FileNavigationLeaf -``` -Result: ![FileNavigationLeaf](images/bullet-file-navigation-leaf.svg) at `/new-guide/` - -**In assembler build:** -The file is automatically included as a child of its parent TOC/docset. No changes needed to `navigation.yml`. +## What You Can Reference -### Adding a Nested TOC - -**In isolated build (docset.yml):** -```yaml -toc: - - toc: new-section # References new-section/toc.yml -``` -Result: ![TableOfContentsNavigation](images/bullet-table-of-contents-navigation.svg) at `/new-section/` +In `navigation.yml`, you can reference: -**In assembler build (navigation.yml):** +**Entire repositories:** ```yaml -toc: - - toc: elastic-project://new-section - path_prefix: elasticsearch/new-section +- toc: elastic-project:// + path_prefix: elasticsearch ``` -Result: ![TableOfContentsNavigation](images/bullet-table-of-contents-navigation.svg) at `/elasticsearch/new-section/` - -### Moving Content in Assembly -Want to reorganize the site? Just update `path_prefix` in `navigation.yml`: - -**Before:** +**Individual TOC sections:** ```yaml - toc: elastic-project://api - path_prefix: api + path_prefix: elasticsearch/api ``` -URLs: `/api/overview/`, `/api/rest/` -**After:** -```yaml -- toc: elastic-project://api - path_prefix: reference/elasticsearch/api -``` -URLs: `/reference/elasticsearch/api/overview/`, `/reference/elasticsearch/api/rest/` +**You cannot reference:** +- Individual files +- Folders -No changes to the repository, no code changes, just configuration! +Files and folders are automatically included as children of their parent TOC. + +> **Node type details:** See [Node Types](node-types.md) for complete reference on each navigation node type. --- ## Phantom Nodes -Sometimes you want to reference a docset/TOC in configuration but not include it in the assembled navigation. These are **phantoms**. +Acknowledge content exists without including it in the site: -**Example (navigation.yml):** ```yaml +# navigation.yml phantoms: - source: plugins:// - source: cloud://monitoring ``` -**What This Means:** -- `plugins://` and `cloud://monitoring` are acknowledged to exist -- They're not included in the site navigation tree -- Cross-links to them won't trigger "undeclared navigation" warnings -- Useful for: - - Work-in-progress sections - - Legacy content being phased out - - External content that's referenced but not hosted - -**Visual Representation:** -``` -SiteNavigation (site://) -├── elasticsearch:// (included, solid line) -├── kibana:// (included, solid line) -└── plugins:// (phantom, dotted line - not in tree) -``` - ---- - -## Common Patterns - -### Pattern 1: Product-Centric Organization - -```yaml -toc: - - toc: elasticsearch - children: - - toc: es-guide:// - path_prefix: elasticsearch/guide - - toc: es-reference:// - path_prefix: elasticsearch/reference - - - toc: kibana - children: - - toc: kibana-guide:// - path_prefix: kibana/guide - - toc: kibana-reference:// - path_prefix: kibana/reference -``` - -Each product has guide and reference sections under a common prefix. - -### Pattern 2: Audience-Centric Organization - -```yaml -toc: - - toc: getting-started - children: - - toc: es-guide://getting-started - path_prefix: getting-started/elasticsearch - - toc: kibana-guide://getting-started - path_prefix: getting-started/kibana - - - toc: advanced - children: - - toc: es-reference://advanced - path_prefix: advanced/elasticsearch - - toc: kibana-reference://advanced - path_prefix: advanced/kibana -``` - -Content organized by user journey, pulling from multiple repositories. - -### Pattern 3: Client Libraries Collection - -```yaml -toc: - - toc: clients - children: - - toc: java-client:// - path_prefix: clients/java - - toc: dotnet-client:// - path_prefix: clients/dotnet - - toc: python-client:// - path_prefix: clients/python -``` +**Use for:** +- Work-in-progress content +- Legacy content being phased out +- External content that's cross-linked but not hosted -All client libraries under one section, each with its own prefix. +This prevents "undeclared navigation" warnings. --- -## Troubleshooting Visual Issues - -### "Why isn't my TOC showing up?" - -Check if: -1. It's referenced in `navigation.yml` with a `path_prefix` -2. The identifier matches exactly (case-sensitive!) -3. It's not declared as a phantom -4. The parent docset is built and available - -### "Why are URLs wrong?" - -Check: -1. `path_prefix` in `navigation.yml` is correct -2. No duplicate `path_prefix` values -3. HomeProvider was set correctly during re-homing -4. Cache hasn't gone stale (HomeProvider ID changed?) +## Summary -### "Why can't I reference a file directly?" +**Isolated builds:** One repository → one navigation tree (default prefix `/`) -You can't! The assembler only works with: -- ![DocumentationSetNavigation](images/bullet-documentation-set-navigation.svg) Entire docsets (`repo://`) -- ![TableOfContentsNavigation](images/bullet-table-of-contents-navigation.svg) Nested TOCs (`repo://path`) +**Assembler builds:** Multiple repositories → one site with custom URL prefixes -Files are automatically included as children of their parent. +**The key insight:** Same navigation structure, different URLs. Re-homing changes URL prefixes without rebuilding the tree. --- -## Summary +## Learn More -**Isolated Builds:** -- Single repository -- ![DocumentationSetNavigation](images/bullet-documentation-set-navigation.svg) DocumentationSetNavigation as root -- URLs relative to `/` -- Fast iteration, no dependencies - -**Assembler Builds:** -- Multiple repositories -- ![SiteNavigation](images/bullet-site-navigation.svg) SiteNavigation as root -- Custom `path_prefix` for each section -- Flexible organization across repository boundaries - -**The Magic:** -- Same node objects in both builds -- URLs calculated dynamically from HomeProvider -- Re-homing is O(1) - just change the provider reference -- No tree reconstruction needed - -For implementation details, see: -- [Assembler Process](assembler-process.md) - How assembly works -- [Home Provider Architecture](home-provider-architecture.md) - How re-homing works -- [Node Types](node-types.md) - Details on each node type +- **[First Principles](first-principles.md)** - Core design decisions +- **[Assembler Process](assembler-process.md)** - Four-phase assembly explained +- **[Home Provider Architecture](home-provider-architecture.md)** - How O(1) re-homing works +- **[Node Types](node-types.md)** - Complete reference for each node type +- **[Two-Phase Loading](two-phase-loading.md)** - Configuration vs navigation construction diff --git a/docs/development/toc.yml b/docs/development/toc.yml index f54f28324..58a8dc394 100644 --- a/docs/development/toc.yml +++ b/docs/development/toc.yml @@ -8,8 +8,11 @@ toc: - file: navigation.md - file: visual-walkthrough.md - file: first-principles.md + children: + - file: functional-principles.md + - file: technical-principles.md - file: two-phase-loading.md - - file: home-provider-architecture.md - file: node-types.md + - file: home-provider-architecture.md - file: assembler-process.md - toc: link-validation From eb735f8a9b02cff1c721b995e14b5b76e7337aa7 Mon Sep 17 00:00:00 2001 From: Martijn Laarman Date: Mon, 3 Nov 2025 14:23:34 +0100 Subject: [PATCH 155/171] fix test --- .../navigation/functional-principles.md | 184 ++++++++++++++++++ .../navigation/technical-principles.md | 123 ++++++++++++ .../Isolation/PhysicalDocsetTests.cs | 4 +- 3 files changed, 309 insertions(+), 2 deletions(-) create mode 100644 docs/development/navigation/functional-principles.md create mode 100644 docs/development/navigation/technical-principles.md diff --git a/docs/development/navigation/functional-principles.md b/docs/development/navigation/functional-principles.md new file mode 100644 index 000000000..fc15b37a0 --- /dev/null +++ b/docs/development/navigation/functional-principles.md @@ -0,0 +1,184 @@ +# Functional Principles + +These principles define what the navigation system does and why. + +> **Also see:** [Technical Principles](technical-principles.md) for implementation details. + +## 1. Two-Phase Loading + +Navigation construction follows a strict two-phase approach: + +**Phase 1: Configuration Resolution** (`Elastic.Documentation.Configuration`) +- Parse YAML files (`docset.yml`, `toc.yml`, `navigation.yml`) +- Resolve all file references to **full paths** relative to documentation set root +- Validate configuration structure and relationships +- Output: Fully resolved configuration objects with complete file paths + +**Phase 2: Navigation Construction** (`Elastic.Documentation.Navigation`) +- Consume resolved configuration from Phase 1 +- Build navigation tree with **full URLs** +- Create node relationships (parent/child/root) +- Set up home providers for URL calculation +- Output: Complete navigation tree with calculated URLs + +**Why Two Phases?** +- **Separation of Concerns**: Configuration parsing is independent of navigation structure +- **Validation**: Catch file/structure errors before building expensive navigation trees +- **Reusability**: Same configuration can build different navigation structures (isolated vs assembler) +- **Performance**: Resolve file system operations once, reuse for navigation + +> See [Two-Phase Loading](two-phase-loading.md) for detailed explanation. + +## 2. Single Documentation Source + +URLs are always built relative to the documentation set's source directory: +- Files referenced in `docset.yml` are relative to the docset root +- Files referenced in nested `toc.yml` are relative to the toc directory +- During Phase 1, all paths are resolved to be relative to the docset root +- During Phase 2, URLs are calculated from these resolved paths + +**Example:** +``` +docs/ +├── docset.yml # Root +├── index.md +└── api/ + ├── toc.yml # Nested TOC + └── rest.md +``` + +Phase 1 resolves `api/toc.yml` reference to `rest.md` as: `api/rest.md` (relative to docset root) +Phase 2 builds URL as: `/api/rest/` + +## 3. URL Building is Dynamic and Cheap + +URLs are **calculated on-demand**, not stored: +- Nodes don't store their final URL +- URLs are computed from `HomeProvider.PathPrefix` + relative path +- Changing a `HomeProvider` instantly updates all descendant URLs +- No tree traversal needed to update URLs + +**Why Dynamic?** +- **Re-homing**: Same subtree can have different URLs in different contexts +- **Memory Efficient**: Don't store redundant URL strings +- **Consistency**: URLs always reflect current home provider state + +> See [Home Provider Architecture](home-provider-architecture.md) for implementation details. + +## 4. Navigation Roots Can Be Re-homed + +A key design feature that enables assembler builds: +- **Isolated Build**: Each `DocumentationSetNavigation` is its own root +- **Assembler Build**: `SiteNavigation` becomes the root, docsets are "re-homed" +- **Re-homing**: Replace a subtree's `HomeProvider` to change its URL prefix +- **Cheap Operation**: O(1) - just replace the provider reference + +**Example:** +```csharp +// Isolated: URLs start at / +homeProvider.PathPrefix = ""; +// → /api/rest/ + +// Assembled: Re-home to /guide +homeProvider = new NavigationHomeProvider("/guide", siteNav); +// → /guide/api/rest/ +``` + +> See [Assembler Process](assembler-process.md) for how re-homing works in practice. + +## 5. Navigation Scope via HomeProvider + +`INavigationHomeProvider` creates navigation scopes: +- **Provider**: Defines `PathPrefix` and `NavigationRoot` for a scope +- **Accessor**: Children use `INavigationHomeAccessor` to access their scope +- **Inheritance**: Child nodes inherit their parent's accessor +- **Isolation**: Changes to a provider only affect its scope + +**Scope Creators:** +- `DocumentationSetNavigation` - Creates scope for entire docset +- `TableOfContentsNavigation` - Creates scope for TOC subtree (enables re-homing) + +**Scope Consumers:** +- `FileNavigationLeaf` - Uses accessor to calculate URL +- `FolderNavigation` - Passes accessor to children +- `VirtualFileNavigation` - Passes accessor to children + +## 6. Index Files Determine Folder URLs + +Every folder/node navigation has an **Index**: +- Index is either `index.md` or the first file +- The node's URL is the same as its Index's URL +- Children appear "under" the index in navigation +- Index files map to folder paths: `/api/index.md` → `/api/` + +**Why?** +- **Consistent URL Structure**: Folders and their indexes share the same URL +- **Natural Navigation**: Index represents the folder's landing page +- **Hierarchical**: Clear parent-child URL relationships + +## 7. File Structure Should Mirror Navigation + +Best practices for maintainability: +- Navigation structure should follow file system structure +- Avoid deep-linking files from different directories +- Use `folder:` references when possible +- Virtual files should group sibling files, not restructure the tree + +**Rationale:** +- **Discoverability**: Developers can find files by following navigation +- **Predictability**: URL structure matches file structure +- **Maintainability**: Moving files in navigation matches moving them on disk + +## 8. Acyclic Graph Structure + +The navigation forms a **directed acyclic graph (DAG)**: +- **Tree Structure**: Each node has exactly one parent (except root) +- **No Cycles**: Following parent pointers always terminates at root +- **Single Root**: Every node has a `NavigationRoot` pointing to the ultimate ancestor +- **Predictable Traversal**: Tree structure enables efficient queries and traversal + +**Why This Matters:** +- **URL Uniqueness**: Tree structure ensures each file has one canonical URL +- **Consistent Hierarchy**: Clear parent-child relationships for breadcrumbs and navigation +- **Efficient Queries**: Can traverse up (to root) or down (to leaves) without cycle detection +- **Re-homing Safety**: Replacing a subtree's root doesn't create cycles + +**Invariants:** +1. Following `.Parent` chain always reaches root (or null for root) +2. Following `.NavigationRoot` immediately reaches ultimate root +3. No node can be its own ancestor +4. Every node appears exactly once in the tree + +## 9. Phantom Nodes for Incomplete Navigation + +`navigation.yml` can declare phantoms: +```yaml +phantoms: + - source: plugins:// +``` + +**Purpose:** +- Reference nodes that exist but aren't included in site navigation +- Prevent "undeclared navigation" warnings +- Document intentionally excluded content +- Enable validation of cross-links + +--- + +## Key Invariants + +1. **Phase Order**: Configuration must be fully resolved before navigation construction +2. **Path Resolution**: All paths in configuration are relative to docset root after Phase 1 +3. **URL Uniqueness**: Every navigation item must have a unique URL within its site +4. **Root Consistency**: All nodes in a subtree point to the same `NavigationRoot` +5. **Provider Validity**: A node's `HomeProvider` must be an ancestor in the tree +6. **Index Requirement**: All node navigations (folder/toc/docset) must have an Index +7. **Path Prefix Uniqueness**: In assembler builds, all `path_prefix` values must be unique + +## Performance Characteristics + +- **Tree Construction**: O(n) where n = number of files +- **URL Calculation**: O(depth) for first access, O(1) with caching +- **Re-homing**: O(1) - just replace HomeProvider reference +- **Tree Traversal**: O(n) for full tree, but rarely needed +- **Memory**: O(n) for nodes, URLs computed on-demand diff --git a/docs/development/navigation/technical-principles.md b/docs/development/navigation/technical-principles.md new file mode 100644 index 000000000..5bd44a568 --- /dev/null +++ b/docs/development/navigation/technical-principles.md @@ -0,0 +1,123 @@ +# Technical Principles + +These principles define how the navigation system is implemented. + +> **Prerequisites:** Read [Functional Principles](functional-principles.md) first to understand what the system does and why. + +## 1. Generic Type System for Covariance + +Navigation classes are generic over `TModel`: +```csharp +public class DocumentationSetNavigation + where TModel : class, IDocumentationFile +``` + +**Why Generic?** + +**Covariance Enables Static Typing:** +```csharp +// Without covariance: always get base interface, requires runtime casts +INodeNavigationItem node = GetNode(); +if (node.Model is MarkdownFile markdown) // Runtime check required +{ + var content = markdown.Content; +} + +// With covariance: query for specific type statically +INodeNavigationItem node = QueryForMarkdownNodes(); +var content = node.Model.Content; // ✓ No cast needed! Static type safety +``` + +**Benefits:** +- **Type Safety**: Query methods can return specific types like `INodeNavigationItem` +- **No Runtime Casts**: Access `.Model.Content` directly without casting +- **Compile-Time Errors**: Type mismatches caught during compilation, not runtime +- **Better IntelliSense**: IDEs show correct members for specific model types +- **Flexibility**: Same navigation code works with different file models (MarkdownFile, ApiDocFile, etc.) + +**Example:** +```csharp +// Query for nodes with specific model type +var markdownNodes = navigation.NavigationItems + .OfType>(); + +foreach (var node in markdownNodes) +{ + // No cast needed! Static typing + Console.WriteLine(node.Model.FrontMatter); + Console.WriteLine(node.Model.Content); +} +``` + +## 2. Provider Pattern for URL Context + +`INavigationHomeProvider` / `INavigationHomeAccessor`: +- **Providers** define context (PathPrefix, NavigationRoot) +- **Accessors** reference providers +- Decouples URL calculation from tree structure +- Enables context switching (re-homing) + +**Why This Enables Re-homing:** +```csharp +// Isolated build +node.HomeProvider = new NavigationHomeProvider("", docsetRoot); +// URLs: /api/rest/ + +// Assembler build - O(1) operation! +node.HomeProvider = new NavigationHomeProvider("/guide", siteRoot); +// URLs: /guide/api/rest/ +``` + +Single reference change updates all descendant URLs. + +> See [Home Provider Architecture](home-provider-architecture.md) for complete explanation. + +## 3. Lazy URL Calculation with Caching + +`FileNavigationLeaf` implements smart URL caching: +```csharp +private string? _homeProviderCache; +private string? _urlCache; + +public string Url +{ + get + { + if (_homeProviderCache == HomeProvider.Id && _urlCache != null) + return _urlCache; + + _urlCache = CalculateUrl(); + _homeProviderCache = HomeProvider.Id; + return _urlCache; + } +} +``` + +**Strategy:** +- Cache URL along with HomeProvider ID +- Invalidate cache when HomeProvider changes +- Recalculate only when needed +- O(1) for repeated access, O(depth) for calculation + +**Why HomeProvider.Id?** +- Each HomeProvider has a unique ID +- Comparing IDs is cheaper than deep equality checks +- ID changes when provider is replaced during re-homing +- Automatic cache invalidation without explicit cache clearing + +--- + +## Performance Characteristics + +- **Tree Construction**: O(n) where n = number of files +- **URL Calculation**: O(depth) for first access, O(1) with caching +- **Re-homing**: O(1) - just replace HomeProvider reference +- **Tree Traversal**: O(n) for full tree, but rarely needed +- **Memory**: O(n) for nodes, URLs computed on-demand + +**Why Re-homing is O(1):** +1. Replace single HomeProvider reference +2. No tree traversal required +3. URLs lazy-calculated on next access +4. Cache invalidation via ID comparison +5. All descendants automatically use new provider diff --git a/tests/Navigation.Tests/Isolation/PhysicalDocsetTests.cs b/tests/Navigation.Tests/Isolation/PhysicalDocsetTests.cs index b85ef70b4..1ddfb7e1c 100644 --- a/tests/Navigation.Tests/Isolation/PhysicalDocsetTests.cs +++ b/tests/Navigation.Tests/Isolation/PhysicalDocsetTests.cs @@ -122,10 +122,10 @@ public async Task PhysicalDocsetNavigationIncludesNestedTocs() var developmentToc = tocNavs.FirstOrDefault(t => t.Url == "/development/"); developmentToc.Should().NotBeNull(); - developmentToc.NavigationItems.Should().HaveCount(2); + developmentToc.NavigationItems.Should().HaveCount(3); developmentToc.Index.Should().NotBeNull(); developmentToc.NavigationItems.OfType>().Should().HaveCount(0); - developmentToc.NavigationItems.OfType>().Should().HaveCount(1); + developmentToc.NavigationItems.OfType>().Should().HaveCount(2); developmentToc.NavigationItems.OfType>().Should().HaveCount(1); var developmentIndex = developmentToc.Index as FileNavigationLeaf; From 805101de1979ac129c34befd2f889cb506b68f4e Mon Sep 17 00:00:00 2001 From: Martijn Laarman Date: Mon, 3 Nov 2025 14:45:39 +0100 Subject: [PATCH 156/171] fix local build docs errors --- README.md | 2 +- docs/development/navigation/README.md | 331 ------------- .../navigation/assembler-process.md | 2 +- .../navigation/home-provider-architecture.md | 2 +- .../navigation/images/assembler-build.png | Bin 112941 -> 0 bytes .../navigation/images/isolated-build.png | Bin 42909 -> 0 bytes docs/development/navigation/node-types.md | 2 +- .../navigation/two-phase-loading.md | 2 +- .../README.md | 452 ++++++++++++------ 9 files changed, 310 insertions(+), 483 deletions(-) delete mode 100644 docs/development/navigation/README.md delete mode 100644 docs/development/navigation/images/assembler-build.png delete mode 100644 docs/development/navigation/images/isolated-build.png diff --git a/README.md b/README.md index a0555d18c..e6c4567c7 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -#ocs-builder +#docs-builder [![ci](https://github.com/elastic/docs-builder/actions/workflows/ci.yml/badge.svg?branch=main&event=push)](https://github.com/elastic/docs-builder/actions/workflows/ci.yml) diff --git a/docs/development/navigation/README.md b/docs/development/navigation/README.md deleted file mode 100644 index 68cdf4e64..000000000 --- a/docs/development/navigation/README.md +++ /dev/null @@ -1,331 +0,0 @@ -# Navigation Documentation - -Welcome to the documentation for `Elastic.Documentation.Navigation`, the library that powers documentation navigation for Elastic's documentation sites. - -## What This Is - -This library builds hierarchical navigation trees for documentation sites with a unique capability: navigation built for isolated repositories can be **efficiently re-homed** during site assembly without rebuilding the entire tree. - -**Why does this matter?** - -Individual documentation teams can build and test their docs in isolation with URLs like `/api/overview/`, then those same docs can be assembled into a unified site with URLs like `/elasticsearch/api/overview/` - with **zero tree reconstruction**. It's an O(1) operation. - -## Documentation Map - -Start with any document based on what you want to learn: - -### 🎯 [navigation.md](navigation.md) - Start Here -**Overview of the navigation system** - -Read this first to understand: -- The two build modes (isolated vs assembler) -- Core concepts at a high level -- Quick introduction to re-homing -- Links to detailed documentation - -### 🎨 [visual-walkthrough.md](visual-walkthrough.md) - See It In Action -**Visual tour with diagrams showing navigation structures** - -Read this to understand: -- What different node types look like in the tree -- How isolated builds differ from assembler builds visually -- How the same content appears with different URLs -- How to split and reorganize documentation across sites -- Common patterns for multi-repository organization -- Includes actual tree diagrams from this repository - -### 🧭 [first-principles.md](first-principles.md) - Design Philosophy -**Core principles that guide the architecture** - -Read this to understand: -- Why two-phase loading (configuration → navigation) -- Why URLs are calculated dynamically, not stored -- Why navigation roots can be re-homed -- Design patterns used (factory, provider, visitor) -- Performance characteristics and invariants - -### 🔄 [two-phase-loading.md](two-phase-loading.md) - The Loading Process -**Deep dive into Phase 1 (configuration) and Phase 2 (navigation)** - -Read this to understand: -- What happens in Phase 1: Configuration resolution -- What happens in Phase 2: Navigation construction -- Why these phases are separate -- Data flow diagrams -- How to test each phase independently - -### 🏠 [home-provider-architecture.md](home-provider-architecture.md) - The Re-homing Magic -**How O(1) re-homing works** - -Read this to understand: -- The problem: naive re-homing requires O(n) tree traversal -- The solution: HomeProvider pattern with indirection -- How `INavigationHomeProvider` and `INavigationHomeAccessor` work -- Why URLs are lazily calculated and cached -- Detailed examples of re-homing in action -- Performance analysis - -**This is the most important technical concept in the system.** - -### 📦 [node-types.md](node-types.md) - Node Type Reference -**Complete reference for every navigation node type** - -Read this to understand: -- All 7 node types in detail: - - **Leaves**: FileNavigationLeaf, CrossLinkNavigationLeaf - - **Nodes**: FolderNavigation, VirtualFileNavigation - - **Roots**: DocumentationSetNavigation, TableOfContentsNavigation, SiteNavigation -- Constructor signatures -- URL calculation for each type -- Factory methods -- Model types (IDocumentationFile) - -### 🔨 [assembler-process.md](assembler-process.md) - Building Unified Sites -**How multiple repositories become one site** - -Read this to understand: -- The assembler build process step-by-step -- How `SiteNavigation` works -- Re-homing in practice during assembly -- Path prefix requirements -- Phantom nodes -- Nested re-homing -- Error handling - -## Suggested Reading Order - -**If you're new to the codebase:** -1. [navigation.md](navigation.md) - Get the overview -2. [visual-walkthrough.md](visual-walkthrough.md) - See it visually -3. [first-principles.md](first-principles.md) - Understand the why -4. [home-provider-architecture.md](home-provider-architecture.md) - Understand the how -5. [node-types.md](node-types.md) - Reference as needed - -**If you're debugging an issue:** -1. [node-types.md](node-types.md) - Find the node type -2. [home-provider-architecture.md](home-provider-architecture.md) - Understand URL calculation -3. [two-phase-loading.md](two-phase-loading.md) - Check which phase - -**If you're adding a feature:** -1. [first-principles.md](first-principles.md) - Ensure design consistency -2. [node-types.md](node-types.md) - See existing patterns -3. [two-phase-loading.md](two-phase-loading.md) - Determine which phase -4. [assembler-process.md](assembler-process.md) - Consider assembler impact - -**If you're optimizing performance:** -1. [home-provider-architecture.md](home-provider-architecture.md) - Understand caching -2. [first-principles.md](first-principles.md) - See performance characteristics -3. [two-phase-loading.md](two-phase-loading.md) - Find expensive operations - -## Key Concepts Summary - -### Two Build Modes - -1. **Isolated Build** - - Single repository - - URLs relative to `/` - - `DocumentationSetNavigation` is the root - - Fast iteration for doc teams - -2. **Assembler Build** - - Multiple repositories - - Custom URL prefixes - - `SiteNavigation` is the root - - Docsets/TOCs are re-homed - -### Two-Phase Loading - -1. **Phase 1: Configuration** (`Elastic.Documentation.Configuration`) - - Parse YAML files - - Resolve all relative paths to absolute paths from docset root - - Validate structure and file references - - Load nested `toc.yml` files - - Output: Fully resolved configuration - -2. **Phase 2: Navigation** (`Elastic.Documentation.Navigation`) - - Build tree from resolved configuration - - Establish parent-child relationships - - Set up home providers - - Calculate navigation indexes - - Output: Complete navigation tree - -### Home Provider Pattern - -The secret to O(1) re-homing: - -```csharp -// Provider defines URL context -public interface INavigationHomeProvider -{ - string PathPrefix { get; } - IRootNavigationItem<...> NavigationRoot { get; } -} - -// Accessor references provider -public interface INavigationHomeAccessor -{ - INavigationHomeProvider HomeProvider { get; set; } -} - -// Nodes calculate URLs from current provider -public string Url => - $"{_homeAccessor.HomeProvider.PathPrefix}/{_relativePath}/"; -``` - -**Re-homing:** -```csharp -// Change provider → all URLs update instantly! -node.HomeProvider = new NavigationHomeProvider("/new-prefix", newRoot); -``` - -### Node Types - -7 types organized by capabilities: - -**Leaves** (no children): -- `FileNavigationLeaf` - Markdown file -- `CrossLinkNavigationLeaf` - External link - -**Nodes** (have children): -- `FolderNavigation` - Directory -- `VirtualFileNavigation` - File with YAML-defined children - -**Roots** (can be re-homed): -- `DocumentationSetNavigation` - Docset root -- `TableOfContentsNavigation` - Nested TOC -- `SiteNavigation` - Assembled site root - -## Code Organization - -The library is organized into: - -### `Elastic.Documentation.Navigation/` -Root namespace - shared types: -- `IDocumentationFile.cs` - Base interface for documentation files -- `NavigationModels.cs` - Common model types (CrossLinkModel, SiteNavigationNoIndexFile) - -### `Elastic.Documentation.Navigation/Isolated/` -Isolated build navigation: -- `DocumentationSetNavigation.cs` - Docset root -- `TableOfContentsNavigation.cs` - Nested TOC -- `FolderNavigation.cs` - Folder nodes -- `FileNavigationLeaf.cs` - File leaves -- `VirtualFileNavigation.cs` - Virtual file nodes -- `CrossLinkNavigationLeaf.cs` - Crosslink leaves -- `DocumentationNavigationFactory.cs` - Factory for creating nodes -- `NavigationArguments.cs` - Constructor argument records -- `NavigationHomeProvider.cs` - Home provider implementation - -### `Elastic.Documentation.Navigation/Assembler/` -Assembler build navigation: -- `SiteNavigation.cs` - Unified site root - -### Supporting Files -- `README.md` - High-level overview (in src/) -- `url-building.md` - URL building rules (in src/) - -## Testing - -Tests are in `tests/Navigation.Tests/`: - -**Isolated build tests:** -- `Isolation/ConstructorTests.cs` - Basic navigation construction -- `Isolation/FileNavigationTests.cs` - File leaf behavior -- `Isolation/FolderIndexFileRefTests.cs` - Folder navigation -- `Isolation/PhysicalDocsetTests.cs` - Real docset loading - -**Assembler build tests:** -- `Assembler/SiteNavigationTests.cs` - Site assembly -- `Assembler/SiteDocumentationSetsTests.cs` - Multiple docsets -- `Assembler/ComplexSiteNavigationTests.cs` - Complex scenarios - -**Test pattern:** -```csharp -[Fact] -public void FeatureUnderTest_Scenario_ExpectedBehavior() -{ - // Arrange: Create mock file system and configuration - var fileSystem = new MockFileSystem(); - var config = CreateConfig(...); - - // Act: Build navigation - var nav = new DocumentationSetNavigation(...); - - // Assert: Verify behavior - Assert.Equal("/expected/url/", nav.Index.Url); -} -``` - -## Common Tasks - -### Adding a New Node Type - -1. Create class in `Isolated/` namespace -2. Implement appropriate interface (`ILeafNavigationItem` or `INodeNavigationItem`) -3. Add factory method if needed -4. Update `ConvertToNavigationItem` in `DocumentationSetNavigation` -5. Add tests in `Isolation/` -6. Update [node-types.md](node-types.md) - -### Changing URL Calculation - -1. Review [first-principles.md](first-principles.md) - ensure consistency -2. Update `FileNavigationLeaf.Url` property -3. Consider cache invalidation -4. Update tests -5. Update [home-provider-architecture.md](home-provider-architecture.md) - -### Modifying Configuration - -1. Update classes in `Elastic.Documentation.Configuration` -2. Update `LoadAndResolve` methods -3. Update Phase 2 consumption in navigation classes -4. Update tests for both phases -5. Update [two-phase-loading.md](two-phase-loading.md) - -### Debugging Re-homing Issues - -1. Check `HomeProvider` assignments in [assembler-process.md](assembler-process.md) -2. Verify `PathPrefix` values -3. Check `NavigationRoot` points to correct root -4. Look for cache issues (HomeProvider ID changed?) -5. Review [home-provider-architecture.md](home-provider-architecture.md) - -## Related Documentation - -- `Elastic.Documentation.Configuration` - Phase 1 (configuration resolution) -- `Elastic.Documentation.Links` - Cross-link resolution -- `Elastic.Markdown` - Markdown processing - -## Source Reference - -For the actual implementation, see: -- Library: `src/Elastic.Documentation.Navigation/` -- Tests: `tests/Navigation.Tests/` -- Configuration: `src/Elastic.Documentation.Configuration/` - -## Contributing - -When making changes: - -1. **Maintain invariants** from [first-principles.md](first-principles.md) -2. **Keep phases separate** - don't mix configuration and navigation -3. **Preserve O(1) re-homing** - don't add tree traversals -4. **Add tests** for both isolated and assembler scenarios -5. **Update documentation** in this directory -6. **Run all 111+ tests** - they should all pass - -## Questions? - -- **"How do URLs get calculated?"** → [home-provider-architecture.md](home-provider-architecture.md) -- **"Why two phases?"** → [two-phase-loading.md](two-phase-loading.md) -- **"What is re-homing?"** → [navigation.md](navigation.md) then [home-provider-architecture.md](home-provider-architecture.md) -- **"Which node type do I need?"** → [node-types.md](node-types.md) -- **"How does the assembler work?"** → [assembler-process.md](assembler-process.md) -- **"What are the design principles?"** → [first-principles.md](first-principles.md) - ---- - -**Welcome to Elastic.Documentation.Navigation!** - -The library that makes it possible to build documentation in isolation and efficiently assemble it into unified sites with custom URL structures - no rebuilding required. 🚀 diff --git a/docs/development/navigation/assembler-process.md b/docs/development/navigation/assembler-process.md index bae1b9c16..0a36571b6 100644 --- a/docs/development/navigation/assembler-process.md +++ b/docs/development/navigation/assembler-process.md @@ -2,7 +2,7 @@ The assembler combines multiple documentation repositories into a unified site with custom URL prefixes. -> **Prerequisites:** Read [Functional Principles #4](functional-principles.md#4-navigation-roots-can-be-re-homed) and [Home Provider Architecture](home-provider-architecture.md) first to understand re-homing. +> **Prerequisites:** Read [Functional Principles #4](functional-principles.md#4.-navigation-roots-can-be-re-homed) and [Home Provider Architecture](home-provider-architecture.md) first to understand re-homing. ## The Challenge diff --git a/docs/development/navigation/home-provider-architecture.md b/docs/development/navigation/home-provider-architecture.md index f94eae025..6c7639bd8 100644 --- a/docs/development/navigation/home-provider-architecture.md +++ b/docs/development/navigation/home-provider-architecture.md @@ -2,7 +2,7 @@ The Home Provider pattern enables O(1) re-homing of navigation subtrees through indirection. -> **Overview:** For high-level concepts, see [Functional Principles #3-5](functional-principles.md#3-url-building-is-dynamic-and-cheap). This document explains the implementation. +> **Overview:** For high-level concepts, see [Functional Principles #3-5](functional-principles.md#3.-url-building-is-dynamic-and-cheap). This document explains the implementation. ## The Problem diff --git a/docs/development/navigation/images/assembler-build.png b/docs/development/navigation/images/assembler-build.png deleted file mode 100644 index d482870d899d2dd397cd349cdff1d2cde21ef2db..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 112941 zcmbrmWmFu0@a~BO2`&M`pn>2P+}(n^ySoQ>A3OvL9w0ajKDZ8{fA2Ya z?!MS_?!G~0rs?kL>iSlFo~MaYR+L6Z0ieLZz@W=~l~9F&fjx(Tf&Yw*2z{l*kgy*H zCJ07GLR8($@F*Ken^+nQzFiHxT|LZdZyj}M9~D(>JtUsJ;frjTQyEp^wMtfb7yJ%6 z3`Y#^y|NT9s$^I{T%ub9q3g%>r|ayz$Q;CX(U$)%g_JV)%Rnawq~0BTc`X6bA>_y* zy~-Q^9BPx4U~kA@GT^xwUm^P)uBWe^QR6u)%Kbf1T{md&xGNv__oc zq0+opyy-7zfNqZloST?_woA$U3o)}PjHtM{LeWsfiNmT+MKb>@Y;SLG6%7r%xj*hg z0Z(48uCAtO;NoJM_J@P=ZzG&@3-sv}9w|oc8 z#d2~vrBzpdc^EQ!`FnC6@cb~*`TFeCvKh@#Vl|d?BqYs7v^kw1{KA8JdbK~bPxg9; zWZ-+;a5Zn1FhS(hCQ8KZ_|GjoENndY^~tK`wBvCvNl7TLIL9Y?jML*tThDZHvQTbe zv$soR>~<}1>=*^LGBjal$I|DxVQ%k!H`j^s?!T*Ru&)fkhIiU?Q_-8?avYRxls^(x z*Rd6lB=WgEQP47Q$CH$lTp0B8^J^jwL1FbjY2MZTo;Zy7y)@tt0)cade!;I_L8RXQ zu<@UU<;(dyL7@1K=NmonA|(YylYLJ#-nSv9#_{P=H3x_C^78#Cnw($I<5^3E%}q)Y z_grx&xIUN}^2mBi`EQ|OWSwL;h!e_j#EVn-v+4CoN;tjRIs;qc517|>sea%C|q4!Hnzwo zb#!z*7IuDWdThmUPyUukvbW&(JTYfjU0Ykzq<*`nR`<9KLsr< z5D8`p>`<2?dNo0V-1c@>JSZg5UTW*)RHJG2c=lUd>-4p;O`#>@^bmuMy@`T<8yljp z_hW%Pmd|?y3AH`TIs68=F8&^TrYqHZi7=5o{A}|z8<%HzKMl(3^8~Zc4=d zWq>C4bKOG4|9v-cL-*n~jA*;($*Z%neGF_ILFo+JjaM_X)AaHc`Sl4zNJ!|hjY}>6 z>-DkowP2=P>oP!^BEESGfMpArjYK0fWq@yzGBu@0Nd@=rV#CjkGx~q7v0?TgMrC*@ zuj;5Jp?Ql_yhtR4BD%Y5*4nO(xkH%V(t}YcxZx~Ck_Gf1e?dQZ3Jcc;<2+~Q;@U=s zA>-~mQegN$#|VmP3JoP_S$A-GAZY1o>C;=gl5u;x<=1JSEG39FZxbn{?|QxfeyT=5 zct=+LTUp{EPEwG!nzi94E}{S5-}!*;0{vaoSy|%0H~xrXV-rAQ@D54xfByinE~L)w zqO_%@SNo|vh`XcE!d&gBW=ma5%SkIO)99;)MrC`V z33#w+TH3tY5oi_i@PIftIJz~rJv%+E>ftEF$v3a}{1$WcNGuoNdXa-~Y!8Ru@;5#J zo)zPZhBi)hG-6zq{M3pcGP5yd1+LQIq4 zj!W9>Rp0&}NoEy%4wG!Pd*wzEXXKmK>H^Pf#%LPqnuM6R(7lb4cSZx}W@i_eoby#m z1Fp*Lj+pGUnT{)r35pXs z5Ph{hJlN}dDWE;~Dl^EIQgNZvD+WCHj(4?Z)9ZOgOnCZBoq4ArROWpuC|)ERuk~nI zm`hW2?966dSg3R_Q>fcWKeS?}AKA9#j}f9`N)mb~<4#E*{y>T?ZuHFYbC7}F zoe6rhVeBKkcrFRva@MQ)Fc}=(p;Ga=qBZAz&26Kc>vJ0EYrGegrJLc!NPk z>#_iW!E~YfB4GuWY&lXPIr~i*Ilt9>5$02Na{-dL_(p?r%2|hH5UAm+&hVx6&nb0$ z5p?aux^{x8Mf4@>BXe#5pCvt2;cvH<4smP_pC9E1TZ|c7Tg8Y(voFUBWQtowO;gfF zH$^U^j`mRmjoO>eRRvB>b75O_ix$>n9o@@Lwwezmh_E>wFZHkEDtgZyq1abRfDe01 zSwM%@n+eQksTv^CyDE*_nFr>(1BWHr$7^od)620^=QDEI_D9D|pcY6^Zo#QTCY_~C zq-U#FRc+{^>X*lBz~J*(Anj$k72R(8ZHZ9tflNt4bpG;~P~q@yZt*aI(QnsRDt!#> z*SpLP#!cz?-bwCGG`zQz<{8RT!#?G+_)zCf{vQP3LQdH|dKgb=`Gz`Skb()Wa;^P&J zVH@kc2oF*Ub(OW{;idT)OoU7i;ZNpd(2>E%%hHG3r@vg(R*u`?h`>kryK8r06clC- z7Bq1FvSalq>^VxBL(d%P^2z7J?6SXeX_C@tES|Q=l6}_v&FN@=J>2ogJ|7yDuBYV= z9BmaaUh$WhUwn478W_#L>LV+zm@_Pl?t-Du6pFLG)$JzqHu6(U9}^O9K*Cg>dAL?@ zo^9dcf57|{8Hq6V?&SKWjR{&({-$qbMc;KLCM}>KRM-1rk`4(!k?ZKR1;CbDpM+Qp zmXY?&Hc9=it&U7^Xq}wLqS{2l&|}iBkMt!09pi2kFd93#qsOsgsaXub0q;EhBN#;M z69}vH8`C@POt5YTq|xD|lw3D_CzdyzTW^Wja$GMf1}vnqrn@XUYyVw~EW5luM=1W< zRDJx&CqXzSS$Ct9(o%q(?7Ew6p0hjh*h49;JZN;1-zkI{k0r_*1Piik}Y!)A(d=u4WKXFHX@Bj1ck3!lHq zw(_avM2nV_Eqy(UQdd^DFGrF~tun2V-tm_Aqv$E^OkZf`Dkmen@J|8S;kPHUCRsLM zyO<|;`<$|WYcptjl2`RTBaiX27)M!U$z(M+ehmkpqLvO@P#3PcB|O-ZczwXwQ%YA7 zRv5W?njr# z6E6*SZh$HUqUZgC!jjF?^a6Mb%X2n{XL;JWn8;w8ag&L8mpSI{uL!<5j9kmyuNwRP z(0B>I(-&s|$KV2J*tBL|$dh`r8iLt~W+Nb+QbOPuhPbtp$+sM6~TRM)NEWeZ?RWoCwEfMh9B}F+cJ~#S_PE zj77v7<_H!Yv=qR)?8VuM%2SZqUDw;v5k{@Ti9?TM4+S<650SDxA zlqs=feW_f7Q)Ir$WsUYbWq$+wX)hP7=pK(fX^(@E<|UO(qo7mb6FszwMRK;@itzN+ zuy*PR&!9w^_kU1Q$LIXf9m~R4sw_r21UmX$=9JDBZqD3PD5~JVbV^j|P6@OV2Nswn zsLTgbdVr1)Eet55(Er-L3qeij+Bx>6(?=&B9MWU2av`&^B@dC{5(HL~#l@T5`hL@g zlkA0*zi6KS>alkmc_)2AJb3l!ykd9j!6#B-=W(3BNV#FIg74`d5k_CPgTC&hp#4#A zJ5}Hj7CKhDt0by7`*5~yXX;s^Y9~u3#NH16A}?DYuw-~^Ge!k>)gk9%I7|`#U=d5M z3VrRye$6>9^ER@0pb7Sz{=j|KI~{;raO#x*dMJc=-5>DbW5w)!h8Eg+&91?SEzr zSnP9r%aIJp&c~Hg>z(0rkL|~s;{xI5eWiNiKX9!VYI(nZ#{!7-(u_DmsI(8!hSE{u zQ9FiysH$adyVOAJEw=Yr*cp&R&dPya1LsnFUy3s{U(JiwtK{q5k&FS>L^S5eK zVniA%6oF3h4Hb3wXuC=>J{*P2#$#4|b{>}57-2PH&AE(5XKx~GVE~B|tmxsfkIK^0 zf&un6+6}L=n&>h+Tbqk-EVA;pdY)zLt&EKHtoUj+=~2`Qx4+8{0A?xB3LxX6C@J7) zh~7*;;o)2I)AH83RdgQy#Vr3|6NKvR=O=&;-~bwWZ{ztur$OtZ`I_XkG^_Hpc3*D) zivgN%cV{Mx)h6)muP={R7eg#l{fU%x+}v{>`r6tb{h!YJp0o{7_2x~Yevo${O9mKYp-gR5(_U$q$5Vt z`8D4R*I|2rj&QJSMULN-?6(dns!dX@bAz*JNsH0)QrE_)9~pGXRTr)_#c){*{1PIt z%S6SpW|5qcW5Z9wc$@8uadGLKU8hXqnS3U3a7aY!H6xX=)gBfRpVR9PabsYqs&1$R zm71V>_JN+kY=Zpi!tJ|AA7j!xf-_DGw_l?b69Fl zc>K51-0v9okX$dX3whS-4rQlAN5{|L7)hFB*BygSj?VO1?ARS**vfJU8HlXB|H#?R+hmzM=^Gyco-Y2cJNrl5pV z`!&Ac^VQVbtUn&Z(qS!Ut_QX!eBQteT<-*6V`KXXL57r4^r<4MrA++yWz=Wa!|2@R z8(Ng!F9w+;(S3an$K%S8nw8Rn{0EKSIV>VB9Xf?b>PL3srof2&n@mO~=J_B)S4yZ) z;CaN;7{i|mfvbUbsAVT14Qb_wu(-uO)yP{})@d1*3jg@MczzTOXUVyo_u1U8igV~+ z7dFiwq-eK0GQ>G&8`bbDZ6|Nd=RTWtf6g4|Y|AZ%pUiZgbu zX1T;@hb(-up>2v1^RM59oi}26e!lM@CD_hE)RbtO%|FOsqfV$uH7M3nf z^`d4GY_MkzYuXawANCNu_{;q$^}n&OXaesKr`zS-&n;mQKOU_=c8k~_-h2Uoa>TtQ zspeABv{avJ%wJY(YxR(#0ekE@F@|^0VGnv2Q*R;(lnqGgEJ{HckRvAk)F$^(Nl}H{ zkmpDS=T$eRu(G;(a;5!f7VkGG^ig(p{&LD<(2~sDex=%SyXvcRC2C28|kApqh8G{7_-*(!WuH4{*CdI zB)a|Z5$s2H6MVh)GckGf-M(KiU0YvV%8CtuCN}GU#aUMc%5cd-`|vG*6uA5p_I82v zBO|$o!#l(9t6^e%TLC>}L019*0AM`~(K%v*J;O1s&g*2pH4x-)+K-zqzcHKXVy2^7 z(WZhXy(%>6jV@!-x7MU49P0#C<=se`it^u;_e^QX%E{5u&_q5xKkLM{x3@PFQ()AP zayw?NoXg0_{Kw4rPusd588j=bDK~|muW(n~#ss;_U(~hRzTs8sHuiujhRrPxiG{CV z@?o$eCDA6FHBDh*5u61RJaa}gDc(VB@`=diNP#tf!!hLiPo7}(1n}{XHZ98MpDP>! zY{~GsE6ix9z9NM8MATu&+$N{5`WYD4Xj@m{957l~aMMqao(lBsc#I7W?3!J>7$R@2 z`p*U@^~SFtC9gM+n?ftzIst?Tk@rkJIXU(Gmng1-7sYy61Rlm2V^Oou)cL(nVJDrh z0Unh`{?{}Awzj(aL?E9=gM)80wIwAH1Yhp9u7=sis(BC)5Ei0%b|PPoodOFy*Zdp1 zV=qAKyiSCuX*p+#%KBShgQnyij zt`}E=J6Rbi(5UBjSgAxLV{o|s_+%^sf9vB*9*c#WW^Gqh<1YQt_sgQ$(9M^OPM&^k z`IslI$+CvZP?n~>M0OagldfMPkDkHlNT51`c>~YqUXl;vu5F11zL+&#xr#aA6TI-svPWLq?gAOhQ@?|YvJtwwwM9Zq`Vv`G{f%kUG)r#lZc?T;sDGtux&|oIL zBZCbZR7SzqF3w(4zF+T5A~IR@ybi@0r#uX#IQ7Sw4mLt(F5xD&DoBqGE?=a!w=6P=|0)iAEFcshYa}x{WVS`A~ z=)a>2t+4>NE@1u=l}U^npu;#Tn7fGI!LUInDJnN3>>?{xC7`s)#S&7oF(V@N_@iqE z{2e|h4Q{{{=yVvo{?}rSG%wS0GKA`QZiAQ73HG^<9HkDn-b|ug+cV=sPVki;SuZD&O_NfMftcW@^S36h{TKZ?(KdENX$ zG`r;LGI$&dd`UWK+QQInu*K6G1xA|5el#4-CvZ7>-DU|i86t5q4xNc>UVD=v99C=< z_Sm9%Djwf-BsTUXOZHr}YKU>lOoMj5kK*TAnlkw6!KH$N3(&aA(&&RLy~y`-k{kuV z^!=xs;}JD!xKD`6Ql6dITkTtfpPDg4i;@s{QGPzmtu8|eXPDYXRus<{D^As}KI$B) zh$ikI^Be?xjAy(Fcxp${;Dt7l8oN1W{Np!R?AUfL<+Bk^U_F>jm({rquJ?)evrPvgUiuyu3Uaiw z8u&xN0?^4cpdN55n;NOU2H@)SS^8>AU31p&w?msdA17-8|4*a@_Ya`+jqsixM=;-=FXAZS&5NJmGv;nD9p z*5ChY$*D6N5tG#3GnRbaf6sZvlY<9}c6rjW+_2|LI5|~R^21R~nPxwO>#j0OdLY;i)^8Yi&+*BYA^ebi0)31wDwT8|A?F55qbCKdhbxuwFZ)`X3@LKKi<3Zz%2ofH#P$|V7 zu7}22bae}DTq1_F8F1YspoR*gHC=HdBZumXnp(n#5~SyHRsR>m_qwjOEe0f` z;SZZXRLd$Ij>%^CxJ()O(-HW&9F`P>uZTt{P6(=hL@?ydn3ZacEll;_ryTs8NYZZ>;^hoI^q{K& z@CH!FsXV)7w)Rt>@pogm<_bU5a>u!Q6zn?+`H_DAkt&mvf+zHCSY1|Lw!y$^HwXS- zyD?#)qrLra9YfT8Wv}GQ0MGeP51?CTzMN!B27YY4X~8Ff@hHiKz~Cpk9nU@&|J?`gUTIH(kYizSg|V*2+p`f zjnUEWJY^4Y)iv$s!9+yGns0j&8|)t}bhYrNoL%uf5mp@Wmv50R=#4bKGe|X(JeY!@GUlY zcpAjzh)n^8qKTEve(r)K=#|A4A5jrvOMbhIrKvbru=hAt6$js@e82VF{s69s?-kIH z4G5)wp8&4?o!%1=k8`nq`t#lk9kO&z5>#=t%$0-t77jCLI36)`zdmA0o>^(vwj=Vo zxB`GmUyJVLLgpu5Bz00ypF$mu<0T-p4D+j!Xyox5(q?z;?+Ec(W)p<44KLFs{W|f$ z&l9)?*1N0lPlOaN**NYBSe%*_XoWe#7)8yfaQ#zrxl*y2qT<;_i-@>0vi*;=i7d+S zp*J4|CsHs-Y#QoCMOA8QbNbHMCnQwhb0T6ZvX~>06(tr`P!STUDAtVV`!#enDRUVM z^JAr=*EE#GNtyG2*R#6~-@Z~Q_;*WiZdZno{8N`?U#%l}lf5>kUA(XpfU(!4`+d~ z@w*T%vswZPVzm|#ElJ{39{q)1d3nK)SiVO*r=FiTeQ!vL{uC6?r+i9REMUiuqzd?L zaR!HlB;D(Cb`V$ZrGwn;%~adk=#NBMvNrzS0cTqgUwcE`H+d3~+wMu|Yt&t}v>YwV zdx}+&P%`>FW2)62LIuVvH=k5BaRYX}t(_<83o?C~sZ}mD6I;sGd45!lx5a{{H&2o{ zpWhks`Qpkz7Jhmy=$E^GPDbiLjSVDYWtUkK|3N#Wk#>9*!(~4a*V5T^A%selm>=nR zhL`+|TRcmdMnvOaw&LCC-;@9s(qXSN6l|<);SR!&-6?si>(S82ALz`FHIF& z0sxtan`_o}X14(z8;7_p%8_Z6UA1$e5SC?{<k3s#o)VaoYToZt&dYLq2iaJPx;l*JvOC86AE?%~`YUl!fz*U{Z9ucZa=L4YY^ z+3?>!t6p7N;pPdh8Tq$Xf)iQ>60=F&Q;kM{)1 zySk$`#Nvki#)Q$KtLhuYJ=2r=-!8Fd zsEkStw9FsrX4t!n;H#a^@?tuxlX+v1L|m~rxVXoI1_lQEW#yoJsC}#TBB}8Av;@Bg z8F@^h`^rf?Egcp-nNd4CP*Bg)J8WyMb?+0B zl^ytkpUXBg!`a6CRH|&rwp58!g;ajCy|du!sTc`GB#Rw(oi#sAH*vg=eW3m;ZfF=4 zWce|MLDUH}(F5136dBh+?r!Z$&+VN*Lpr&V=<3`O`P2OFHX^SexJt_2-pw^-LmV6& z-^c!Lf4lpdTvKL1UbMjL&EYiRs$1h4TJunUrh_Fyn7+_!in!TM@i`evYvVLl;x+6V zl~aABtU>_posOiaGyZcQ%lQhMo73B7*jK=Axi!(9w}g%z-#F-3wcQ7e=~kem5)ebj zEEFZ+pPF+aB7qg=7AhWd7tA}uIDdjkFm*)gz&+<1W3wPlUB-O!fT;rI>`)(3Opfmf zug`Xhmi2UrN^(+?S@{~+f#?n763-bnyHd5E_Yz$l)s0P#=ZVwtw`vTpbM?7A-ZZ>? zOiM|bfVz2J|TCp5Ud$OU|2Evj{KtRA%r0`R4NJxnNuCUh``SW=~;AbTzCB21#5VAR$ zDS`V)9n5$38$XOME)1lgFukTnQ*-tKKK9|gnq^)8ew3CN`_fPX{vEp-mA8h5hB8B; z;x-jH!6}1(XW&IqbsK{9EyuLx&`edt$oSsC48GKB*UYjxNX3$w{*^=a+aoT?1U z_Z^PU&~{i1^A;J4?)@-hbdv36?o%Vfeq0VW|Kpg56aUzhvr5soUU=K(vaCh<5!q`* zPYOy-iONelI)aPJPiYi)H+#-3r}lN_)R zwVJD1A~zJL=qT$d4@qAUsmIPXMS)Py8$l%Axqn^Re7NHWhpn9(W7hk24XK8r(XFj= zt*W99|3_6Zv5-ppo1=v{>^okjQSKFZzS;91d=JGG`pU}6svi-184Qn9+eEp8p2}<> zOJL;33UwvUk8U3x%B2|v@P=4P=$5|%Ys*>LmTLuoZ?VW&X#tY0EempM5ISi*3fhc)F)Z*D456Y(W2!6Zb02gUhE>tjrMvi_Mbk=Bm zB?na3#)+suCDYzBBLC)Hm9w7qBA{}e^%7jJ$CA@taJ~e*N7Eg{_x2UZ`UK}ZcQ9nk z1oi~onGG;DRYx~Cl;PBm=-oO$Lu^QoUK%DVBs@qZ`?Qhts{hd_-I`C#;*wjZ0+8ro9Owm)vyBSXNs}uUt)5-eY-|ZK#Xop z3rp~!SjeTT2S`{}N@RhHb|0@`km)O&!#IxyXP!?M-M$L~fCb&+@R+b)Wrmc;AVWKY zV}pZgV*%i7AmtGZO9-nv%<#b)j6|n4K`L}AdE9_TIKQ~mjsnC58AiqBBXIegsgNU( zzOq?ef7*U)*s>MRzjshxWy#GK1QgE-4b@}fHs;DKl};dC#utu;g@yH?z_5FskR~fk z5PB$y<8){1GQdF9!K|n#WnMk9jf?9}{o0pYoi1NF;O6X9wNh;`7p?t)n(kb@?Zd)} z?`>qAoI$Pw1~+SkEU?vb#-?f5aNGpZEg%3p26=N3TpMDFe?I6@JCpdNsjQt3(z0Eu z#w`E1aIHA#!&FuA&&)*B)LBK_ngs3B7O9w}`PA%+M^4CRPTD`~Qcn9~@}I8yCufmW z^U;Th$FRn5Zht15#}#kgQ>f=9upE}EuCbrg{H`C??pa@x^3C^VWYxLu&TJ=*>Jon{_25O+rXVL>hC8H1CzP@#j z`YfBynRcnoQUL)%Zz9GOG~O4O6%#lK@$oxekI8guM~m5vcP6XtbB6Mt{#L7Bzn68E zJQN+C<4HkI--=1qeME{u7rp?(+CAcD|Lg`x>ubZ~uavkjgL#^;n_71F^v_}ba>?p= zdf#h$3PLIbMY%)X)pUqcD~rZt26m^e9BGV8p!+Y?J5+VNud~RUamOU@>i^B{gR$-| za%7lGi{v69pa-ut5k|&*>M~mAKzRG{j2ta5_@8i7$&y>tR{GzS<5uhN6>^x{jXVh- zqnITlOfhtUe&x9t_?k#PA=UxictZ(Jbn!-OI$%M=8Gwmz38l5 z(RVbtex|qLv0e{&TKa$BVU1&>kPs1FFNkVqm5@H0R-!jFmV8>pqHWUOaEzRZxT_|G z^livSJaW`h&Bpv>p($cAka-EV(;ogVg7}>W={)Fgb;y&B#$}(q%;oe9r3(Rry91v^ z$nSFm{zGK76%0kf7}T9r{MTldQhyDq^f%|~>OV?oKb{QAcvM$dHH}!6lNytMQMmrJ z(bQf}Xsmesj;e`N=w%t-#lE1)6y{ytoasI}KX)#*;`#c|m&t9r`_+a^aevk7ED-Z&F|C2PQJR>{?Uvxa%6y{lmub|)$$16G7c3JN zS$=k6D6TZ*sQ~KPX4=f$T8;{Fqcnn>1=Zzql#-QKC&0q-QpQy6PZvW_v%M5=h4bfp z!K-ocZ_xH}F^UKB0BSwSf3xL}X7k@4+X3C*)RTJOTSuW$+Rf+3+g-OU_ln}~#C~nC zSRAtsGE5k?07XWzJR$Dt^2!= zRKRrEuJ0Vh@3D$FO%}DKEaS_DG;-M&G^&CF3IZcFge#|p_&n#tet}PgM4G+BpKx&6 zo7Ovw7d*d_LOK(qdd~eDcj2V-Nk*OCscoIo1o_iyPh>UQD|%s7BA+H zzo9bq{PNzOQUs2T0H&Yt&(zeMLGNFn&mHeD$qwtqPnA@w47UIorW6U)jZEm zleZ}QliSMW01h9x=-6*5J_v>qgr3*|?YI{>FgA?Y7mE761Yr7M=EjWXM0R+07IPUt`fa4UTfDSmUs?F_vt(eu%k$kFr@*is%&>Vq zqSw7pB=ibf)~d+wOU4xL9X}siCf}te}9dw@pQ*sa2agj}HJ0 zf>hhC5YFAqYY&5cNsqsurT+LK*0}77+Q}ou!;6L6tG~y7Kg&IA7RlhZaMAE&S$m1@ z1?zDvXwHcLZbJaFA*o*gYDF*ywwcD+sCJ+Mix3BV<{41pC>`g+0IccoEtpx+SCMj zd7D=t{~>oCjsG7b?|B_Eeon~&HG`;0Uff-nIiD*>(TJ>}4dH@;F3y$upAa%_b9m#1 zx}&Tqy?0V(TSow0wx%Bd+hYRqiK+P@N#WSSN{(qxH7H*zjL@SMoD@rKi63(>>gS#P zN@|okRU=6As=MwcDx-<}hFbHvbK7q(&1zD!mi=C0JHsU_%S+yejJr!f#$fi-7wQ9& zp7~~s*MS2cJdc`c?zH|)dD5Y~Wk|XCJsr)CA|_dRsjA!W%7x_6XM&H50>o*~tU7~I zI2@x}H-KuCUVeDqR~5fnGH$}Tg-a86Zp(fRPTm}f>KNjVyi{aSx0&bHjHRg9jK2Ty z`uVFCe5gBnS!w?7<9oJzMh+;BiC&!c7&$)?QPHIoPUvP(GJ>#np=4_)ypMp58H4(& zf4{5*53slQd)7Q(R6rkALC1C>S*%UeF2fxX0MIe9FEDMrucluBwIjW$31td`PK$%d z&yh<1EGTMXt+WWv#vg*V%FC8?&nvGv9V)D}=L@Op37&aMT;vD0HrJEpD{+Qt|Lpe7nSW@D9QaG~6STD4=PXpW&Xy()du`vBv>l z>}c!#$H2c~CLdg0r@i#^GShx+>1#j476qVXp|$o4q~hSn)Wu0Jmthm5g&ybCR&Rh^ zRcXIt23Qip*UO&OlFAOK3S>eLC<${-;I5$&IfRcduX5ZuoYrVO=9z8pyT2OC7uU^! zuP&*oT5XB0K-Wjzg>SNYX8*^gR##~V?*NzugS*3c()GFp@ySIJN9&4ZbyfQ6Yrd$|d`#TzMJi zNxT)z>DEC(mWWDMC;G$q!q;91Y0+sO{HG+CU%Ax0h9+yG zOwj+mMCs_baB^JwvwH_IIZV-D#Px63DXc!|hJi9HrTSPHLpUF8n3&(Q1e5WOnc1b52l6lczb0ZZyzS%da>0BkJl0+QSa~xBM zo_5)nhj)q|47kK(Jj8 zns-$+gw@+Rj~bYUYA%DrgLwxB0M~SOBb~NC%ZP{ZlsiDtceBwEG@^AoCN61oqGUX# zKXCS~^~lC8D*)B@3pHtQ8l6_O*wXR_X2%f@R|mdEs`dop2wqZ9m)R5H`FXzSVJ&FA zf6*s;L05^lkMYBo{2#r7M!op9S35ocFQfPOFY2O5p$FE(YK6qFvy3w(an~HXZtO9x zzW7WMmJ8LIRqqiIx>LS7Wf+Lp@B`+vNqEWzEu&6!CN&`@-jPg|Ti|q7{0mk1_qR$z z&FF|YJOKs1=y*k(7wg^0Ys0CQS$wZ3#j=2Vb2?PQ<(-W13>?P|AvE8Lvgzn!YL9%urxF}Q=QRQxJK|+ECQk!)z1$@v!(r@bf zba01WS9aeDVXxsJzj4ZY9-8sI{YH0>Uh?nIqDKb_?aouH0Ns! zKBlyG588X1PFj_*VKB7`l){-E{xOI;QhJu2+6+nodmv#-PwqvrJ3IoMi>0OdE_b0O zU;vi~V1_&EE|QS-uxCij{*kh8?Dxte#-$bXVFTG|PSI&?{QA_6Cz~(cC9vK0PQs+h zvBbjNT*h7PSEK^7iUQZ`0BH(6CI@A0bur-^vL1NCf+f=3ZWy?{&?%~WV|XSl(}U;s znIFX0jjHqmj`{Nr*NkXbZq??>RC+-Tc$FdOLpb>sAI_sUsV!hLd2cF$?>NG&+U4SE z5mO{za?+{WZEKrjmvCLQ>zX=VzsrI_CK`Y9!!&f0S0%QL7|rB}KwglwYhpCe_931X z;nPg?ur;dQKK7w*E*TOzqGn===!x){)$ zttq}+Yp$jT*{=CZ5B(-oaZ>$DS`JtFQLP8GnBkfYOsJ3MVH>_a(pQ#4_0gK|`KzT^ zVY(4U-rYmY)AD0w9Lv1AZ;gJ_Im*sBh*ka?77m5M)g%|?#3^N9ky<>8ReuI5N+l)X z(6?{530F%(K9wM{QB+dIyV(+}qzZsAgXco+@;Xtc8whS196N;5)aKc!Hs5Pvud%Mt ze+PD(kvV#-KN}!m#$UOdR%oLd?HU7Tq9f*D95y|{HwY&l?Lf*^p@E^2 zv3rhD>Lt4^CS=nj%IW+AHbGcjFt7=u>33SCc6V-)>f}NtdO5rKh?J|x`KBu}rRfYX z+>V$RFm!%eMD5FJg6-tO>!|_RrIpi-A?&VvHtSSL{+T@ZND_4i`eJ8lkfhjPYpcgz zzz0pU%|zD9b04|T@JqOLE~7SkDyWKAj|zW;!7tHSBB~&(8v%vjL5CiDl$EKUB?vW5 zOv-T1kFPoI{@|dbD;G+XCYntet|ixH;{VkwV%+I@W|$90o1kN9DK-I z{r3P)k#Afi?ttOTHs|>?o5DK9>!(Gd_`q}=xthc1IjJO^0@P@_|NJ* zBN(;bFL(1it(@5B)qrF)Z^!`~_coUc8VM1IsJ%2t(r@esA4r^7u(*elQ~(rhaOj*u zROw6E#4JF7HU77rT6erWgt|??7oKF9Kw?=sswp6v1d$sVftK}7^)=6C)33v3q;Em=wJur0NIsTFR+NQND$y}dnw|DU7vr)-G z+0Ftfk7#4!rmg_!e|<=JxcM5to~~AdwhJ!5ef-ng53~jDF<8OQ<(E5dyDqoQ~PzV<_Y>RJCa$>(c#e4ra1efbB|1oYWD?5qNj_ok~ZsCrBD&ZPEx+%Fdm-&_KK79lv- zM@_wo#p$9DR$y=%>~3)R@(!O9SP_?D#PQJyA-TkLSxZ!R>0#@!LMG@;Vy{$V3@QES zycIwzEf{ECXt%ylD5*UUh-?(aMEvlRDd?jRqSXI9E92hf59+BN!!NZ-KYAeLuw`pq zZ$Jm#QYO0m+ip!xtwQ1CdK|%ShCR7b#XkeVO$=%LwdVf6czesBxVo@w6NdzMZKO%C zAc5fSkYGU)2yO}P?hcJZg1fr}cXxO9hT!hb9G>~={WUf9)y&lVJ5@l_eR`k0?|rYe zu5I+dPb2e2pmHo4){~}P7l8`3zAc&_GgK4kAB02dBTD(Y3JP+D)!Ze0u0pt)H#l-0 z>wa#krhor7HL=TG+v#=C4wit%crw@NVpg6;|BOJW`vl7m&$m5yKFz8&0&E_<{$Ff< z|94}}!0yxlgN$1kKrAqW_?^dJjop+f$PkE=u=-Ds1w%nb?A<@SZ{Ot{!dI*~O856o z&(F>>@jrY&P1=y8kxLR+)wDr;PeVge{JPPG4>q z6(=}e3+kPEF^A;8d|RTJzUF(s+oqaq_9eRFM^H{)-X!1;EXd0XoSd9A2K3RGm>9!i z1$p_r*bk~oU%z5WNJ$;2*ZOYGm1)Toe=p5SNFdU15fOo{YdI)u+FdY8&jkYPO6KMz zS_1o?&v!-|iLdguClz?rx1lxK_3*PI9C=^o$|{oH%i1{i;&(MY^Ht`}-tcVTZ702Y zvs)jIvr(Dry@9M#m&2{wl11G_tg7}vpQhK-6}#y)T~TrIrfXo=!JxaW~Zc( zE!6;wj*-r?y4C-_5L6)7)f(2X@532VYxCeWNYeJx4E*mq7m>n5ARyOF0dWXfyPeUR z4=<-(n0wbtRz^0n#>U1O20)K&1yLI zc`FFs`ICEGDn=x~Qebvy!B8gt3YOUCVpMR%(T!;tWy{rf0w=YD`B$~IalfdT+h$~8ArX-Fy5PzcRaCmc5eS?f zJpK=WdTVs!YG=1Y4i6ZhI#{u>tu|7{h_x!aqqiK7whdy`e}-$NqsVn%eKvdl{{1EJ ziR>m90NJlYwAf_drS^D5Gm%UO{2!K0^HQbffEX zN9ZMSd3o9V|NI_Q@ni@%+C57z$KqYhgEqn@<>R%Yqjo)W zy^}#3i7G2Sb#)?9FFRh(eati%Z&6YAYUZ>JM4o{U{yE+2nPl4o0dznB`hp z|Cx>2N^@yst_&A>c6zp5VOHkEe2$o?sOT<$w#f?%Lo>jSjf=BwegG&J22R|+m%w1~ zVMND}{zok5m!}(-E~934NlcvesLNN*$?u>b}x|4*iN?oD=1&U2v5t+V9#&O_GF(9jGfoAm}fal4j) zx!5S>^3wkPBK<{H!1Xd?aPW&kjaY^AnSSHeZy9F`XJBqU4ustDP*TzR5tPo0Z)y=t zCG4RZk9K~mzOiPUnN3aq5YEBQJ`V6f7D%j2P1(Nl;k|m7?Tvs%0(^CaUj5Dx)q~HP z*JHwn&11pqt;dWP!`0!=@RFgE7Xuvw2j>9}R-f_@%j$-P1i+=x?O$2B%<`qVev34K$JwTd!_h-OxZggcOb=C91 z;>+jH8&81Y=zb%JV*cL~Fy^P$JN)QBd>C?k1fWuTS93b~;iO!Z-@*@gcz7y)-UEWO zlDm8Jk=kFADH_+S8C4TtDm4$z#>|!suf**J9vs)36{m)^F~JAMo5G?&*AocerfZYq zQ6&1~gK` z&T&HArFSh2E*uB?`*jr?x0B3vgDJeebbs{vLcB1~k0P7~?`7&SVj1qS4;T7zZRT9k2xL91)81j5w*EZn|3$JEQ z718#1j!J=xkKc1eEfrCnYByW*;}S>+ti7g9m9?}$@j#goRn~m|#n#cW7L@PvAWC7K z{+fKmR7!K*r*8x#t3ANVpKFxn66kt;I538>bK58M+~Efom69ThDR@g6pMCT?Bm0q} zfy-L1)nA{v#`l12?M>S{)1?H(4ri9da_hcOfTVn)!VJgvXkKh!(cQc)0VcarwAoF8 zo8>OBmbSKhMMVXp;olOD&dyFX%6z%x-iN=2R*ic!1mftB=~p?DfLRUjRq3F1BKu6W zb6A(oX}gVVk4kM{YdNh*P-|}*nIv)4izeoHee@m=d?|#)@>c#p2%Wos# z=(=0|UaoDyN8pE?BISle#IIR`#H5@rU!x@i2K%NnA$|Vu!IT0XOdQ;mM;@eCi_7&$ z``%^Ocw)@jXLGVRI`QMoAo45#jMl_kKc{EWiHI@rIM?>vL%`RK*UtO0N%11x7fF#1 zSTP8I+<9LFXp@PG$_L<&HIT*8darjt9UUBIbV7(f{X$D;VP%cJnJd-k0~2FmwLko| z}cVR1`V}7H(jy%kDT;t@Q>?Pynir6yqa+e#-#RVbKyY zZ5_T|0LW_|N2}@=dO9wvHT7TZ4gG6Cob!NuS~RmO3hBejirYqD*pt|zpLx#c}9K58QI zcH4j^(l_>y_Xnjq*$}|kpvr+Wd^y)`)O{BU)FbtPd#1J<8?~=O-Ob9%>Y7n_nd&j= zo259eP4_YfJGKRHWuV$!j1B{t3fusrFp&1SukrZ;#=sU#@}eE`-z*;Py>(LpY!(1zq;shRf7LG@|lg3k7_86sY))uw}2|T*z4ulqQq3K zz#4m#dt`Ci`!84zy}|~CW)v2cklCDx1`Ruiw1FBMi1I4eFayvx5{y-IKPd8LD35uYHM)=SdY6H5J)=pY1$?DXW}MD@xR!aUQ?io7vVPfe^O1o zYR_cCxWmL05x)~pU+ONPhhZBqeJHB=N_WL;`RNp{IJ@u4v=Cm0Lz?3A<>X%BA6iwRS2F@ zE;8vHqA6nBQ?nN{`leiP5WFU0rMO8Nnf8zUIe?Ai+L-?6hwZO>WYfXq2F!`%C5){0 z%vcBZeXF1RkloSgZ(J9;)YgWv75gzAFu(jXIA*>%@b?!Llw|EyZ7V8$X{^`}!y(33 z5c$9r)zG|&Xs)|)5BTm@+%SoY8_@v|sL8su5d%Q6`2MENM!Xvo1HhLCUH_#|t4uT& z#cwy2F4sr@E;Ezr<~vj5sw8nLd+(eUYc(T&q5yU4iv&wCZc z4cz<#V@^ttx_@4NU0qBOS7a3DfdZ0DNCjcxJ6*hUpXU;N7~%pz}Wc z;M3zLQ)#&kTNy_J{Ocn*;xU1nmj%9)`hv2{mzT2#vn40D&kHN&zgG15}Qf2ZLSbbhDM;))^5=d1|NU`melLtl4ErE!#dzJ2smGOw*$-5*df zbkNq%mEJbmN2}?ejhEgw*awl6^6F4XLIHN_60c#t-O>p>DG-kL7{1DQj+N9|FL-|c zH#B8!Kj&Q^`D9%JZCTW7V@-4}K%Eb>A-cx-1n|qMu2j4xl@KRb3`xcn zcDVTHjg(JvIzmKyT4CSbt1BQTKlJNVH=R{6v-TQ5Rg;z!L_S?s3$;eV{ zv`x4594qYXAMc;#X6%IPl)}*Ko?_ET*bT;)mZWOU1dP|7yS5!>IE)5nK0$1qjVvSm=xk*0DS~ z>{4vJf<&>nUK=d`P31$r;dsPYo;1~U;kLudioi0)sL{!x)B`5j#?=vs*Y@gdmTyZ> z^D?ck_9X}N#N|tg>SLSiYMNN_oZBSB*_VYh84r)V8`wEAJc194a{1jc=IX7B0q*eN zLb17l9fh&Qfd?w01)M*!deLv1HJxgsKM|B^cWhDMy}zbFU5tJn8(bJMany8MuC-?i zxgg_Jl1vuO8rea*75K;=hk-}IQT|60B#m7s+E|zWM$sc8P^vA`t)M0rvHrdTxzM5% zG$Z)L{%zI$)0JVOB{S2-j`BZ>7xAO(gTm+R$lc5IXYrN?RF`B{p-2X+hS>d8=TzQi z{sNO(E;6iB8`SNoJ-%Z~KkKK!`cg2rb;gFJ%+7))N4MyAC-Xx|4`Vl>_iq&WezUgw zHr@I-F5<5AEk)I8IV+GNj!i3f-dn2M>yW;53pC`qLfD$awwSv+t_v}!oHu9C!Ed5@ z8{A~OF>>GLKk4%Ilpb$T0zY(g^a}~~H%OQ6F2+Px>RhzsO_&_>ZGl!eE;cUOd&~p1 zyfRmwl9_V{r1Jcm6ZvlQ@Y%puwT` zxS~w>-c$eSac00xtxZPkkx|a+9g^eud>YlLUwfZq82U5P*R>1`GU1Li`0B;gDN)10 zh*%(a@WJHxw}Ys;zOjrs+fS4_jLh@vCE1I(5~EB%tkDDmF{v++7dBX@3#doNC(mMx zWmqcpN+@3Xrkp(Ww-t~etIs*NIkH7o#%$@hKtf{*iXSmOZfv{X)kTemohm(LRBlLh zbNc4r1&B{k#7|UrZr&|A`+#P(uC^>K&$A_rtHAU-JRg*{vy}HO)+B!X{6+37}2j{H`(%_aoFX zGMG~1^^jU6Kc?+vpm#v__~gZL({Sj_GosZTmz2CDJ-5)~0|yuN1NHk`6Jg=%B13OvohTba3x4f zO0hfLX=@lYdWUE-TmK-L6~%53_t=h987cNK)uGtj!ZFRrLh!GsNtDDfAyoB3)|@D|Mn`~JIzzNwp)EDIABxE$dN%rW`G8w8v$6t?hkeKTal z2e@#c2UT8w%E+rXA|ud;rG{;o&EPYUhX-*tS(z?xCT^ZeP{`~75mJY24A$i{D~#!vnQ7Rz`deG1=|^d_(gn0pE>FC*8Fq{q^+ z%f2Nq$x9FFV$bCb;`?@O-sWb^k&3rd!o9&}gwe`R>-ofQOUgp~&S)}4!mLncZ3N6~ zw`$6(vDQ5WXMN0RCZHxB8JB8UXVEn3SDxKBFT?40{OD_->Q^C5F~-S0C9;u~KUTY8 z_6@g&Bj90T+8oCwEIT_?hzSR_KRTNhV4584%E?%yaM!E-olbl3p+shGh1#Q!=rpE{ z>W95k#RtX@nn4G1k<0lT5>v8=vH8?Qk$+sP(t-ZY+OvJVN_P%($_m=d=qfQ&Kno-v z1Jh|W(6@d+h$6GMJnzi5i}X>fTix3`vVN~El`7=IA5cYn|Ai;|Op9Y)8JOv|?$Wj&qC z^TeMPG$}i$MPy}4+JQKayD|iIzxV!!;1Gdjfq-6`jX{EFRDjMMSNx^QF>?IAO(TRm zk9VA$ja=R;sTcR;qIF)-@r+Zy1ox1Jg+V+OM%+ECvl}XwTdg1dA-`lsxclXk9nTRC zv(`xP`4ppdsH!DaU&EQ%9fPXlY{s>$fXiJVYJ5XrYnsszwAk^M&a~?cJ)CLV;jP?A z>9VDRL#m6GnFb&7n*tCa>M7^}&%d=E;t>KG?M)va6mq9OGhRF(wcI-VW$b{s7M4+YS?%}zPv(Po%Mopr?wt(n&Llr&;A=y%Xo;07>#{HD42zEWjup3 zd3{7mRtUW(9WHeAR$)qFf%3h8+5A6@yw}a!d?D-%M#MF>tje!L_@+>or5b-^#87vx~k1JOz(`q84@a? z>wD&Bc4SISe$!b)F3-htRz2}dEr-tfJ?|qW6a}Lites+RJKc97r=NcObL(kZnF2SM z+xOii$Oq*K#-)EQA1p&cq7?xSvr*fcX8OPZtCB`3pJ1Bbrz zMTM}I)Bb4(%eB1c42Y-NFZOS`egn}ZEYU<2CuAloye?yg53SX%cR;{A?3P%g0uZZt zy29v_yUe18sVndeMDv3J3I9r-K2&4&m1NKQO(Ej#VG7pSxAQ1~h2r1a*x56%+o?~9-f9I2@ekmTjEQteynhOn^LyJ&f$-H>{ek7P1p=49L3qOe~OPl zY*t6%x{t@z-Ez^sR~&xO{>IB;;I+|E1m9v z(zh(?O&=b+G0Y}9QdU*Omlbi8OHq+;Uu`T#!wQp-UaZ1^Ta9z6 z8(fi-Z&dy6Jge{06Nk73%A!)6g52S2$JtmdcmFhDK{NiAGA;*51i3`4^n8NUTFMaa zpi^7DjJT7<2|cW$Xh95bBv_QTmcPjrlGh=T@QhBDF?UBY7>>0 zlbeE#A(n;^twg)-U37;hcRH933hQs0f2!jSRN@@7@vv94gWjND<1POys=S0E|C;Nx0&R;_oGu_uBY z`XWj=WRnvK`P&s@78BllCPjh1)IZ+sUXeEI(|L-HUh(xiEAH)tTb0m4B$b0r;_JT< zXPcEJ?u-skI%jz4gD#6r_uC)udf|1+Dp#p=Cp&h_Ju?0W4)`Lhq40Lt@mRb zr+&7d_eMUfIyzS0%aTfydrAQKbx2Jf9NbhjWq=3O^jm=f~A@Q~*&nSoX z!5VwO`IK&c--yp2q9L#K$fZky(19m7f-zRjr5fp!v-VzfAiZ4NwueeW)Aol5FQLi8 zg-3g^AzDs;^t}|ra}(Uf-fa}9nQ))SHdtI=OBy>~`W;Si_m^lebM)&~ELcJl{Cf34 zBBWmY_3C>Fo=x!URS6zF6zBCyjb5qy{p(eQQVv80_#<)s-2eL*@^7PIV_*nTQdVZ! zU&<&GA59aGc|E`5exh7CR{GuVVq$ve4va4+zxhg*BUK8IUEnKX>lH0oSXHImv`_?r zKr$$vPlND4pwg9hz`IffDRt9k9I9|2N?6d6?qI2#QV3hZ+QiiDe7quR>ne|K^!3KY zL`To{_ea34R=b%Cm5`L2jP$&u4hasPu>Ae{Pci7B4EOiQbmnF>a*1s|f{7Li>0=IC zbNP0ME-p1Pbg34`6XcE}XlQ6r6UNhJg89Z^+zdIZo2x5Dua~2t>((Qf zCrLL`n*F7Q`X~=%WaPX1YT0Ue^fF}=FQAKk>Gg8s^?Jt!IDGKV6e;17H>-9xe;oYi zZqr<6KSrd|ImVJc;%&J;{agsZCIFMl1Ux3cu-Dy?&Mt7c3vF&r&M7AfT`eHuyG_r| z{<28{g!(5yEzJk?uCw!rY^gnfgqrbcQJGp@bH6}O74%qaemP!g+5?OyIa@jJUO{x} zR(q4VcmxEJEG?HfAi!eK%+fzRoCEZ)A^>-gT87Zw7Or}|H5F>1C~$7ae`nw9q&iJz zM+$zCmzSTOndw;qf}OYSz)33GPnZ;fvR)5MYyBf5Bh47Vc_PmbhGu4b^8KuwR?TNf zK;iRZUIJtKDR(-1F+g{nlor$?p>~wJ=Woxf9OM&z&Qj0p#iSbq5ej-37!zj@bBkWh*O|6`MW~8GHK- z97O2;4xnL-|9&sC3;04Z(jT{I-N~+b91n?f=70l$hAmuOT_;~2yj~*HJg$G(&1M!9 zKs=u>(x>jP4n`j60dvs=;Bsn*hOhqugkLf>VSgcl%_~6mn*t8W*_%=j%GG&3kLRa3 zIy!PU2jG>jZj)ueGt@TnAaLGK?T$XL`ub!x5JZ6mzlghYdaw<7rWl*)NSCwxpb!o` zJ}a~VmK-Jny{n6hPvw5M?L4ahq>WLURw2!hDp|m-)+HUVQBAUVo}t=WTd)5_ArW^2 zSkc=a>zY)RoSdA^Xmeq90`%gVN8=mDRM#xcr*NSh#wI42fQCMn0xQf3^B{}`gdsV! zdmRnYDSJU6(01*5p4a_Ux{{+KXVd=c8APvi<*L)RhvO!9i@G&WzEo}--#LH_aLT&h z`hgOw{%$+@E}^K1K|9FqHSbFH@!k9Pi8m6bz77DPv$@&!-Y8%;i&W!%QKKR&^BGP(*zHn4b+#eEw5QQotV7)W5Gn%?If>0d$n0U_&g+UyYwS9P4B8Xq* zc%<%_FD(S0GfoAegM&x%>tKY!(A(B;yz*WEE7x5!R)0nYaaVEX)R21^gko^J@6GO`Un0?{Q*%ylVR~lVG8_}b z9(=L>7rBIZIpsVO^Jfv1;W;R)by*tJpaN;@bZh-)SX2n+jWAQ|N6 zjCO2~B;VbC6~T-e_?mx&|1qwTR|_1i`9T_+;{8XqaKTiMXZw4?3o?^u5}ip0;%YfH zZ#<4=j_sN&*CA2v9B(S<7atA4&ah%T#B>N&?00;+n)?H!J*~u&h?gkMIXE~{82V5T z&U|RFuUl_eMwg{=S|qtNeeY&%i+BcXc|THc+ql@-6F{~G z2GpqhZ&#xPoPn2g5m4I2I`Me8t~fRD^Z?^wtNOxaKIDV>SxtRDiR4{38eTKuUU+14_9yc(zeL3oXaLc2xD9xIFZSer~_2(x|FowH_$B(bk+kw`d5+4dA z$iQ!JB0auVlp%GX6iq6AD?6Rlu(GlC76fy{O1DtgM7WsOyZqzDmKDCme+E!gZ^`YB z7JY?>C$hf-b;bXBvu3WP?(BKQ%CjrdP4AgU zKp?gCe*eQgz*A8`C1CvPEJ<4S*mzK8Fd@dE{=^-3K9ARpo)w#(ZpD1*dx3yuQ@=AH z-dtJvTN#=lCFv-9q)P>r*4eaSLtz?4*9$O)R(#4Cq`Jp)fiS;4!8_$9RXSAjT5pHd z4&ua$(tQGaMCNJo!g^#3hpb+hl+b=&V+k@5SYMb@Nbyd$qC+Iin+Gc41pvB(3@V^# zcTJT4Uw8W(P-nTFNINbiP^gk~J-DY)mp}Xppan57&5=9Ge+&w(=0!kF1%WJ|$nu;V)<(X1KoiEWve3eWh zo`(}Z1@#bjAo^nG^vCj;Afj-Uy@|{MltaMy%GkJXM@89AL}y7NVfRYkzvX|6Wy8-F zsM4U`eP&ZsFZfT*8_bJ$qbIPK3MlW9xV>L_Usoa~N(oKBin_?!9lS}?xsOQ|!>O(6 zTcV+JcgoiGyfdl!2^MT$6T_iwc;56T=fT36#M`Me6!lq1dzYP^qMp|zb{nQqUwSJR z@0>Vxk(D+Q%2oX+VRguSFBC3)7tnGxc~1S3G>#{-t$~WdY9k#@rj|~@$tWR)E@7D zn<3IeO20 zNMl^4J-B0yhjhX+43TCS@F5o*4R0NJ%#q2T8@_(9mSXdenY zW!X}_p8QPnp!xE`ahvkw9Yth&je_S9Bg)+17zOrBB--Qr`sDVKmzDxrl$%bCmj5@W zBeh~ezbF?UrnY1`_~zD95La*}H)ytHT11QKPpD;*2IhOdD`QoSkMAx!oI=tL)8;0C zZY({^R?{BE?o8y@5IaneEQiNIavXF^u+k;Vk0G>7Hd@IW($q5b85in6?pVm+C~g_cA0>}T#icOku5VaIPbMJ(d9bB!hp*AZuwoK$~qK~2x4igZ`3XjVV= zGo9qg5!MeT>#^Q4Y=YB)#zmlww8N)mzTi!dZdDQD$}6?oJu0GpeYyn}oVRk7Ls;x~ zP|NA*IY~wDs(y@i4AGt#S(;utb8A!76uUUhIXdU5F|fmkxsLW}mkGUlq{gk|(-RuE znuu$4NN}~sMP-giHvgh)Z0(Q4u|Rl;s@is$$+>S%mUcNm3_`449SFLAVB#~of4-~u z>5iIqoqXDU`u86%O6k4EKHhX6!vWWoH1lL} z+xKAL=D;YHx;=61qoH$_9}EoTHmbge7_GSkErJdUp*LKK67Kb-;p(c4`AM|tTOMe^ zacdhE((*ZtUaj&ok^G`YYaT&J@@ZRFenzH+CfjI5F3`_KF83y;TD36+R_^!Y%>2Lp z&8s?edRRVIhjKQTQSa2*$L!9F^(v~ykqbJqYu55(D<~lDB>af_=7x7bG3Aq`1D6A5 zt$G6;)CMLXE^uJ~hjGRXIecH|Bxm93wJb3cBr76Tn{M(n*tcY|GNivGr4{qHHVAQI zTu>I!S+^cs2odGyl(iCnW@K6sV?Q|BC1aHD(RD5L@9+{P;LKlA2>e65`F%pDwRI;# zo4PDO;)4f!NT=-J{|jnqRee&&AwVR zEPN+OtJ(d!{vYD?_4P!N19`R83)Mwq@AmiC)(z0K=^qe?l20kx0;r@TTf3FP*A#Rc zKeH?1`Q1h7IjAG!`FMHh-+?3IN^LVauC-&@%-Vj0X)N*f4^T(d8f`20Z*!|xclzjW zPD#%d(h`nQzIKcr(9s~QZ-es+O7mV=B#C zCaavzm+#61ko$$`LcCOkbp$-R3UV&}T~ftubE$!mHJ;j^m zSrKJuz?W5#CuVVM$D5l#{Bp9$Xz523e!h;Kvp#mY0B=8RZ}g?-BE_84hxCs;W`f(0 zffj0Ro*JX52M5#gy_8%Y_!MSIiWdKRqdSs4VSS;dGQlpCBea9us0CMbd#|QDBJXEP zi7(phVmb)aD%4Rz$3#qKqjxFsn+_5e64XFz`s zaURFRy1u&k;|EAdYQGfd_Zq5aZKz*mqa=;R`9hW&hrIA-?BFX+i&^y1O^{dx;H4{uDNuzhw6m*M+LG>#Tf;-27ilD8 z16YgJgas0b4?ByGTjG;;MmRv)Itt&MR^&!d3D0eZpc3}?5q!oOjf2ap{l*oFL!F2! zXF)=LZ*M@n?0O937oFBqri4aVT$j)-PXb7(Xof03LpN4{GF<)EdoPRZMn$LXJ3ZP4 z6dNUf#CKL$aCe4H0m^1}gJ2vpzN~(-7r*XYy9}wFF4rEZT*d zc@2m!aB4GaZ}U#{t3X&xgUw+q^Ri6~QNGfI6oTNwbKV{X0(vL~5DPjEeAYBQE>{oXB6joo*H%8mWHFBV29{V-zC@;GwFCqi_mYvm#aJ8c<3Pa#MBqJ6t z4D6gR;2o7@B`sDFJk+}djep_es)A702s)jq+uE;)OAX0aT90WoNASCM795+yhjQOX z=yMBg3}VKUL^!#$Wkl;CH0A*`V#bM2( zv*jh4MDQgnkdJR<$)S^;T27c6BKB5&NMw2{{ivTiY((bb_3j=%~@GS=z|oZ3R42d`_7fnc1kX5 z_oEI+C+Y>k8F@z*#{`|?w7m>J`X_rvimXM+p3iv)%wO1RJlxrP(6Oaxva-h69MTE( zw}f*lmfwlaQw#+u^>vfm-9(hDhul6pD_mt=R5+j!`PPm>QkoVa*E^kV6bqrg<+CXDf4Fz^KT3sJQF}HvCKm|v@ z%;K27Dy>f5h--Ipf2^;s3dEN?-a!}l?IE#!52=j&rXS~;j{jkp7LxyhmHcK8m=+p# zi=MZzZeV}FBMytXd5%k%Y43hiRWwK=DKxs2_}sI(_%Xj}`wqkLF=RZG2yL$6=U#V! zg2a-nXy~1JK*YTs!Tw(tBs*C0wctSv%@CNBOyq+R#JSn?(-M{su%u$y?Z z$7h>hO70eeR3xuaLUGLbb>*O3`JY9d@}^f_Wfk+wl~sS#@9w4OUs8=NHiLvJ51ptf z+9yKadzbFNM83`U5c>9VRX)vsK+5}YZjDiG+leDRITpaF~*snVEJ_{>8QD)Kt zV5K8C#uLVC7Tn+BQOWTTGa@xUPG9mLPf8IKCmWqT$Ywr&P#S~A9Gl?eKSl`>;0CFl zN~@6~j*d@lBO(#VLJ&g!UiXBtl<+ked4~>vTnlaZ1+i$i7eM9pMiKdIqus{RRR7V^ zuz#A6`wTChTsi5*hZBRrni;Pv(@VN$(}t5Ql)VCO@{TmXFR1H=&<9OvEyKRgo4d^? z0Anr?uI;n|$y1xG&^X1cf}8dpo4K28&aIW_j5_h@aD5|CQO4UYOY41th{#ei-_q{1 zpbU1On|~Mr9M*6THMOrkN6otYJ19}PoN^QQly_E7{kIjy#h(q?CN5{>l&O*HW}zsf zA8YqZ-b~&Tsp7p*O=a=dcsJ+`TTus;BE_!KnfiUYMqTc-+oW?pQ27>^-F{eAPEd&( z*oBmUi@8=A{%g2~>ZP471W!L=Gp3q* zHcKvL9-~<`%e0T=l{{$=!NLZODBR6YoKtc0hRohFSP`TdEEMj7@z+N}!#;z^=_8rd zVeVJ-yMX;32F4rav$%-L``;hPfwlgvO_y&5=U96AO6O%dxBQ6Rj^fbyzv2b0HHhHVbBv_DM|dFO|ILvY5f)_R*;wWPF=2sPek z9cC;?>{2l-kgyWfdW~+nlgt5vB05eJ(twtPnJ%J85i(U<#%aAzqKm=f8sM`+R|adh zts&uUoj3jNub8Ot&0B2$`i|&q$8~oFO3=Wr(T3#9M@~8fb0vY%AQ;%4E)G)>MdB2` zw?}LquDt?Z0>ipo&piD{YTV_?e)yORQHKg@7ig=#MQ1HTVT{reX(~ukMMmoPUia8J zAUx5jBBHJTG0fW6ogBhcbBSVvy5aXJkQ?k!n%7*JB*Dh?X$-3P*T)BTF}f`tFk=qL zK$y124n{f8Iq?!Ddf&;ZgCd^Rfn)iq>OtPqE`!&a%(`M48r>fl+0sJiDNn7J`>}3A zGrFc|!yZ(Ad>iVEBmY2n^xl@{)eU1+Tvn)@t^{s?MuR@F&T7d zCfnN+Ds@1Mj|~vqcxas;-;70Ux|j|GD8_wiPv_0YtMnkuK*7LRJ`TWmn~+!M>;o-Dpt?p~)4al6P zIkj1Z0zKiD>qRqwbzQJxKY@lhBrN=2qyL!;%q(kuQ6a(`Jt>oG)1sn0uFwLCpIu19 zgxn?rXFtDY;v5_tOt>^ofo$ItaLkgNg@whU+}fL0acVc1HZ9}$*zzbZRR5;c&%XLF z#<3=`X#Z18Oyg#e?W3QcSk9Eg#Ho|B+|v{M^g`}jS*z|~M7**_lYCPW&s=RN;)#K& zWSaAsf+<%bF)?|b_*t6jT*{1PBVAx%AU%gby!sMvb@Dr$fuCEIq1@ekHm-zs{L5b~ zy-cX)gzBPpJWBEIBQbb!Oma3vj18iIkKb<)G5-9GlEYHdx! z!NJ)*{PAZA;L!s|l;+rAxb2Ll#-ye)rF<8v`vW8+52i&iG9w}*cdl=MMEOCK?OX_V z#K71Xt&ot=+c$4Afsow=Ct`{bKpI&74#3)hShA_phTtQhlH`vhbD42zedLa;lA??y zCq3!3@mp)DOBkKyQk;)NSd2Dmq#2)H{^Ir)b29U{0sBYeJDOee75nwC^hb+Y;xjdI z%osyW*JSICKq1Fen~K`9DvXRl{*Tj)-C{PaFduGW8`u^SB>FBkB5i2$0nLDS_ynN5 zG;->4F32;8$2A3P@VmdGClUk1Lj#vMMNosv-6wBTwwWb|SnbicABHt4&pqymX+z0s00d&}u$eG*EZ>4u?FtE%_d>n;XL7}YjBmFaFzdFIVbaLEA9R2Ue zcwBuSG7qLh1?TK29wgESKNOF6CA)gm1tgd8Ysm!lJa9&L9UmQRURyK8=9?L}{9(BY zZo~Gzh=p9}X_6$XGe&grV1}}eu*|cRwSeNgANWY7ZAEJS_q*CLZ zeGjr;V>p{_^-pM8n-Qf|X0#&sqsLBSvfAu&8LIQ>>j?;S{~g!F()P$!s@;<0dFn^; z_ZIH(qAL9<=|P?rncPek65aV=ZJ zFDx!j0^wwFFW?~Y5pcw&(95IU>P|1IRSux0&Rs68Jp-qoE*lG~%E+Le_;CVBL-IyI z@pGZT#Kc6+v;lM&M*vLmOWUd9aLEg@+$_fZuQ0Em-XD>ffivKB@rA}lxL+V+!APlk zg{&rhIY{;Rffnm2lEYPRl@&E&g9nP26w%Wtbh8h$rj}~QB5g|*rG^GHv3yKF(M%~T zdoNWd=Z?1MPjpG-=0duh`p1p(=I4y7!eIaEz`+E?rG>IJdk^i;)7t(7!_`?jIIN!_ zHY|-giyg;6$#N<_C(H&bH0+%(xizHj!%yt?PE(cNm=75YHmABzMxpLa&Klq3Kqqa} zy}@vz2Dc;|gEQ~)k22_|;>XYa#1b5I5Mx`Hto&6(Alj^=G7^uf#8^^E{Jq-hNp7Hn z&F*itAbH?Fe~&D2#)*JQ+U+c&1}pBQ451H&iw+uhy$cMz0rx{aPOx93+&Lw_3QMy` zUhQ9L>z8ca64jr>Oh`r%xuswd!@p11$L%~}>0JFVL3ok?_+&DfH%SbI?o7$LB>3%>!Hs;~h_z9Lv}vWKAtC6W1NjerRZD zYte8u$VR@W$S+lSntDQ8NB?Pc1?m8V*=?P;>@HSV$*&Bf+*2oI_8KJlxm1037}<1e zMq{x!?4+1UJHOA*HP_zv4=+kUeY%zr0nK@t8Ua%7dS;SRoU=IA+8WdSXOBOo%-=*m zMzZWM^U=x?Msw~Q+Yre%>moB8JxsSWyK4byd^uve0GqhPB(m>8R>BMm%>WZBI z(F#FP3w0{9`P=9IPry5UyIv(jaClqJh$ePWyH!;xpXpb$*Yau=muH0E3_s*t_+Gm6 zz0X~Uir(x;SMHR)$nSb+s5}q18?rE<}KLEy$ z&p02YAF2l^^DVRel97?^4FTqg{y|Z@m&qgGq(LRr%~X~vFE4+=%v3d%cKJIky?94B z=!8yssGi0a8IX|>!Yqc8NI*4CV5{3!>~_gQT(#WPcfea2&3hzZI_)h*!3Kz8pH%uP>f1N)GAQRFeB^a`0@K21{@XFcK>5a?r`+x{~!LuPA z{ZVGj=*d#%c2({&b%5}5s^?rAZH7rYSTu$xeuo9USg(>sYFsCUMWx; zr=%p=x<=o1=1I@T9sY#Q$;tWq=<(+LvJwGG^v%U9z zIDcI1>gpm4@~WndP4}^|pcmS9-=FzJ9-L04e!!14`i%-m^=s?^CLRtYpnF~P{@@*< zNz)D0^UZ*8KW@{%QjalGDak3YZOKsB7_5 z;QGT~LT@gC*^cO*`RJ{}#T#xHRuCENb`h}$=EmHXr;C>HIx7$GP^wg zQ8S8{c^-B!J-t+Na*fSfSXZB8ub((y&YUy)9XPD9(ZHP7izqmgs$6191?U0+$Fi&i zg4xz#$3NDS)Mq(oUEB0sQn(;i?MiAv4^!6iIK{tc|*h}N>gN}X1%io0w*jQbw<*ivN@JF|+%Wz;}Hgu49rjzR8~il?D3 zMMYXiJ)W143~Ab158!1G!*|%&J9o|#`GUT3RuOyGI#A45?+cWu3m>=n-Fuu13LiiI zWvQ$pW~Oa7?L#vRBh%`{1fS0XZgqz=uzFpLmn~1~pzIyFj1zbTAZN9_SXH7&LtAc8 z`80(;@cP}kVd1NO#BP5$Q}EcA9}mz|jlh5fr^RNaH=r@q5@>a_!Vod$ zXDBdT^b$U0%mqdw@lUd1Cz5;Q0W4kJ=Ig)YI)g4tZaiHUdLQSSn1YJm)v3JB$>9`W z8!*iT&X*6s-QnZL(l}8#-0;7Kz_dE_kF9ch(g%c6BYqDK=C3?#@wwd}HrAS9VPkIx zy)9^<>5PP$Jzlq8i`3NCnw^HphlhpL2!%V@%Cu=OF2ZWE{qL-^h~$L$vF^Lf^KlKC z?0S@rfzG6De*oj+A_UF=z#Piys7OdoRH_$Y?LVkkq|62H2=MTF4tthfoM3n(1V$M0 zen-pNWd>|PvFw5d9|zgN>`H!=pm&kq>0Pv(7N+an51`Y#T2N~BNd_6toTespWCw5s zKD(NbBgPk5qbaJYAm=08pz;fnb&TsuOwut1U%2-vMPnF)An@u}g7D~uDno0;<1XpZ zX*%=Ri&hV^ejLA#s8+(^>`1EnbzXYz6r*a_%2i762$5!HcoU58ku=b5(#vs<;Ukf8 zJ~JjgIGyZ})v!Q@FlR>`d@uO*=_*LRkX}t+p~ofVj$w->4T1U*?Ty<`D06KLF%ig+ zB>%4a;YMr(44C6Nt7a|BEo0{*__aaub1)IIc#TvL`_EeikehT0rW4Y_=)(5-Bc1J=^N-7fgP_V5Q5-v9=`fwuU4ct)yaR`80X%fc@bxPHf zbAJp#4{?6}LN`DCbr6pEkpbPR%03U<4_@B3=N}=3@CTAlLIQMo%GO5jSDXFu#Xh|L z`Ye+2vA1hC!+-w4BK)?;fO-+^u350XKvuHi1VgB3ddt5JK%Gu%=io+wn0yxri?OKh z^v{nMrvp^AV{~jsK>K!H#CG?1a$+K$SaG>#6rylT*!GZG72vTOVsbMwGV++&3u;8ECDW z-qC&w!hb=aVX?p1m1PYkqA24A=Z+M|)I?FuSRH{Zx7taGsqCD?8&RBE1lVNS&! zAa7||(*Y(CufMvQOw(e;FZbtva@`t&p`E@_NKO@dGpyUmQ)bkW1=yW@V)g#>i{~bn z|20zossDesO(TnXUw*g?B)&j463shI+)XcMHxL`In`l3a(OyALk_mQuU7H<7e=AIu zVR5r2)-;Ns5isBnIIX^xez#TcJyp`@v1IMg#ev*}sCvmHd0ak++7V{lw#pyZlTQTTO&W&M1{ zl3ia0sE;JRPRcc2d6JqM`+QfEq9EbzA*z&FpFv?imkiN0-7i_%)Kha;th z$Y8ShG-bBvwl8T{V`0aQmC4vz?Kf~6KAj8-)VUtR4;#Kb0GGzov{@YFvt4xN_>{k5 zO#5QURyZ*4P*E5e0y)KNQ)__)%Ct+;*gIH~N=oz4n`o@awm+gynU7j_>LsxG*@?4XYCV zqN7@CLpUx;WlQq?Q$6gD3a?sdNDqK8?@?joBYpjm9eGh(ydg96pz!OEKdEK}f9&AZ zyNUL4zi+UAQoJhB5s-!NCVyXXtJi4oQ4He6#FAd*O03oS=J{d2vERvE7gEBM?6qXq zd9y4w**N>_xzBFCx%jtR{L8%z)v1S$>KiN6hMv#UAo&E})4QC3R>=g~!v%zG$PVZA zdL_lojy?!#x!_G159>#&tRZ+1! z$F9}5pZ%1-N2>A>We&1u<9PK0{N5pHZc<#m#)IG_KX)>=+6kYx?xP~AKzl|0^0?cb zF;&7!ib0*^!M@l>6(CMJp0+LcdyGr5CCtRdUqg=ld9NT|z+3kgf_fOqiL56R;dcNa z88PsG)ufx+xBgp-g9b3|3TS!{aZiKkg1DlAd2oV$`OBsMWIFJ}Xl$k;zjAv4IC8#k z1Lll7(zn*kqWT|L4)jAh=~Lr^IoXWX?F_Y2JA6epj}`|yQZgBdu%^DCprZ2ld|JaJ zmZ%^L9vVDP2Db3Ds&J`;PaCDk2Q$;aGlwV9`{9V}?JGlnle)Q?aee6YtXOB)d)v~@ z>jW0+vAIf@W8>>~L~;mggL6SoK75pG1EqDF^ynwfn z)W()N9Bj9?LKMT4IYoa>vRFI`g0P6PVCDsd*bS4pb^d$fF^Xwf|5?l^N(r5ZOr+u^ zI>4g*t_86|Zkeo$?1u38ZC)%wOMz9y=gt7{xK0t2MR*WC#m(M%{Yy=v*`;BN}q`g%=$sobLFYekwG3(M%A9$)jrNSGbnwxbo5= z9pD@5fe%`@6?B1o96)wfcJy#j(^nVuN1vn?j@|L{8(Oh{N3 zXUUX(YMMR#?)AB`YoRT>*s0~IOK)fp6-$|YjnNxXUcm?oy+>m>+N5$YG;Es4+AffA zRbW9S)y0vs@%t53*(b=ejdz-4yENBV`d)FEUPz9kdU#rx9!4y%&~22oBJ}QUlHaoY zr&%L7^VNTT)A|*;#iwu)tVO1I_PlIMoFFV#smofZ9kKKsZ0k^zz|)9s~+ z2Q1uamEc_-$=>*{%NmgQr*}RiqXAN5s;^N7%A91O^A}z$pKds{V0z8+CSf7BJ?Ch(H8xo<=9*TsFp@w+Qhe8yM`LdwjjW{EXCf z+{vx9;Amw0ibzmwJNHE;y*4On1&dtp!;*+>dsrpWoVrhVEHF8qi1y;mc&`do_)vVG z?K_-zbGQHpTWtBu&2V(-Ok@u+7&!lD@7^<|C8XG(;u$|FCdA~_lC||%7=t^LF5SkM zn@vkZ^81Nk@(D7#0NBo*8|;R3HVg~6hDtFsf#!c{20%OzI9 z+288zp-U9RKJ;8Iw56#Gfdy)WC^r4*?KgJ;iXWAhXIDm(&J7HJ*e(319Ubv09(+`Q zJU%KoMydQ;a*`z%c6;Rxr}-9sYKq#<<}Q&C@e4T^F{6Y@{vWsW?W}Py1tY1 z*Z2BSiT6yC1M+g7sS_LFjn^8Bq=?$zi<>S~+nh$5C1kPCNdJa2Y9XPd5^>-h7QO1y znmD{<2xlCZ0Nv;#5>--+@*(S4eh8QLS;7}23$+Jwgbc2`z=nCg_ooc`2#NBQI)Z09 zW3R8?2l}YCvmpN2tRI#%v9T&Z@@l#UpxM+sM1P~QD5&S~h;MY-ciCP!{`6TU34F~8^Y*mHZnY6eolp_Yc6SyOvH8T8 zDZ;*;2ZYx)0zyAV-E?+#HmSU8U0lWC79R0n`@-E+hVruXJd0?t{Wt5TKlp!6w*Bu) z*vlXHe~ZZZzZ&0l12R>G1|nF#ZY+i=Xucx00Rdvq-vIUQ&vY>UsR9zxL-Tf4{)=W- zFstWZSd#ogmlS=vSx3ixa11UkW_ST%PAx8$0h*cqj^#9+v8t%HKjh{N1$zd!Oh-YC z1DVL>kf>(#_J(~vUcTaU1sV<&C3$W?U9e{In8KTJ*`I&+8O#U#%}n!kjb`jw@rA~)|ez|=uj+`9wWJ0FQ@M%{QPkK5*IVV z#SwGe!gxucaC;u<15#Vw#T?TMmPLdJ^6nTIl&DqLUgv2LB(79(5Y}yy5&tl--vZP) zWOU_EoQyeU`OA<$eQ92)?pgd*vp2HFbEhYs zUEpHoeMxhnQtNzl*O(Nq?b45r(c=LMR-X|Lan0!d`=OsbqM&{*p8tKw@&A+HsQ1e2 zCadrBLdZwx7#PBReSH}P1=A+ifZ*H1{l8jEuWVqT1+bOLQs_rVMqU8F=h_7p78dFb z4i$acNxVEfA#zh`GtJdPIs>>K)?_BqqW3LKP`u_}i;&euhJ_|T7zmvIS*FIE7i z8=sW)gIp~6_))k0!-1vP`cr_O_x+t!@B+QyCx6}6h&{42Mn`QL3Vw*De&eRNr<>b^ z?m*1cKXUxvNJc#l^xgv8L1wG0%>a%_7<>K$zP4M`-(RC zZR+qZJode5!-5YXY#Q>r$JWRKkNiv%Jlp~vpBN=J$=|W6xsc1txOyRK{pBB#HgDao zq5c%>?R$bs%7i5PNO(b(_g?Y9hBjmE)0#DAO(Dhp5af$Na7 z!B|={WyLsH;J&p$T<4*rPG`}9YXuzJ5l>UQt;D~kbC`FE`f{r4}2ekOo%d3_HMcOmyb zG>Tbc1Auv&0Ne7ve}5$X0etPJf@+!?@IK%=mAlwCdQN*u%IdcJ3KX}#Ohjv;jdgWn z07$iS`v!=Jjy`s_FE1}|x$^Mxy8NVEa2k0bc{((~P+p45!F>Q=a}0=6mOA_WBKA0B zqE%U`a%QrFhDXU!_t`5jSf0KE+*BI5Xn@x-;l5v$R1=x2XU!s3(^}IG4N%bHGlTgG zEjQB}*NgVoNEjQ;)o+sRzhyRt&q_gYHEOIcy*UWWkg7!jX8q4^8LDJ%yC(tTXrC1bT@eq2J%5IKbAm7ksMcSg7G7PH^$E z`(w#w;Z1}M+`~-Q8z*toG4V-pM$ORGH1Kc|rHw{(m`D8jO-0115xO$0gq@T*R-B@) z@qbwJpP#V3!mkmC9iRrN!i}2|;?$h1Gi{C`peFT6*IUoJFGIoi{b2v+Dc5OmKwOLo zKI=R>Q4l&2CHpeV7+cWm>_B-!QzR$b-lmKn?+X9Hzy_lEgx7G z7+$2*PpTT~>W~fr*R3j0N{ZeHwp0~Oeszs8E#8K_bUqBB z_9VwaPErF(1VP~*&KjsXsM{4;QnM{?M3p(tM_TY}nAKXv53tp7i&L2TxgbFDEF@a{ z@{Mh=;4btU%brd5s1qJeT)d{gYw+L3Fon|ly`>m^1rq)TqV zR}II4f51lAr4WD1gp8!T{FHuseb;YML(p9$N~B+dFcNx=@aap0Jg`SLq6wMzvX6$B zt}-6g;mi#zi%e|7vd2W~#h1X`A=oykaX~yJFvSM*1!!SRYhO!AfqOdT zWxzV7J3iAcd~ExsgJE7?``s0lAFY#F_?l01XVMBFw?n@y4TYS7RjoaU5 z30qs1CI9Dkb3(^Vrb12NwDIx>JurPre-vM3YejR@dbr04|*AxI}t)rKUn1lg!Ezh z1f1fy5UiE_iZB!Kwk2&-C{)(?w%`a;-DbjIZM_gn7Lj?)-B3Y<4T0s3jj8L#g4}o13S(vtzigy%~&t{^UX?0`=pM%Tk9Pgz~biQQ^z9 zUK6CJI3KwKs=Wr>O#oE9yx^dKNxsUNc10;cm>z^X6A{R2&#fvRxV1$96n8R#_+8lm zF@=fS)R&qmL7z>h96I1z)=G^tfPTI{JTNN4g+&y7aqTJzjY&(F5Eq9l?(8Us2G`Yb zdyL3C0-7nKQGxM3UmsSjeccWzd*(oq0fFaL7fi3dl;lBV z2Q$q1<4F@rv`EHJ!Zr?&PAqzHDGFc-Hu#_i8Lf`yk`|-{EJRk>6#_jZV0Q`fSgD-6 zEIhffM$`JzTU|A6*9!wXYU$)u#!#-6)|V~y^V31S zVBi3)&7#G!_NSKpKRV!bwbouq+D(6DpxgONJp*7<*l7ylDX0eqwR{*jl_gbee=-$% z6B83Z^@<1!v&MGCJp3rmT5EPe!uGj7+HA=7+9J_ygK)6{5N@L=Lbkj@g_Ig<7D)pQ z4XKKXiVGldf1bgQL;+ZUAK?02Q&;2&R~LdLX%{oiMoH%4 zk<&VqI2q`Kck|rn4bhAOggoXxQE>)`ok;PMVFP}j`osmm)3j|ylWY?X_3~it0i^qw zW}iNZ?IZ5tYAMAktX-G#9Ho!|RqoOA{A0#3J&s=K_iQY?w)P$flSuO0+mSg)e*9|w zX^7P1mGpEzDWR)5A_eh-v2o^sV=9}ZjlD%DPk-HMb<|O9t52i+4`iH{g!IJd{N_kZ^Q54uH>~z0=YM1SI?V zx;ocnGD59O2ANY|Q3>=jfvWF_ap8l5j5? zJqSxk?$|jr2{^-qOh?{ysji0(N31MKj#KSebwZv5xcCi0CP1&P7X`Us#*s?EoklHG zN|7ek+}wQYQMIiI30@axAO`t-CH4LZfZ-1hz-hLx03JDCa)ehy0nbOC9r+yXH{GUU zZ1XCR9@J*VY|+f6y9WEBTN!?@&qS&w&-PrA``!9gR|(WZlaf{Ol1r~0W*nBg4wCVm zdSU=8QE3RY3$q@&E)YtQ@|Wq98b;11XAgXL!_m{>)Th9Npuk_XRKJOm|`MeYPXWq*T`e`FW|-_d;Ys6nT(@gyG1uhQ-j%`~z7poL zCHj$!u_)}j{pdP-coky;6N{A-vv0_{E_&7VEGe}MnrRLoFh_*$m}|`093*~zswaBD z);j~{EQl_eXq8sorM}xGs|t-e9^-uyoubzJ)@*j*mndH$3+!DlF?;YY)LX&3r`_fL z5<3cJ><9lWgt1VFFw|sV#QGc)b1!HQq9i6-N?csY7x2dKk&###q`2VQ+uKu7M%Bqk z{syTWSW^OX9AFDvR}B@lpXX*by?;K)MAadmZN7HQT1@BQbH)|wQ$FZ@6Pwxl#=~yF z*)hV5!8yj&V4aoCRoD%pm$qZfdJ@l^ z6prbLMw!Vg3Ml}MZxMYvk(s9ilWg?zR8l?W1wf9#b`P;08{c_WmCUk>Mjz56(`_s5$R4we};76u*gEeCB0|rZ7dvUV#|#N;E;y!++Us_hpJ4ha98+DL$+5qE-5P;MT|ihpW|T?LM7}2B2Um7Y8zwxQ5JZ} z^{e+xkW`{+ru={qJrlxw# z{k$rr{IarWWMUI{$iB|Njh%bQc|BLooCDBhqIGNuG$q)5&S_HO7mNI$tR}m)D#A>d z6Xbhe0cy`2%nBF950r~`;ywB!`?=pxS{xiQI2cdlJZ5r~8ULG(6W|v8!%+Vaqp!X+kX2&SuAw;je9z9NTA2xr=R4|9Rz7;x z-(?{wfLGlJ^O=0<>8a#+sm}lfaZ2Oh0PQ*85SmD66Yw)NhT4Bg@+MGPpm+fI6{-IL z#1rQM1v|jc-C!B%2izcS7Q=%XXttcLKE&7L^vd|VzQg(8Ye?UIi zTEl^Sk6k#f&NsL)D@kVxg#S9L`a@xSBhu*4YAW^?_e2N5683d_J+d|qRV)7OQ(5Y! zBy+NvmJlv211~-=f4e*aN4~GKoOs`Vq7E2ammCyz_he3)Y;Rh7p$b}eFY$b9ImbY} zM-xa|l3710$@POLdLDuh+sRmGYHC{uRqA<$C9D+LKeWaLfDf^vYOIMuDp_dPg5j1= zc;q7T6Sxw$|M_KKXm>$RYj?(AbPs2k`LmfBX%b1(DMv9XKHd4&H%;Fk?&mKh>=ZFj zWOg&%xLeG`ReX9rr~Gm z!s~V^5>sNFO!24`uZb!(4MyVy$}70KMop!RYCuAMJyFCalmxNgy*9M*DKIDhXz0p( zy_t`MeLOqUDO8ugetphD19LZK<*82=RngWQ@9)eM?nnH4KHa&{H1l)aYdg_-uc={z zceC2+c&)#+COAmSO76Fze@ZP%KU~)=bR|XcU-RwE@!WHNC%^)Y?d}j_O%tVfml-gS zT0G)h)4-!yBA{tX_Nyf{_IN)>XGQC(%=SYolZQRaG_%Q`>g4yT_M|f`05^UD&7Zn+ z-q&RmsavA|Y-li{1an~fR@WROnaneJz;Fj}RH~w`>&+6r*W|@(RoPAcbHM2;KTos1 z@3s&mTo6MssuZ>>)+YU?{m+t16Tjq1OZQHQbkYG&P-SjxLZx>+gJMOZHJN)dx!K~w ziQ^j$r4?Cu>;oOUBKz|pH|mn4N%;BdN(%e6Cu4bOpT;UnEb7?QD8F>wB>DNsrsj%t zT)w&qt@_Fx9=0hRcZ-C|-^Y0no+7+InfOIp;QQ%3a!!49&xY{?sD~ioti%FQDwBI- z-}$lKe(uJhJ$~Dm;yvONuhOr90{?-=9?$?efFw;|+MqU}=Q(FRPEc}n-E27%OF z`2usXoBO%V&qXA0QxkhCO8C3hx9RYM+U11;QQ9ZRliSVl#iR7LM!*vyE3J>(*5|B2 zp;MQkukFfDE*ay=>WDG_qn9i5ib8(#a^fAEWL8?1OzAB0*zZ@hOv?72oNt2XeT+~e z+qYO<3V&7K&)vISo=hJOi#lZaUyM{a;}fJyKf!l#o7^G1y^drLxeFfcC{KzSicHpic?PPQAB+^!6yE$iru!s7u2wSkD_y|K2PAT`F*qZ@Pu6@JDr^|?2iB-qA{@yxe4U?l@dPp zpkixgx|dygu^vWWcb#(Dtxo1%X@tiQ-XYYsyQ^f%dExqq*;(|#X{#e5_1+j8LrviyZ6T@Bm&E> z`ofFzP3H6Sm(-BQhi7$g`zt7PpSV0k0WWngO^{T;pYWrm*7zAv~*RzXAg1J-ADa_k;Lcn*gCd+UymSW@Dx)G)u1V}IT+E^dZg zb3cT4asEQZb;z^Lk8Hi7Nx}9|7yHUt2c#?o8}Fjwx5O+OQxJw>eMPxi`}0k&TbVlR zefG_W*?CqmBW0FjbfS&;O}2x2WJZbfie7v}i`cU&>aym0A%S9%>s`V5!-?y5_bM-4 zJfj^R9FCElJZtQk8q3J2_D0;ZW|iq`Vp*TY3C-!`=EmTdz>ieiH9GDr)vrD+580pe zr`!14z2Xu8 zNApN|H5o1INhSQ%5pSQ>iAP0*f25f(E}$+<+{RIR6>|`Rm3AhM8TBR*9=@{Wz1P!j zuKkCT1xB{svoGN>j50-M5Sm4xwbV(r0S<54i;C*pj) z8VYO;XdMuRN9g2SZ#+ZB66Dy~*))O-6*RV3bInU?0=EYim`zIAk4!7EC?DD^_xQgp zPX8m?Qr651^MIE3Ckt3Cr>raddKA%C8CtGFXJYkbSf%M0Mw?R|nusN98eKhyKi*Qg-A6yNnZYEsx4SeoQ6kr6B818E8 zm>M_Dz`p#RAI(OLz?rjpT1mD*bU8LT#j*w(|DW4%ni_)mAQXa2i*-zpb(B{~IdiGM z=*Qn!>U^Sl*GZe@=TCc7(RPw#JEPZ;kIg1#P$5m8)vVUrc!;*$_8^|2)C-HS{QNJh zzY!^XLXrqOmeXqdRZMr@gumAXI{Y$KcM&-1YKGDs*0AA6vj)5p-Xhx`Shc3N;#zFBU3;yaIMGZLyR_U0Irs!=qdV_)$+?^S+#~QYm$#D90apPlG z-YmbH3_3bF(%+&E!x*J`J1+Suv6#Ht0R&VgA^+bdb>PDx>51kjYc$8nM~^OROsMAqlQCKNLdqKw2bCK$*25gvlCB z)^0}|Zn@t?F$hEMxtD!{q-eamjeYy+d>llTpGLkGmr>iZvR6`lJyZ3K!oKcIN~2@p zFSVi$w&A+Nbavbxhf~o%W~UQ~rHIx*mRNt#LYY$(aR~9P12MsgsEJNzpg=|2N_~D> zlNDzFA4Q%cygzIZ1g{coGNXk{{~t14sp^4AQ3ia8NeBS~67< z?5|(fZuXiWt?C<75!2mC|AeP*bNrLUHxK2<$jSMnbU=~sLWP63aVZ4Qo5t}_PyNf= zDlu)AJq6SLT$amL?4d#nhE(nOwTXoy=ZrtY5#xjQ*Buefrt0G3q`tI` zud3K!W$gE0YfDQKr|dYcHV4`=-XDZlxs$H^>x|`}h&n$Wvauy2FyMc+?vBXzp*1bJ zSG;CmQZ>L*A;|Z1;6P5~57BBlUqaEUj z4o&p#2mQP@T}Gj0moC{k;*Z1NRMlCFH%Ar~Kf|t}Xjb?oG#zLhz;dHILM{NokFn2~ zl3th=Y<{1-!6%K~_i3Mjdwk~Bf?2ViH5|+yQ2K?X^H8m*_Z3PP%0;m-B;@HbgMaQv zG&+wcLoH7kX6}4J$Sc}dUvB@rPVDQH@SpB9H=OLN1Pf5}#=h3DfmX{O-$k@Fhs;J! zy8N#gkKIygzsum|oKcuqLRpx9#oT()rCJgb+#}9UJ@B5-)m^~R5|#vt;YaJ?+c(;3 z2*%y6aMwqauEGTNB`)Cl2XiU>fFPnoq0g9IEot_5^qo3=A@SEJ+X#KGcZYC4Pn?N* zu--gYK0f*1tcI&siEuDoX#9*tAoxF{gM7()7A~B6WRWRR=XF7-$2IEj(KG z^_eb2q7*oi3g6z(7dARX92M)xNexOGAg6Fl4}KC1ZmZm`lEq5M(;0$rR6E@UUhm&J z(`5?s@^8bLI5&Ed-oEl^+V0TPW@Q9*uTs-iyc zfRZ&I*M9aLAt@IVXbf`N1^+*s)%9Om-ohCF56AtEexUnQ12753*mQ?Zj*oXa2DN6y zZS>`PR*#M-ds#!NL5Os;PduAI)2tg%^g02cbEB2Iy=siR5MEl#OY-u%=iL7AdtWe$ z0QQwSAP8cX2+R@)aGZtQ7>^mYrI}=o74o4?_#Z^RDqO}W^-r@4z0jdws zp&^D|F;yHG6VI4`|Ni|y?a@Hqz|GcBU{!_#EJs8{WaCKH<|%o+(vjVMvxW{7+;Z_6 za02K~0Rx}I`p2zD^{+rTsP_m=>O0(d=`PnF!B6!{QTm&7(Qdk6gp}O z1{!x^5`MPnz);0+UvDL^vs2t`68W9WhI|W9f6Y+}r4n?5{wIMOP4X^U37C^;N%y)J zP>D$N6tm}F53uING4NN%+h)goi|^pGpaJSxQ(8u(+*ZxQLPHZuOIgYRpE=Nr`y(eO zFhI=ftYEn1gtmIQ#(a?R?c0eoYj{!!IlAk{#UyLAw0avxz z)w2}%_|h-DW-8D>SWkF!9MFF}UuTUMZhhLCl0w90(DDtahn{1Z2^PKs%GRxJ&o=9v z*JW+2tc(#&dcrVYbmDaPQE4qL!g7Pw!}e z8+rX^X=&-t8R#UW=sewQY1J_C@&XLqSW87!8Tmh-=S8ZXJ|-@G|( z?(JXsTUORfCua929Gv6AiY{rqZJ#kzGol4(;}fEZ-2IoH zf#~#w!s_Y-z)NL@6-7&aUuyIindNgNFrpFge7|Cuiv;%8FRR8nn`>NjK{x6Q$0UkG~h|Aop96DM zC!-hiw@&96YJVixic=3}TU_I^cT^}TNMW`inHV>ot(o&5Uyg+Aw_==dkx=(s6kt_F4|0-~?)7;UC(;XZ&37Ub1D}REkp0M?objngjr`hv{p+~8 ziCoAdy0k9=F)UC*8W_fVi9n*kSdfGha+-qQL}nzyg8DnKzF3lXVWA{J(Z?re#*CdB zA>uuWm6es2T++(K-8q|@<0xMOQ#*zFy#e1+H7Tl>mseX>em-3sNq1_l9G;*QR8_^t0kyM61!-xdghr6Um+0l_tNs{91nCPP7`yJ*sRMsf^>WW^8aEdoG}&`-^m&x0s^C$ST-H@j@jnlBs;s#)&Z`(8uI%f8fPG=S#6cBNdG}m z@$9KZ$e$h?DXJ!ufIq8ID zpOl$I4`&D@V)AEb!%1OmzZyVrq?E;oedQWx)_(^q4u~|g%Y$b;db^MBATYn>8(dO> zr6lIk{TPEexZI%aTe3||LMFAz^Km(<>Xa}}p}@IEpkLZNVK(qa7)V!NY$U3TwDRcOb`3Hp}2!a+@TPc>tEsE%ys1+zaerkZd9f24V&g zR3va9rkZ9t7j*!7hx$^jIuZhfaJZ)2jiauQu&>?ta(5*tCx0Yqt%9d~-QD=Y^x&`j zIndJ++(#!YoPmsJNZb7v9P}cz_x!o}5&E0wG*CyXSIw&eHP-?#%8rpDM%*0yt1aJ$ z+t`Bn(OP151#z7Mg)9mStVMhZyCYqzpu zKutCMD2IQy@oo0=E!7bb3OH5*Pg~@;{hmEL`yFrtxTFbJH*Kj40|h20=z)M+YR_H? z1eJ4VjPGpKdu?rP&MCnOD0#M&lx|BYEN#rFWe_!;cLAmjFPDL49(e8ILVFn1L7${F zS0D2*O)NGfUb5dr_8T8hfx=>}O7u!1j4ssO_c(})F39*IjEfK&Nx#-q zj=4VGA1N#*Y@qazfDo7vgz#?TkS3lJ4Aw%`8O?&yWyFq&AQ8gshfvRMpa}XU(9vwM z>{&SgdoR;YDvLRR$4Sm9TfiD^CF#RXTM@bik3j@&@)&{(i#F^vS?vS`HSJ5xQEmUH zx&9sS94p9Ab-9j22sJt6T*cOkz284;leEQek^cVkAb3U~oIo-p;zJgW8Y2 zCCQ@GbGp8K_2O;n&zG6zlN@0LBW}~K-RTX1O$V+@cBnx3^|^+t0PDOlLXukjY)lHf z+Jq~W_UWHx_NiycV9Y*T0ov;f;Jfh;4170=8ou<8etJ(8>w3Vjc!%vErXb&N8G?$C zG+x3Ph8nrZAPu6i3>m##iyh+%!Kb8aGXYVDkOqZU)zd>y!f0fFS4-;G4Fqm z8y+NO$Zsvn!hM=>!usb_)v)Q-_0boc^vHg;sc}`1&@+k#%de$62{X1!sVxAPXC&!D z2b*dNzSB6+SW=P38;FRD>FkX3;f15d3UlX?rYanu%GiMR%>sVUnmsdO^!kcjPHE_e zpcuQ+%m)g>L%AY1IU7^4qYdy*CnnU7jD!M+=+o_0|CK8l$H+gedYpPc11rN~n96hK8mM z3klgv8w5Jrc`>I6^rF%9m8+35c6PhyUzmWbWHfbRpXxVHt~4|>J|V3ZB7!J;F-<;; zfS;c4DxcTZI4~b#x#8}5iK#a@#W_U}lgr=1bC3psa;MM6_&o|RoWz%QM*Dbw^-XTG2);ET=5RZvB?lh6K^QP7WOW}&^wna zP8aS`vgx}th^Zb(OxSe_Wb9CX+y8i$`VRl7%Qz?(E+2W{_PQIffavMI<9We`8lD(` z2i?RC4pE#%M;a5UAGrtR5{?*Eo$$Ylu>%~o@Au8RWY?8sh6XmxsVmsF(@jn++ah1R;MRqdaY@kc51>xGi6%HYFcyYUPC=B9GQCl zcH3oImkO}bVt)au;k&a6UC||e3pS(jrJ2x&)8;_1U@WREQYbBV5@FD1CVZ|UE8RxW9UVfl}8dmxu3L>bGHm1_pMSXDJNIo3{ zhq%MM3?n*$LB(#d*U;}^GhlO#c`Z&C%?jv>`T|g>kttYGPLwd$AjInQ;@$sZ?JRiW z3g0)4LyJRkhZc8t9iW8*rL@JpID@;pGnAsm-Q6jJySqCScX!ys@1IR}v&l-bUtlJg zoH^$`&wV}Dy)@$R!M}nd?JRP!$7Yq=ZtQ+>_BEMgf`7e?5hVV{0P_F*QUipX2>(-t zxt_IWdXyR8L%#2M**GSzN_PfzxN45O3d?O)PnC)h^j&SUpPN}x9A+t$r1bjP}G6-?;-$;eevLwnfE<$*D zsZV5wJMf?hEpPlyiCVJNSqTQA1$rYBk~ng*C61KPlmMrsi=Skk5mI!d%)aa2-lzC;T~EETjm`EvJw7)6ECbXVNe$v?lL;M z!+9V6@O?aH@{xz?7HoN_plFFa62p zZfZlk3f0hVa7D0_s_=cJV7S( ziL7Rq;>zLu`G9kGy+ESbohJH}=tt@_>~Y29{6B?dOV6tareKR~=@If+JO0TzhZRzh zcv%Y%_`PrQW`?*%S9Ni5-wBfwhb5X!kF)S}8$(pTP5(-`F&2$)heMhO@=1M%=Wb<9 zQPazLQ_Jl*gJW*pdj0}w4FxU`TH>b}2J9EQEAAU$U%uY@&NNU&eOidbN{VHSKFDlMvh4NP4h|J4`O$w=N70KVTR;2E~&;_fUHQABY zVzEP)A-wX5cpFixm1yFWwO{@=mIb#Sw7Y?gr++L;UA%mih2;WEYoV?$T;ym@+quj` zt%VZJ|Ms7CyA3E-G*{HzKx9w2*IWkdh0JLFD&Wl9JSl4Sl=dp>1}c^{6o8}D%CA3*y!yZ^kd`sTT-=N;Cau( zbI{qg@R7T_EV3gJv~@ckpJbGb%BF;%h0ixAGDT+Z&pb2lf|Ds~c*IU!LEs^PzyxtsXzHbU+^XB&Ud#el^ z3779?PfCO9?RZ2ZEA@sdFo#I+O^Y`cyt`?QVbF_lRG&Ov{&wDlYkqg}O~f5PwPDY= zIK(+FL3wLe`J2NkMd81Gx`J`<)UAD#Det--^n=2GfAR}XF}*V;cGB~&R}x}scD9u7 znqD%qke@-)04ez0P^LOs@8@JJ?E-JB(oj*@-@eUknWPs>GI09p=VDRQoRGM9)Nh9i zKkPi4eA%XI>6-eFQf0z!sxQ@JSEL7@%YEaU$HmhqlGQ4>i^7}w4{+tvUt`?I)*0nB z=I{`y00R#V(=*g7D5Rqi|QYo}=(QUYwGX*Qu)!Xqgk2 z?O@3~L-4NEBEsgPD9&zFnvEW8B{n_UrBzgMDEtl;@*%$Pr~ri47JpwH5y-3R~Fg-zKi)pJa|DB{^;t<#koEW z2{XTQ24!tb6zYSihy>_wmt5!%1+ z?l9vyyXh$(oS_Xw(5qZYVW47v{UXFxP*_umi@fYZegcV$)vt*$_;vN&ny@Ux8_k5R zfB0jT!CTx$@wniXs^o1}EeT{ybshX~s;lCn&zhziHPYN>c{Jc$t_pfd7 zB2c9oKJLVZ(yy*VtV|vNiDtbb0g+doye7k*0VBgbJy^fZ>o*sUF=qbJdm<4R$Ftw+ zfLW?TQUGy(6G_UKOgpNT1+phXJgEa6na5(Wsn=<+IOnkCv(eIBx!d+LShNz-^iWKarlKAdeDi~y%HaL#ET~x`M@#i9*LE`#kxhNRtH^vz!DRLbfW8LMOGo5;U66Otw%V1i*k?@du+Ij?RczSffERYhWag9iCq*S0S;6F48@&|yLDM=*P8-{reWdsu zM|hpVD(z&>U;J-~i%9w6&#fEhiG!>mC?@S}?kC!uh)7=|{o!lh)7ofm%~gJL+0iyn zWj9w?$gk5?5K*XGIoX4_>mke%01&InZ8c^jmnIKP)=)t&DC8P zQU6Z4Io6mIeh~cP{WtjsRwWVs(v~(zm#zUx!yb5L`bVe`6LX>z>QH7!; zb1l$V2Xfl7=uNEtO~SF#6I+%W&LEa-wH4O)1uhG-0f|Faj5%}v;^MAa z%9iAhl<`kz2O6enI5LF9pOQ5h6v$A8zq+I{2ra4?&=qffUY_SuybO0ud=zI1WVXly z!Zy?1EMs$U)ybZD%E#kxy{dzoohc1Z`M$ieJGdQjl$cVk;_wRTSx;2|QbFY*xDrN>axJBUy%Tz< zwlw5Tak_w6^i)D-l(lDm-0`F4bZIwRbj{{oIVm$Bv#{Szl(OLhzp?V8bnnp)`@=)J zSAQ#fKxeOqKsrv$S9)$vi0F#}k~D~}Dww$S1k3f#G7sC(WHK*fxrUE6R59+m+5!${ zw23td8hk0bctrLl<}d$Exg>VSOh;xtsWruW)7zitiw)YIetX%Y`KK+uqkk!DU;Y4# zFtX$;$-A5DDt*1y#g2=mgk$JekBxt#MbMkjZb$trpqO%!wrt{CWy zRUW<3QX3btn#n`i#{uD_!2^Y)A7Weezf;fL_&6`R_>uO1I-s}`d&_=wm=QHsIbaHqA z+LfD^ovQeBz+--TUd3vz4Cnq@_dDV!;BBo|`}@4C)i;Tzv#_`*?CnWoFe9DPMxE7% z+d!b^6>qcm;-=`$X5r~^6F7X$O-g@yGJiaj#5#-ds#<_I$Zg@?eI0;d1foh{F-fSy z!ddBuzZ#t7YkUxocXSfsn~ackmxfk^8#9b@{cHq>P)L>&*rM!Y5yYxKWOkeS%JLnJ z*6#aRH-5a2;I6!JqpXOWkX)EU@!1La#VTCae=|L{Kl<6h#hC=?FM*HrUt~7OdBZ)& zIXm?5QEhd=vy8-I!^UdfHXN4yR|w5ic$QGe*mj{p&A(hX2o~j3R9|OG{(h`P2FOi? zBs?}c@TGv}fZ( zToPbkg%j(~e7Z?1G$#4s5c&6{dJWa*Q^i^=mlT|Xbwgw2<4|ggbmB_6*s=HoT}w-Y z8OBP^uv?iA8>eARjK%7QVk$CHA6=M(v-3wC*FF8K&fgM?I?>*ht@coPlvI1!vs*`v z2KkV^t8nDw;bt_H4c(%n$sZ5hBUQ_!3^yqTQz<@q1?F=o%uJD2?d z7)caz7e*kfSin$a*jBxJUjwv!!vr5XU{!R;;6jR0wzVtGKRa%4Mv%3es~}l+|k?RL7!p zWVE@M6d3||TD>kI42s1=R5ja1?}nSShjaQwJ9`_yGynDI@Cl~rR)t8%%>wc&?o9oc zPkGW^hENvj$Pk#8&KP9%@pw;<)y=06&>EbAwk7X8AT&( zYg6uV0^A*2Q^Ivymk}%nl$dy+&|9cYvedQQZEZRWecjF7r5yDq_AEgi;H{VZp}Q(R z%~N|=8$@4@e`gFr{ZYEocR)2HX5OKNAqFh7+B6Vv28?a`7v0Pcga_VWZX({ff=Kx- zMLVMv-(3>n-Z8`oYIpF>GYu58*eCrMCG^@Hnbpwk? z`Pp3yu-8T_-qkP5@L*iHE-oOV?sl=% zt4Z$ei>Wyq!aL5+*l8-MoJiL;$-6$M4X`()p~FXx;jtXr-t3kY)A#tO1<=gZBU^W^Bz00HvV#p_ z*QUQ|?e;Ta&3bxmzr;{lI%kj%B_()`j&+)K!IjmQFT@R^;uoUI zZHVP$ch(@p8qsxxb;30=DRvd7nhR&<;Ex6*0iBJ%N){*huDhiJ%%MG)@jDb%A#D4| z6~{WG_-QJo6-v0a0hd>l6d09J7ZzKd-K7>rqr!as*M64o&SGSn_&p#sNZY6nmi)`5 zqVXIL5Z=l&QqeFuM zuL?BYrQporkQx)^z_}hP#fF@`4c*@0DH&8oRkbD-`b6}uS>R3pRa@Z1la(6c>epW5 zu_eeF+sWZyD%8=SNO=O{7^Uk8!4fvd`?V9?M({4KD%#Ib`p75bHZC;e_CpTO9VMRF zJ&I!*bFKQyx6?rke?i=LPI{bKv2-tg>pD>wj3Eq6?M@m?*ujB`QtmQW<1eNt^@wP(?NNnCiT`hH&H1TyT?U{1FnoXDfAwt>S zRkg>=ydA?@DK9PEvAGXUSIS#k#`h;&-(jO2+Vo?Ou%EgG`juV|1V4qMHefa~P+* z)6OwhviH@wB3VA~^v8TVIQ)FYr25suoN>BTOPUO^IaA!oH`}MwZHBo>dw-%HngGd? zEV6gbE-F+hSQ!le>n^noWZ~ZqY66OAz53JK4>F}@z&334`DH-2Uy8rVy!Ii-CVZuM zAiqz2C_9_*IWDqLqlS=J?(9Rjd49q4uv~>E{Yq=xMP8+aW=TCK)z2nP*FKc3V@&h! zI)>?OltX`Kiv#P1xl|H0QMbryF5g5Exq_GKp`H?4xs2mu!mFQa#ZY*x*s75kvkg5p z8c|TGyx$d>>4=Dj2s^5hNW0gM!Sav~Ur0m99^^5s3~3Bjjph3Y!R$6p%)Iti#E3@s zZ#`hFiGvg)1dri0*C3m7f&zSG2Nr(_fU!(K7mwYIvyCt1S8OajPtkdDtg}6D8UE(D zQHexNF86lg@7>y#{NMlGVCr^C1+DvJwZ3;`M*hK?6D+3?6Fln=V& ziTt$@T_l?jwWCSr4YS>7{s8#oL57|%(56W;8(dW1Sej9<;M&+H0{^Wr`;uW^U}3z)m(fX6$4_GjwMGXt-|W58Ppd>pN}Vt zb3J!o+%u|3bWBrlNSE-D%84`Am?9%WAYO>-tMAU^U<27*CX632;~TMl^@xv?kte}p zl9AW0>=IdixkEb(BruKY+XdlcprD2ouEEBq)Sw*4fqJ4z@kPHS&?rT^k0}?>qUa)0 zZ@`5Mg$<_Yg4f(5V&E2T&3<;oNqv$b2;RDiJ0?JdnlEWWg16UnTuV_-i{W&(q20&a zhAW)-z3nc)7jt2BL*;tn!4@nz`f ze8*h^YJh#TJCCTup`4WaLNW&_D@p>%|CcfO|K+58U{P$P^thic_mT!s3rOW&*6!h9j$toE*~*IbYAqGg96=@&-*Q!It!!gcVRa-wN}%f@ z@mkdjeER8n=N=%V=M4-DB$|P`XlQ8A)6n<>0+mPwf{4QY&j8Zh)9qwt%B}qQ5jHGL zA?t86ezTdByLfX%*zNImt5+2?D{NJnjI2IAEKG?gnFw%Z5X&FqT?{Bbdbbq|9Qitf zNfzal;#H)-M-FoW0e1Qz>xQ<-a!oFCs*ciGc9UW1-u7v{;BuC*0xnzsTVqI>a z;@aV$w0;86?O0p>rC&P}ea-BF#qck(va5)1JL?@ zQ&dz`P+L1a_Y=`TTU-0JyJN=aR8TQhQ2J)c9`os{+&gz+LBoQg!E`dk}<9(sMwv z^XL((!2faO!0$d1ucoH1FzV@|+U>|#PO+bzHBXU8r@B!+HKxTr57{$|thac)41%h| z7EoQ*;=3@#VYb<>(;%o&#!XEp}w}M9tgeSD1_F?@xizN59#f`IUFh%PK52f#TroL4Wy`b+G!oM|;1; z=KV=Y?1}qmY2IXg5%&y`?>;{_tv*5O-EY@}`vLfBo7!=s>&^X|Kj!X;(8<4}SAs|5 z71*F^eNJ9|47zcCW%aDRA2jcQ@{~sKYfbVoVFD89q;^(k)AQArbO|Uy0^#7`N{)6q zfHLqs0Bgx1v1;(WxdE!rlQGBye}ehK8UWby)YBA$Ldv3@R+It!;{Q6i%b zA-D?pq+v>)2qKx;974Gt7xeyh|bz zGWeF_cy_o`m&oyo_h1fwvAc_`!fNNZu&%gzip$4tD^iF)y3=-_fl>6+u@2$jzSj!N z1lj=~vb}fWq*O4MtH3p0g(D{7$%?#1tg5j6GpC%OP2%FfAXDF@JJ+zC+Q{H?^UP#k zn>KIHM=KqwVeJM<*HN<>BVu+0ZPn6jf2WATvX#1jQ_o=b92*b1!I7sIfc$s@u7k91 znjghND_mA&&StzR&pV?md917m{&~Lu>{dmFprGJyfY?J_cAV8V8Ac?LCSCc^v4)i) z9gbhg-U{3rF0(0~LQp$lpi{jR-YJL!z|wP_<$TA1mgDsI8pB?UW8B3Rz!l;E*o30P z!%=~oba;JWh9>|*<@+OJp5F`&eb&KT-vNwMF#whpyjCgZ?d`3zV`({8;rj~h-Cv}? zdpawKsFfXm=P)JrNZ_2l+S{YLhb$!OEm4`Y_RYsYjCS^%d-bhDwi!Oiup^zL%DX&N zzjD>?1x9k9N^M_#p&WdyzkDO8!cHMPAmWW8Sf%+%6WqXn-t!aLX?_!-hI8AtFMoJ@ z|3p$(FUa1mAn=k?R`K&P`>*@i`*IM{5V8k|lJPXBkHbKIZdGIkLhG?>^u_K&f|0J* zD}H`Rr6+{`eJgQGYc~(_aB@QN$RsGZ@V5jC zO?h3lcH7HMxP=Q`s?h0TVq{bk=COsxa&p;v1NIg-H`!oii6@|jQ3|GnQYHanOR?L?rmMWGaast(Ljpv4sdyg|%$^jVWWHj;f* zJ#F+Gz#6mRx-1Ar(&w&?!$6e#_wV1k3l`kcbZ_gD{)IInI_4s zIjlqhYa$+A4PkTU^y;=a`}&?!R<$Ew-Rw(b8&i5Vbo@R@X3?lf%%xX`le4;C*7fF@ zq+V03W#1i;m*UF>9EHmt5tRC_X=A^uS@vq7Sob zZz^|ta@RSvV^`1qX2K^)zGGz|3S|_-U7hT_y0o|9au0!nO-+Bz?ih88d&`kuZsN-? zb7O0s!7zsD9M*;RFKR}XI0);)4VKi;b_0UO|Ug)U1uYvGGh<%;81oe0Je z^%N;N`N#^NRjH&4sSYR z1P{h?N=hcMPq&*<;5y{fh}w=?0N~U=oy=FV=Y~;!JA}RcmI8!l)pW#r4;+Dd7Aetv z>EXVjy0PP5I^I@DuVHEXgk-}#8;H|8WmY2_(MKsL{l+0emUX^nr0QA7ms1^vO25%M zJ^r}-14K8-IM;+89$}*IYl6;3l*Zsw-=GxA-G!KDDWSg#_UXIoQ*PKWx!dgZJoR=(cn@Uo}kdTsK#uH z8IVi0aZl*yCFBB4BmliX#Kvn%f8?8A>DbK|_A~@Z(R`#^AY|&ebc3hbvcWIC)UbXk zL$0qvv|8?5X(E3VQ{O(8FEopHP;BM>uogV|P;X}Wc1c7p2Y_qrcd9~fp(*Sm&Lq-{ zw-WqMY5W95R@GM?k>c;7>t(zi0Oa|QL2B*{H!I9Zi!f5a*5#-VoNa?Dt=s3FJwcj< z!NK)o4IdA$vVrOnoeeLIa}4zO3gKa!Hq${`xRprpw%o>oY&Fg+Su9C22FNHLAbmH+wJ6PexsacWTD0R?$`#LZj=&A^xyd zYGkZ#cTU{*od%5@2Y*nqS=;r`eFuZ(PLmf2>MnV$qHSpR8fs{W(V0J8E7SWV+Rxg3 zIs!hKJ{zn7|0>bQYeXr+h3_u~Mh?d}>R1(qqBS9_&L3UZSLAADzIuFp>;9OLSsA)5 z^tZa#n|_*8@!$HWjFf-qHk(~OFobh$0t-AhGL}hH( z7J1|s$#~;w?uJj0*G>C=(pPFvtIe5u)y`uuZqEE7Sk8NjfHJro*6`>3Qf`f`>v5r+ z%N3IJOr@~ffq$j&4-qq>UKi2B@6!|j<@3P(tUDnt2&2P~Mkj&A3ITeag7?GrOyqJ# zb>?_edw!-a`hWdZ&AZ(N+tKCg>+8Y6z1_90U`TS5Nl(ea5gm9Az_`Zipkx4D-P*ds zSdH_%cez=e<4s;Zrw_B%rgznN}ZWR=Sa3Khk1f{Sv zu-}g@W=hsi;6wTCHVD~zBB6gp=^%?6Gv&IYnT|GP)DQzf5USps7Bf~GyAb(%5P<;L z#X_0mQ%b`0q^Y@qg#S%pU>nsJU^r0o{pwF{7Xf+sk7me2#!WzX#V?vEOC?L?d5m#T zZ6iLltQAq9wtM!?xDF2lWx7@LAH;(TsufUBnaW`;j8!1ITDe$72KCr<`aWHIXlv-L zK~7#jd8TII){B$c7ide&zmjo8C1=w)hczB{vF12Ct9dV8D`Z7CnGbV%<9q6?_LG!E z;l2+J4S(p9digpW`;QzJ%6~EqVD*;)SCP|0eUCSOHfKPeM(=VSZq(6!bN5ioFPf$2 z2Q?GHaR)1WLCJ2c9|7G|X$2cW&M4TaykKJ82ukw;kT+1J=9-+5L5a_}bRJm2!8mBE zNem+G)`g)C)GVf)z_Je^wVOe)$Yvy4>$m|#aQ+FeESx1j%(wh(?@~cB>M#!>v{0a< z#=e`+?j!^$dw9|`|TQ)-AXZ{7F;(7z`o_Aozs z`=c?AN!2c$UTspfzjI8y2DZ1P7XbEWCqbk)5a8suOqwS9OdjRY{N=QZCm<;D`&>sH zDwOy^+p~h^(k~I4TX@1&XDMLJZm}YdSWFmQp4Z=O?{0a_NH6;X(`xezx{P8mL;_2bWUXe7@Xm~QXFN;!<32t)c0qDOlYZfD^ zw`pG#NqA)NX=^u>*@w9a&GvSg386Hl6!Ed$aPbAoCZfnPtd)e7*AGxfuEe{o{3uhzfX)k8)`fMvd2VJA zpJUZ5Pt8LdxAiq*jU)=${L%F~PIqyavkvTl_UMag_{=WOO_ljyxIHvqYqtvC@rh== zTY+P2s!Xd?fIp!0E2jfmzS6$HJm!R@MtGQo#HU~+`}#$fg2&gg?75U!e3z> zFm)6O?`c4ioxnZDp-*w2Km7*4O@T%~)(tm}zYBLC=(U#({`^O(a(5mV_8NjQe;hlxsT@B#5lG(%ewJT}|Zz9PNwx^rh zDGa{#_nDj?u-dJ$e>{BB$D7Q;WYCM^n_se+@&?6HUY^OlJWx897wket z(-V@ui@7<~3PL#X>mk&1us-W(IAcB1&(5ESF0m{sEv~nkoLL#sc`tf&?FO75;q6FbR zni+OF@93Yr;;6|m9_E3}Xj>?>EYlb8!$ij%5Zh@TOLJ{Ee);Z*jyfFFmdfpVjdMSm zzuDit9Q>nvv4r)|c*W|RSytoUhJ<{4c^;;$$0zq#0uSC9@RH1{e`CaIh1+b~K;x)s zp14TX)N8;}*6!v!TOeG^wH1iQgfUjpHRJdN&*t*!qo*Q%b-^>#w7KFn7a|~wX^VIi z)~!%mAxeJ`=5>j_4`~;z9WFr}RGUjq8iJ zH1GQ5SeRka@EpmZymXqfn7JIq>&ntoEHi@?ZFa&lX6&#j8xYXNQn ztHNKJ-%6J=+|BvsPk4nelyBmFE%{0`8r!}7R{Qogfh`I2**PdOo6McMP$d#EjT^cy z6&y82pgXeV1j2zn!S$<$HX=c#YZB7yB3WTb4;+kQ-;3z&&d@JS|y9z>9M5o+v(bP^Wlni}w zySIlHfA6x0ll4r%IdzUz{*ndVmTttptUeWRki1IP(EL1G@Z=Fs4pPPwxZFIQ=jNh| zsAu|mz$$>PAcm4I-89I>pn4VQSwmm`gm0t<&Agt>fIfWPiY7>6sfAxQUtxcFba!4o z`|rPARPw$!LonB!bI4UeX!X2i;Mma|*{6$y23xDUNw&P`(R-GeBP1N#fXPWW5iJyQ z5VZDa=uQ(eJ(4#|!20a%fB7YzFe${s8H+eVK6S+oQ_ zegRWy7en`YuNxe)kIK^^7?$lQll7xUT&~g#9{M zC7m=osAAp9#gxbV+5RH`o31r`Wr~W*UIMG71w0s>Nk2C9U#m5_{pAOV{V08?4iVTlsZeMFd}6bqR4f~Y`9nJ? z939R5a#J!)>;AG3&FWv1fu_?lJYzeX#&%^I@R`mb+Q5;!wiwKV>DX}@7+Z#G`>$b? zb|Y*X+^tr=8-j@VK3YNhZv*VT}BDHt%3^>3JnnmRWksirl2anO9MQGi( z_!oLC5xN2WU<%(1#S(wF*DJS+`<`X_O4cT?ceFQrWMwimy#Bn#cw zQ-@S?v=};j!D7T}wKDTiVZvtj9WH&aHM@w+)53MiSFM`$7wu=wzD!#br?ol}<`S3< zkxut2s&QF0Y^?Lz5!0mRhhboBfI7Z2$XX>u{fEZYw4{VMr) zyzJI5;svqFc#@62%Ia z%WHq1q>(K19(a9#dT;t0jIl-d_}Y!r#{3DBS3Ik`cRVd_KEiU6i*g}_f$@R)1d{j` z-0o}WS8uhC9!EV=&~NqU|Apc2e?3^fe#ZY!5_ooz zQ&t86Qa=V}=Ak)Qj|mZJ5S?#H+`9Ftu5$0U!AWi7b=&G29r^Itk&W7h1l)-6+oGlRyyD{I zq-4$8TR}nOG~7Y82>D?0L?tcc7Dhp#Ce}LvJfw6gwhb5OtDuQ@Zanh|UIf(vRuLK? zrDOa2z4^>)`cyz^^s8604`Rq(tnC@IQ|(;xRp^Qs~365)UsI;Z6|`39`cD?Qj6yAov+mhZQ5&cw*!i{pJR$v9}#DUcy3D%8{5$ zqMYJFopd*?vYbZzFz5P!$;WVjGWxqJ3XN_d;Ee&A;M1j4Wrs40J4_~>rA)@G4O8h` z1V!N8HI$NR3hOZ88O%oFK2GbO_WR+ zQy7YoPmz0U&(6(?l%nafKDg}Z>V0i$%C4-@Gj7l6Hi8J)-xA!233Txv`M~+h`W!vo z1PBnrV`@I$%ZL=<(S7kvQvuvOd>%nFKR~&lSN*x+oT!R=WqMM9!USRb1vOQ17BrMg zv3RG2#~W@HX`4f;5@>q?E8wt;^;!#aNc6lx(3%0P!}|I8`*gzbz~InosP6(azQ0EH9%m9Gc%IY_?ym`9Y5j^^XE5f@ z(XWJ{1{0$z^**d-Ah8+$`y8Vr`iX?kN_YpAXhsuA7DXOUJ1>D~D5dvlzqov&-5;rM0Hl*SK0Yu>{I=BP!5oGnM=oP* z5Lkw{L$HwL@$vD6Uh%`F#+aQC^z#u8Behp2pWxUiQ#{2xqy43Z<gvDLuCM0A$x7vz4XaI z4B$^qQw5Rj@yjxrIX=byinZ%uZ;`26P*v_{ko#)5nt5MRRL64&Arou2533~3CSVu$ zbC#V5eKgQmUzgo$-of6%3zGk0L!U02-5%hX(OPBq;O+dJ?1#*a^2lqIFS1_Q(>-8X zn&}$8+_&W6n(fum&GA!O_=oG zB%-3C(C5u4u@Pyv+cm~x?lRNM1vn)DutkYZsR`QF~%KKoV3vV-Xe4+9te zBz)QiFRTkxprmZ}a-s-O!vb6YH&82#Kld`aqVCgct9|sccXRbnuWxKspwMa7r*BHQ zU_W=22xE%Ncb3?B8A0GZhw|jI?c^5$cdOGtEwtf^lU*%92h7?M(QZXO~yN`G!xooZhhkk zTD%q14Sf{JtS3E|0fU5jD z^}Q1Ty^)0DEAlm9ML`1*iGN&kpz3p@oWfE93!&gIbO<*hX|18MLE8>43(YMk5uX6< z%NXF$1}lHW$H$ieK7j(2kUi`+=TnSY3A>`!&tKQA2W3qKIRF(Z^YiEX@J%2OynX74 zB-uNx9^LklZbC8CMWO9dmY=3^@hc`1bSP*nHT1Rjno&0y2M#&rBQ9%x;cIKJ?wNOD zBZ#E@e%Y6oE8%3EbbL!T@KLZ&5+9zW3A8}~BoZynNCD?LBCu1T2Y-pnx}2yJ%++gR z$0+UH^mubBt%<2cGI4-K;L{=*dwJ0*&=|;!ArlQxe=7-eLfC;-jE8b3(!wtWYBl53 zlHdp}N>&f0TAEm-r7#gh`9?3BVfarH6IS*}N)`vEbVHj=7NJDWOey0$ z?_=_s>~sxo=%P0drBf7O&5*0qw61nONO~f#)kU_GsO6>9DPQtnjon^d%u{dr!we^W z!tz?4D+y`EIT+rv%Qhv^&HMK7P7W;mm#GaAw+siv8K^;K{%zC$@lEGZyS@SBU5Cl% zeE1N~qCt3g`1$tEY3f>3EEI8mDpDIeEcAC(d+ku}0DzRyt;I4>=)%RpX^1jKE+elO z+iRC5g$sBYr=tDXOEVl!uyg{5jRd}kCLaw*vj(qJC#XUSMMJTi;E(a?Z8h=f0K%knJ{Bv`J{{Yq!j&B6T#~6EO>Sf`Lm!4npBO2VhjZQeJlN)!a5Pl?ye^$7}3E8y&hHM6nH<|zz4lB zvg!G7NGN~(7i3WpB)^SSI^;6!iYIwo{aw*}!(bo6(e@3=k0G++)bmlmAF*iE3BxLn zqhQqI*$;1Em@RYj53`gFd*^(oFk)7Ig$WrPrbUMLt!0!mG3eG&ubTQ<<`74)*V=$$ zoVSH?BcVujfdfRE~c@rQRo)(SK zo5CMJ+F=8IJS5-=eh@GY%Ccr!24o8gNN5?=-X~9$VA2X&3@usQV#lTT2LSF?e#NwcsMOI zoDrn+`L)L2+2&NH#*Gn$*LTp|R5#E>MqYA|lBFa+SL^AGEMGPkWHs9z@#34JD@rj%^E|gs8#$uoRZ=fE zI~bifkZmVNk03YL!#ES0wvkyZx)s7morH{+YkRr7wV>*Fq+$O%B_zhM^~v;Yq3qOC zD8LrKJ>ex=Md2id74fV_ns~6;_XF!!Q;XjDV2=JL0T&$g=3J)W4K~NTdFnXug(qXz zg9+#P`mX>ZxT!~X@}(`$oj%fjBQYjO$RA6OEC8RtTWc=bk54D#i#Q^|4G_Fm#RU9< zSLlj_D|Oz$avs{Dp7WtRXct6y$4}nl=SAsQ)?6Vn(;g}XwOOslKFrUJdB6oqV+d-hBfRR*lpg z`c;K=t0;M^tW|F}%=^rTfUJk%xD=~?2xt2l<+B2KwaAnv@u9~nQAl=wwMN(4jj*@v z7W*e>iIA(M_Ml2OHs^ zNDGk3$oZxH==s3 z-5eF|PiDHiyQfWzzIN`66nx=w_GjbKw8+s%>!h8KZ-N2_b^wK}$M3`sOPX6hO%>k& zA*0S8rLeZ>O_x)p23B;@NWoB?q@GJeH>Yw%wA4wOpa6lb38v4Q{!kNtBfoYhSb=3U z6oXVC^=4w*8CJ1f{ra^p$Q>DlD7U?u&=+orN{mp*ftC6hcsy%5ZY-pNCO2&-wdyTd zZTz{xxyS3ENTzP&0IVr^+u;x4Z|GJOLM!qLfTbP|g&GEOyVPc7s0C+cxu6N7fON4S zL>wrKnE!~FA=uy_sJ0{uNyv$%6&gOE914;^2TXa76;Q!I+-~?3O9KXYZo7Bl^B1Cj zvdsS^mvc-<)p5Nkg!JXH%8)X)c&OU1M@cr<&FDC%P?;oEM51w6CUX~fw1?M$S!3d4 zawIQ^I2^?UGcjrKU`?OZxuu6X2~iII<*Qy}r^OjTO?Y4H72nKg?Vsbr=JkGZH<&JR z#g$#^?L+XAp$=HnTVJqI=`a{aU~;-&nG%BLu^m9#=B+Vb-W>%ZWie#{jfa2UwNI=;S&zX9bT5Iy=OlO)9Z6~@YoJSqTC@T zs!Xk!eQxIC$V}ZdlA_Hx8%+*-ig8OCS;y=H!D4hf?-yR(w%X9Nx&0CjcY+KQpb;hHcD03_oL>p=Y6M z7&C((B-+~A11l@(N7Kr+SPnYj+DmwNZ9#2twvCZ5ryM~6s!_O-tKbIy^LbO?n z8?Q1~7jR&OF#XKnQYjG>v$AGUZ7#{gqov=VEL}zraz%}0IiV8zrF~~^i-S+ps4+e} zvXGq0Ine}@Rll{}2942t?d8nyIDe~F0JRr8BkJ6V=61FWHsp`^xP!4X`T-}622(#* zy-Y_e20JKcx`mY=nPx5b(igElFsT!~(UQk){unqF;C^gljnlb&9^H27fOInP`Pux1 z)0l*XI?AitH60mqEaxb=6Yijvm8gVSv$?4`fA(<2_gQWGbWRhCCc$V4!prPY%DJ8k zK1is$9=Dl(zjiS(M+Gz8v6iZj()U4HU#qs{rJSt9w^N+?n!xtF(7Uo8J#!7~prrU= zrI`BhzDffNi81Yn9pp%#b|M%$s_5t0{_>+)sBQF2>BN(I+<*OowkhjhB=P0IL zZa8_IW?%jGxGnDo#f8EjNa|6GJ!B%byRSOZOF2&fu9^CgckQoVm`t}KdF*hAxku#p zZaWclPQ#(1!?g}v2-u4j8%0*Zbe5M`-4FkaqOY)Oa(o4ZPsuxxC4XVL07XNL$7lUk zdLLp{J&!jxdsXfw3oJ|lmqG_fjpp672{PGlQ^Fgw1K*@<#x{HLaX)F8$<@MZ0Xz`8 z-^Zo$0;C5K`6~?!%aW`2%s87_CvGfA(e_c`wN{bgob2pdqP?ldBwgPDqh`Rj1F(2rVAMuc%Yk?|}>04wZ$0CQ|B6qBx>@~<`|1tii!&$ic+i*`A_(&Qkq zYw5=8^tNdyP|5LOVyEi?9$!&=dR-(^&>005Ff>GS>W_Z(D-hqXb?$!%TE~=Mk2;EY zu+S&0%_CMzIpEY?suS3ccFoR=PI}YXT3O|JDVJh|^mGNkXb`oeDae9%Xe51hGl!R8 zw9?XHpCe(hxclb8g3xN?LQF4uR-hWLF-3^5p>B*|jjdbRPc?}KP~}(GxX3ozh>nkQ zXPI*U^p3nX%5->oDgBQW-9i07QgqkX|7oE?AOzBuUlnc3|1F)V{NFa3|JxtvGB!K3 z%xR9otFYuY@j`&yW8k?73qsTjd90b7qx_ub04hT2s(N6QX7um+(Q6}Uy#Zs*MwrJsI2B> zEIXa%@uQP-nuE~zzj;}zGPM3aYDB;H6|Kl&2PR7dx7>?9YqrIzt5pfJswl_{?$g1^ zz{g({6?r`2(LhU0(i;KnS$s?+d?SM>U&@WQw*tn0<;qO2INEg( zS5Rku@z)K%&fb_c5=2@g@K>rIpO*H9Zp3C6)+I2VXNW~TMQFie$lU3#>4(WlK_$%F zZ|3Ec)ql9@M*_=%0VLHU~ki1QbbMxtweGz)6&9`nF9Py`r z49rB!xC0UNTZbCUOKE=I!o^XrZr_VZhrmVKTUwhbJm4s)%C`4G->hF-sF1FA+MT29 z={uG8QNBZ+K8v0D!jP^W08uyO(|cx5&Jc`{U^mx*=d-KIE%h6WLlrD_cX=B%v9x69 zwQ5@!vrmZrXEn!@Z&0ygY~s{61S6&Q^@>o+BpK6gqz%FrkkaLN4!X`dUy?Lb<^`3b zr3%;7FlL;rG0{;0>fhQ->2v|>Kg`V@Y4>uxdP3XD?n8%h=dpD$?A3|O~jN&K=uM;UNU$G>6JK%^n~XR;B&{Wxrh4C;?bolQ~rcl=)R z#pB|=6!$2DsHxaH8E_KhWu>(zdfSD8wSwNlBD3L|F{cFXaJQcf>E3zR(ER9IG(K%s z{pStIiq(%_!poQV({#ghf5gbHY9>f~FG9t*ka#Ec{D=oQs3Q_3=&{RpZz#un`lGh#D1uG(Z*>jf|RIhVRyc=wF-$5C;Epj6`;ed*5zh;=T*0v~@1@%Cb?Y zI@?!wQr6Le9258nxIY`(rzBDLsaJMz^vi~>yVx&(Yik$+&*OQ~iwE=@-l05Toz0f_#4Rd|llC(f zz`F1J5pPv&(nCJNe``@Fc4U$9Q792fT4A(4+0f#ASES<`JIIyosAw=Ze44eO@Cv5O zFNdce(JPiVlOpY0dC~TGWFIt#C|eM{p+>K^JXlUT56e$wJ}=n4Ex(YZi^PX8)~E(S zp^+8G zT|I!)^333dcX>XtT@|k0Ql{b}EJ~)5Zo~FlvX%_*FL9%|kOnPR&N>pVFz>MQgN_<_LyUSYfxveUf%o`h&63&WCkZF8BLgfGe>*Zu9fCgl?E!TA0m7Hc2R-LRq+tpQ7mJ}GL3 z+{uxiu$+Wj<%eVWVav3H zcx1t%rRG?>{$L)43}!(#`d`a#!#o9(tlk{9^&mIZnhyu@E zU{~u?ZGhi?W)+osM55Y|lnatL+KKxjhDia~h{L8<2r$n=r*wu-ILJ0^FUeG%_;mp) z+&cV@e*tZ;=&*n3YP@QSyRE{c35Dj@SBN%0a6~Px!{GywU`5MG)u|`^i7)X>`b+dL zjzF>Y1N`;?hqEVLEB4Vh=AuLnDb0gjf~|nG2intn0atkbd@*f83rnkX>@(U+r9op& zZJh7l!>*@_f4dASQM?UyZ^FJ^9yBv*C!`P%qu%%)eq9w}!lj|Q=S)wIQ97I(v&Xyp zWrmZe=Ihb=XE)GreD)0Z>M>XB(O_#7>&TMvTlROAQy8op1=!v|ulKaA3(8`q-Yg+g zM@f|4Z2c+EAM1W47I4N@E(X1q;9N6zMAr@1ky#{qc1};?{NhfS;E-a4UWH0d(_Wmq_TNe~b&hDtiss4hkubqO za3d(ttAclUckW%pCsE(K%}V+{#1Plhbm{Ogr+~4zO z_FklUey43}PYNFdUN^v58p@7L1xHWxlj^+{irpYk9T(Ks;aNifYGxk9d{o3Ei8cs! zc5t2u>sB9B>`q@*l)*T0+`4lgadh7doLb!H56~UM_?xolE02$N#z+<{ZaxAEa!4bxH5={aX{YNWcV6nEPj(sWzpD)jtJiEnN7=g)soMGLtdiGufUo6 zw@xTV`T3vyp!cndI|@oDOGQuW?X6kO!%|HYt8{(F_tj^<^!~ys8D~4FUn5ls=R{at z+dRwM-3KhCcz>YuI?-`G`(SSp`V;Ts;sy{|vLk9G@wf;d`Ge7w zWS>cJBN&FcJcHD4varunW|FKGj0h^7nU2^Ry@fHoGJyInpCR!)=2WKC%5QFKC1P2TjCL8j7Qo*w7+@3&{dnnYT@jxO>{lo0yu+@ERALGlB3m>00aSe3c; zLT>+j;%)FD=7oC$vN{UnK6rXdyIBXMN-i9mEXmx)Pvj%0k%E8&&LbD_8@}%4k?4;O zJ=V=R{!{z6C2h5a?2qcw-VJ`Q9&kUB|AF=YkLR0L6#xI#4?Mn`oSqij+uKvkaB=!V z1T<6V6&0bbi2 zp98&&r$Zs~n9%;^GC0^LV16zJSR@|305s46nB7#dtO881!%muWb>-_=oFRw+NKtPNEd|=^+Dbf=YC0=VGbnO08IlMgGOzFMct2G1mqdO1) zkt?gHhy&H?G|NEiMI2CH@F4WB0sw&BfQdv=Q`5YMH2>}pkf1K3=l#S%dJ)BYoEgs4 z9CFPXiG>GJ+146g>(r3mr#HdzXB*?i5GafRB8syDz6rEOj$~b z4gSL=;=Vom*TI_Yo>Mt}6n&Y1{kC-Tnjh-F5kEleu5SF#++xhj!}Bjjv{%plh{LHC zQ#E&_qHzO7v)zN6gPpxn-TUzj!jL4#cZR?W4=iEs5OYWBD|-3R9q$c%z}B?wcEC|J zwCDkR2oJtme;8QENpzW3gPq{|{dzl4v@`(JmOT9D5o&fk*abo)4yVZvfthRucV#CS zvrsyn?++b4eNlF{-)EWRUeC_<|H^X;FtE-@jF}hoIU=n+Jh_)by*}^;(-rSW$Xs?M zhls8L;SJVR%l2dZ5ZPuR`=BAP=6NHWnW5hI_7$~1v?ISPJa0eiC)|6wSv7$TF5Iv& zG^9X66nwtTab0~yqftrNO))D(ZEe|KVQ$$az{$}1&hYh9*u%gjR-6IzC{PiJF4%Mg=9sw1F?Xy=FtzP-Ju-SF zT7viaUusCJ8c^-5FUowK1PK=MKJ7wkW|UFcNy6@I6-?#!L&#l^1U3W4VgLWEHYu>$ z!#b^h1bES@1#FYvKd~E-sLT+xh_nh~&7t(NlWinX28WygzMnp)(Nu1{#m=8b0z?#O ziKtk?@Ls?MHUo@3wWbpkUReWHe}7YX*fayj#}3g=K+n@|A8YMsvPF~WO=Q$E)(yII z3<0Hfkl<7Z_bPk}HslMr1xBQ1RcJm&_^*9Qptg{Z8_W-Q&f4}H8Bct%M9Xt@3jEC} zh}3i&NorBi(IyIFf*_Ngz56{T7K`>+L|(!=n-HeRq9D`+ZnqS7=Tl8pZz6(MXZ&BO<^x zF)V>NQ@K$VSD3LR(ebOPQdj3^vEQMI=C^1fYkn6@gLxTwRHn- zi7^R(T z?@}^->EXL-1M)auN#~jA=~IEmzIRc#dj%QWR<;%&js+zISxGTCE&mC5-}aD#I4XRl z>_b>4Z`@esJOS6jYrIQGu;~k6C);eNR3EoC5zVMPG*vkRo{rt23Z&4E?%x%zybD-_Vz|9!U;VLBLa|cl)D{bVEK52#pD!kn1yaL`liD0E9 zz(K021lVf0pg!%d1xVh~U*!NR#%pg<_zpWuyGn`b1W@jDTVOJA-0u0{!mIVT> z|Brs)1qCz-PHH|d-wQcD%j$S@%6o=JT&C>Z_=fAuadalx9otlC19V?U!;Mqf&+i zxsotO(g;I@jVGyTk!I#zjW*+0Z`Wk-dt=){@JPX##qLfVW5EoEcS>uFHM)~Nmls8;c1z?E z;IL!D9RS|GQf0+@y~zhV?vjfOq&YiI80hmfO{j}g1fvP&%JU8n%5#iYb@JmSnv1A9 zGm85op;kQSjDa0BnLg$^I)!%suh)i@-fR#UBJx=_Dev^UJhCnwuG} z5$)|ncI%S*rd@NLi32>nWNAEOTQx#WDg#Saw9>@!Z)I{vs1nA6nJvqy@axL7WIvSA zgk&Yi4(<6gHj5AxEFFYgrr~Hh$}_UQHJ1{l9T`E-N=oLdR)X~_S?H)6sy#8rrHPPU zmP&jhoyVn2nuh*n@_O=zeTo7{)6$AsafZE$w*>}Q%A8p)5uJ)~7~58{KFnrehqd|Q zZZeUb>XV~#mvEYO$K$rSojl~@@A@=2x=BV7F2bTHt}mWQRVfgv7Wn2@&#h@$A&9y4sSJ5VQH`sJ$1mRG z-Qh2_;;vTa?x*?qKf;MP^QefD^W~z`99wJ?4u)L`YIZ%8_jUMQ$j&Nv!7|3o6b;mI_UPusEAcIMP!b@&|!HwGv zcHZR8X|oG1PUgs5cAKgCK4;yFNLbOx9J<*XbRxf!b*+T2JNHoq1u89aCC`r)W9rFq z20SL1r|zOUyVv>>Zn{@H*e~B|S(yFi^4R0XBYoL+nT4+}v-crZ?y0*>GX>kDPQ}(X zxsVy|7MaMWvVT+#;y%Qlv5o0GEsXy8Ba85+<&f(4Zs={mML(keF1kFoM}XZ(&+y4& zEVf2&$=Apq1={>h?|$mIr(qlE@k}n2B6#~?-$n0AN+8uiAPP~~+XU|JD-E+<>D4xk zF`6Qxx1P?T@QyRe7ajC*i(_pUOyNB9PFEriJyS>-MjC_dOh{-it+} zT;!;RBwH{R4`Y7xlZM(e5F7m9-*VTsy*5Xm`0Q;isJ69{qRcXxhcKmXnK-8{?o5lH zUW%YjnaP(ns!D<0u$)gM*n7MBkZ{fu>@V!lzWPb3lzHQNWxgZgnPg*l2cki|GbR9~ zRY&}U>~$=Ev=ZvLpOk(#;gDc%@BR&YrjBa@2Nx%`U{t|QwCb)A4K0%0dXPctn*^Q+ zaDTp}r0@#b8&DX%n~*5tZ{ng_!Y1m?UT(Bz?66s?4XrM34%38lRx zJAPL1ju-@<*sF=2+`mvV+z*vU4<8<>n`gpU1^KzJJRG;~L?dZIAX`HjK|Zlwt1##}9c*wplAiT?GgPKN&XA88+JD>Nv&^Salt$fSD-wfbSc6NErra zXD)pErNR;&>drxh*dqniH1Cjf2JF(UMb*mwrqe`cK%GOzK7B7S=;uk+MI0fsW!ct+ zU7p?ZOmK?JG6YP|ofdTw!kSSw`q!TyDjM0fRXir}lXe?=j~EmKO%?xw58y~2fr-{T zB_po*!H(y!k|_>NO3JvDcHjNw?Td#Xaa7uaeG37`)DMWz3Q7e(<1g8%5^Q5W zMs98Yy!WfDEM{nyFVoY*G11LOx>&(HyYFFqB%$3x>fRoYrPB{7`}?yhs)Rn%lf!ko ztk*Q^7`uZx?jD;oWFJy{J;vKO-LCKhyn3%HNoZ%UT zUZyMF$J{YI+B$E7AFg~iCmFu_Ez(?PW_Lm=qJnG8o$KLV{@QU)X};5Uy48vvzr9X8 zdE1t7u6*0DKI)L6Zs|{TMN@+2&GD8a=4@g%evEtiUR$~;RkdwH1x;dj++AuZ|7Jl7 z+n7~xu0k->8tfHk2S}9}4cKRb_YptA!6AC2e6a!%dR79uP1w)_xTC%GQ`g#DvM`Dgdl&A2 zNq(*f#dsx*LQp3hFN0^WYsdVcct_Fwq+{>o-F7my@YUXvRz*zpK#%Z`k5q<5KaV}aYXw=w0i=BK))e*O^J7hj~p)4QEr z+uyV!%WLbShz^Lwuixpc{co`@)nXO{)*>`}-mo1}M15`S=|36;EU&9;zk1z8XWI*zvGQb-JqZ0#f)|Bk*?DC3= z*p8a6se6BN|0g5Sr8N^k=N_`!Uh3=sIO5p(`}gqw-mYxWvCiIqVrGse@i-MG;IvEv z&wMo49EfpvzT1iI=;#3c_!CIieAR;#zkPe#LCWXC@+#-mnv6}5n%u7%pRBZ9&UHM; z<1uNQYG0+~m6a_w4$vVZ0MVU$@<#9L>RzhlJVZ&)Oy}9YwVfn1)JoNb zUn9&8RI5nA&qTr{6)%-wY*|Ef{KhOs>7Cq|TL>$9fM}RNO!M zU5gRmYvtwD#h;^wMTX0T0%b|hkFp8BDoyDa=s1|R?Q3{Jsl_E0j0rXi+j4)9(*o-ata z0xM-m{E%)1SE*bVWm*B2aOj2-g_|CQE2EB$wukm+k&oemG?zJO+EiQo?YF(n&HZmj zL8Wq;DC)D;OFyfmL9UjXy}RxW8W>*--EUJ0R7fUY{u_wpvT9u zTU+^-!sh05kpvuJ*6nx3%|I>ZzsL%$hN!u~prFZGvnh2Lc9xSCgPu3X&@sUmX@DEz z!|2++ldh>45Z+tXFcwZE-; zJurF)DJXcRSvom!W>6BruOil#WJSKb2KGTn+#{l^Eav{|4V-#vwtDzClYRZ5rT|1b zTm9Q8Ac`hc`KXNv3_ggK3rm;I9~70eB%XcU&90h>YaJoh#YIIm>KR$7Q$dXFwQf&N zz=VA5rzHG|0+Z!(z7D&2E2Y)V+YL_O-WBSq;|bp)%^=!oklX(JYcJ`*S>D!^y^dt3 zRtPxUA6+i@JcxgV*bB1LP4n;~H3-n!6L}|kBO?L~0h$|jEXH^;8t(uXnR3(eN~a(< zM{Kp-dHiC;IyfOg<^23JE&~}n+*t~r5mQ6t;a3QFx}1y>tC}lljC!=BrX+FxZO??L zGHbnjU1UOhNonzC(wPQs>+i33{e><3ZX=||=U(bO1zBD4yFxF^6%wH^oyVr9f6u7`YUI{xd4p#5?mYquSanjP7ql~-36ue5;a`^r9& zxb2d^%IZLbqwa<>4NDj(00G~`(izZ|r{2r)bs5621{Inv3jH8c9^<=s*Y1Rp!CvPN2to5RNhM;C!GJu~tRYisM)cRP5VeLMb8vy1H#wTf*4ZTk@phbbAZ zv%BB{xDd|Ko?qmmF5IK|qqqx`NQ8yB;|rbYtawC5fTY{%1vHTVH@c|qc-&%M!t<+% zMMNL4-u93U+zwk~g*>|FVk3s6DiDQz9~>~4g~KB% zT`csA`c2}?N3i#Co_+am+5^fBia{Synotuinh|wN{;Gu1v^QXaQ`sb_fPi|gYDtkwvO)>?9h^mG zjj9;LnTfvG&oU`J^AXmLty?7xzvrqQ0WiVx=#p(FQ@JFQj)EtOjpTsq;q+`0{l`Uf zf4*@t%S>QG=~C|w!XH?KPx(;C34o_F3%r*Gr?aN(@LN%I_JCR1s=+194WSn$NU+P) zdT0f4we52W+PDl92L_MmejrCCOxs;}1~iXrrD^Bvx~p41&h=dYZED#%-nwj3sEw?! zkN@ud*}Mzfbe8u$(GKfls;0d?n_l5sJ6Y0exv5nIMbr(ZD@axb1`LJ-lET4|Z6`~b z)Aj)tTDNr|6CFUITHlR+pg=INw6I{)dq?I(LlYj&CBAHXhxs)GrFgSy`TA&4V|e_z ziizUG6p)2I=7;LrRdp&-dHSkA*ILehSpldR;OZE51+~0+MUXiw`4<9gMQ?4N$<+0s z{gIC(!f<|=mccM^e$g-p^YE8kOiWSYczX_(_NA;aKhVdoODft2Ghq9DHUw+K$G)Q? z*sfd(qc6yQ|DFR2|E4m@h!6CFoPXNn1;E32o5K>Wxv*3T21N5Z< z`eSV;8lvwl1NpzC3kYkiTNvQb2LEdhtT*L*L8opP5y$ zHRvi^o6@tm!O=AuSZyz@B=zwRHy@t#chg2@%^8#Dw(RP`{SRbL-)KK>g4|=~(QM}1 zQvmz?wl_5mS7fG;{RO59f)ar`Iu;XSjYFU0UUbTraPtG{kD@;~e&xa5wx-1oebo;< zBd=lUDQdVz5I2^!Zw0j&pdr5mjkIgCnD$!i@s!lzvM|=fx5!D$a+Moa+oP)Y^72Hb z)nP_Oj)dJ(gG+XaIznGz%4Yeta*R}2?vdG!T!LG_j_U=B!~9<;Ha0dp;bgsU;b4%E zVrH4HlS2rZdvD>OaOeerA}lZyKJe~OS95nF<#OB>4|B7#)ghvBmw{GpSQ4Zep4S@9 zPuGi#lLs(+);Nv^dI$)O#P7;KR%~wmz>_07SnKj<`1G3N(D(ddakflrB&tIYOTwHe z+Yv(SZ2J|q3w`wcDE{`CP16?MYgo1mY3>}B?Z3svlxTSQ`RHN9dxUYd5Dglz z2XivqXlcYw4_>iF_V%kK4CgVGZL9B6L-S`Drj8!6*5nz47JU(*7yD(It~QSDoa@*j z52YX}kRt$SWQ3m?CR*WGSyWs+QY&nbEAg}n{>uMqeji3(zSbcb4!r{U?yHl?XLPi+ zhw%-esi0nDrb}QPng>jq)2B8uTb);2gP9u}GmXmeMG#pf&Ip#g3=OlJB7sQynver= zT_EK(T!3Qv-A+YD2dCfQ;cGzkq2!t0ug)wHkDi_$*N>A>i5x+!;FOVpfi(wK?R=;7 zQ#asX!A-nQkmVqw>9(7u_Q?qa39wxP&jXTG)QI<)OfVFH3i1>QFOZ7*D4uyC_;Mkr zeFB3Cla7uhkqL7X;Hu5P==7%AC|NL~~ zt)*Q^MueFN!1m$H+b&r;wtN&(P@`jfUpS`GC)k@X0ev?Z=G>7w!!IEqqSDU{PhfUn zg&}^v)ALvJU@5>|M5j`|{NE?=gH)9;f z{r%XHi?@3f*n`2HGY7&3g!!P_uv=gni@*r;)XfSWwWUvYVzRZi6{tPz-@^tE&vfqz zkzENzkVQ$B07NDQgQqYy)4uJ`E^>waZjXV(b{5hI3Q!}R_NhdKBTK=z_2db3l)g#~ zBA)GKjEk{(%NRqTO_!NgB6PM|7!D zr``NL6am@t(JA=61FLsf0j?CkAU6&^^`{U_k47LgJWF-@O2Tu6|kCWo; zCx>f*lM~%t4bc%HUY=FrCAjT=jlBmRB@46uFYDxVognI{Fq04p#chJZwIHP#F`_*c zqZCQ_9Ap$f^h=6zd@tZ8q4-a zxib=A>bohT!g2GztUbpJa`~M^ZhMez4D6;Bhxn@;)Pv2nFan&m7Roj^H~yl($G-s> z_HZltJ3jka5!pzSon!$m?a~6?=7~o{`hW2~N5qGJB!WSm%NrsXYQar5Py7=}1>#Z! z2Y8Gc5D{4Uy$H`=&;3=~C8+LF3&Og=C(gE#pKjpsJxbYBhqT~Dntw{bj1d}j1P)H@ zg)f|HWd0^N{&1iW7}YMq4i>=dY@|utU9U^-BM|z;Y7N?SB^hdzQQpsq#&j+lM`a@I zl67>fDBMWkk=D`oD2cz_@VYbdm2wXGFnP3#FOajHu7@us4+jp2?c)~a(ZdM4u$5i^ z=X{s)EEq`EGjC4&6gFs15}lypYibWpCm_tHRV`gMCU4K%Z-r`S$t-=#q~`k))h}(A z5C5lajv&=Ynmdzb47uGKq-s94V~K`CajjNEmP*7)zkwF4wL5{##WU34%@wsc{L5}t zW#xtw$zsikO8XQXj@gSU$bq6DI)NQFZHv`qCFX_&Nu|H`J@Au<8e-f8hx1JC!5rhG z6Zl12k=ZrYq3mhnaKna>F7ZvSfIdf|0dKVLV0)1QRWa>A3XRs6u=XR6*z8{Q$~ONt zek3AsadC!BOu^hvVQ-3ZZiB~cLIeW)!Cm-%#GMG7>Y5rh8~av@D0>u>XsuzEo5<3E zDjyfhK6LUv502ukvyb|+B*O8ZH`7oX~y8G^Mt6LcAkjEW=Hp8zUAfa#1Ne_Q^x z03!t#Tv<`?W4#LPZHN)BWS;Eh?fkSt<545{L2_B5)VnBRjRG-CJQ+9Y?My$7dG}Jm zXOJYdQQP)EGC1{U2fDC%vTnfc{8pRoxu9E|Bv2ib2@sf-8f}Lo4|dE8R>+AXh2YDHXmyb}t^KE%rh&IF)P?A_ zwH4Fh8cG!mGG!1FGRrtp1gfV*SE>^;w_o;af6#WAGg_)qZJvsqD9#zG%Pxi=YHF~N zFY5dKceW%fULLxQPpDmpNHlbflby7svZQ4_w~t2WI3QEKr_It7_JNG2R4$*cU8w67xn0M6u;xo#avmq|_JS@PCHQ-f%3 zPL&AhmK;M(O-(ks`;U3~hfaQSD`KpP2Y4h~-=qgI;g3WV0eB6bP*U|S zr&}znr|1C~u^!v?B%~lEBxI7cAL}?Aw(I?86B4=p>(8}|i&6NN(RW4>SQoeVXE2W- zl+?PSn}LFi-m6JeLj=^ukAEnZUFlfoZ!qwt+3p>RB{mrU4#EB;2% z4~3Eqoai20w)n7721iInuQpSJ6nt>xu;^B88DIQt+-F4x877Xj?F_B|LhSSiJ3hX8 zx&Y*Z@IOdPp5&uAS}7*lGrd-MmjoI$CT57yUCz(E8^`vI{-)ktTq#ntMn{2Fw$`A+wV=cBC4 z^h=-k(t=Xi_=!!hbf*9OY~ZJg%#e&okNnfO|3n4f{m18-1AHFmVrAf0o7Ict5>t1x zFFJVo5;fH_82+q^(~6=<1X9rtw2l(+7(?6;XFqKFgRwdRyEr3u=lZGl>lbEPMSE{; z-2CX+b`1fyL__llyjnyqC(cyfbF7*D*&fzR?mb1bSY1O4-*W4_)#!ETky#HOsn^TM z8W@M@Gln4+f)+rKLI$rEO$$~XB@i()`+&_j4P17`Y_D`{WjAzr3A>lQI1ueGT}^0Z}S4 zEG(Lzq;g0wsr|_n?q$)v&y9d<=CT#+W8Wwqy4v!fHa^4}Ze}e@@3LP-*_In?@^QCwe>B5jzS8{nwF!Q;ld5Dtuj#ou1# z@01*96c9bqPoz)JPn}R~d34CFi0KtNY1jDe6)!vCtWa(sV*~$$bOiwFMU)De@3HgW zJL(?mx+z4y{Projl%5v3m+K}#DP{jD(TZwhW*EdgqPEytbl=ea@6yusS6=^(zc~i9 zuW`NI*FyF~+xXPVMX?QO(KUs+o$E4y7?}b3^y3M$H<74tmM0rvXaDyGfWX>Gd19u* z|7f_@T3#6^Tb>MFK#~^G(W0~bvax|Qmuz9mg34chkKNb{jWdZsMW{x`s^yw`l4<<* zqreu2Dm(7``m;kkLs4M(E`w`O#I7z*zABpE3%55ugFLWjW>O|sw{T}JC+0-|_F1t~ zeJZdgs4Xu3`qq~FiEkk)+YqWk`n_qD*Fz>+xNQI1RSy`5OQhjSw@HV$vNX?Ur+WTy z^BkayALWyDkqnHxrFEx2ftRzdu@A(%` zpC?YlNmV?Ov!`6!E zZc`!R<>^Y(bZ)?xYM$Ux71@V*Was-Y2E=Jwyy@ zf_ru*WQ5U`-5bT*lJ|sXp4sSNx?E1s)5K3o^pU%Ile0nh*#&LUP*t#d=vBT+Hgz6R z<_5OXc#zLKM{HoJKLbnsCOWye*H!R^P+T8#e3GOo*jm>g&HmRG5|Df3{RB8?smN?qAQb*iIb=EOAB@p+LdV@gllTGG-j3u9V)AmrD=~G;5 zj7UkfsasG7cE{QOMcZ3OMHRPi+aeN@(kLY$(j}ckqeyqRbaxF%H;A+}NVjx1gGhG` zDcu4?3kr@m=eA3>0VL(ym-3i zGHa*tn<8k<5r6xV-)UlLHZR1P7h3&1G?wnA`lQEDyJBAxO;)yQV4$WSnn{Q5)h*d9 z+WL30q98P)ceBK2_ctg^#)~=@cZxCUkS?EVw=!!!l#iuy=GDpwzM|;>^}vDA9xk2C zRFdO@=o$X?J+Qi%bKpUp*^UF{X_MSvcICr=K07l6hZy@Vv)Q6qKmy1S6VI8#sZKYL zq^yK9UQb(JE_68147#KsBmj-wxv_DuXM&!9rq ztZcuE3eI($@w$BAgGl9zSEX*YRy3lr0$*(XQtPv&@10T^>%MwhM%+M6*e*Zz`}uJ9 z#-f&JIekGruKvUKQg;BU0ZeZ|X09YT<&n0mo8ON|Q&qUelb=NP;CZClDsCTOHi5OI zJjGm-79GIs@sf3hsGg{4Zt_dpvDGh^V`JA(_y#3Qu|U9xweS=9$9I?fo8I0VZrP9T zhHW_oHoO|JrTh}v3~;{vpvooZ>lTc`>lwtDwS0k&TjrV@OHz7sDOqG42W}|hgp>V3 z1EL2WPe{>60i{YC5{C<4x=p9QPHT#;BWw-2FZSk~FQ-EhEBoa!}dUlKR=CLDXiRLqWRDv}K25J1sUSw*perYH8!` zduIxfD7V`U;%wP*hce}{xwy=_pc`m3Qk~_wCd0#W2bnFg z=ugr~dM<8q`m6H*cw5EHlI%1?-)Mve1foxxw?*-N1z1^ggD3&y0Qq%TwEEW?1r)TO zjnW*6l_|En?BW!s60@tndP$>UPfYz=nkr;A5}L@Q%Xhw7Y|oKj{^q%8 zfut|IOGPL{OD|YM27v=Nnty(6s8War`qg#sTQxevmZ{~WJ!n}7Iom%Y;I_G7g3lAd zuv4M}1sj!MDh!=%Z?g@jn>pP6fKDezC=|)gb|pxhs!D|3+KupZ5gtpk4|+{PsmIiT z7?Fxxe}e8ctI`)dS-jnY@o@>}oW62Y zLQH~{wzMw52Qo$F_D&U+v?6=2={k3BOV@LmqJ!hB@#o9^l?v!-p~<95dD4O;@y6?) zwu`E9GiBmSIqg{${%$i=w9)swUH2^k^O;5)A)TuxgV^h>8x5w3bO0=*yQ~*nb+rLi zYU7k5AhU2Xjkh6PNJJ2=J|H0TpI^}PxMlmslciCKB9fKn5JKG+f97!|OF)ngWQiS= zHtU5#xVVMq07^FOZP2p-l?~`MD#z!y-Ji7_HwgtlJoA}{E-lHxHxnySpU1|IvaFZS z*_&A!8ZI&~+)$@3G{3Vseu>KcD$_Wp`y09_T7#)S|!`3pFG2%Y$|L^ zO&@}xTo0I~`^UM20w3Rph1Uuc{J)3;Ff;Fb%wbuKz?czKUQmzSLE zJgbxIP0IHv4L9aLm*MY<%oh2e0Z&wq#K29}Nl!;*zjXY95_^RWwaJl5tj}l~(}8sV z5T3V7HUv8uW z(jY@)V{w2xI;Q;Ti7-)0s-0?S30F-+K}17+b~5k%bX|s0QUa~)RL{obv6H`+%LLRbH=DUTLh%|xA zt05h3mnDgRx*gFN(abR!RLKViHc=4~xxm(v4B#D~Hu@q!4;?S#Cap|N?jN?c7#A0p z|LgyyFY9iPtdP_8UG%k`QVwp`CnFrs{AwBg>28y_%l&cK<(8M$QckAzN~O_{j0~M2 zfPcF|}G-W&rv`z`{IU*CxQ z!v8PaMgqWXK<}vaT<;aj`YQ+9YWAeGNB*(of6>E0(BTUJFS-G;XjMNy;Vw@Xm;5LF z`1B!r$0fdjQxA&U0Fl;&&}q9QFd^7M0Bwg>6S?AUz)Ei#aE|sfUS54_-0fa!dMCei~)#CU;$KwLE(*v*Yx?4eT{1|ZyhHY4e^ODY2&^41Iu4W9yK zt!KTM`@_uDe?>La)hB99`t|??bcmRHajNliSFSo=4R3dhFc4VHa zfv@+0{14H85Vxlv?5n`(xWC69?b6_Uw}~RRT^ZUek9JW>B`1Jb689}EX#0cQ57deI z`8BZl%_8VRDE93mRuuT4{Wv6X`pCT)^q6?k7-Z~6Xn`}uz``R)+YY#{Zbel0AMbZT z`zLLuMeFOv9z#y^PUnbP_d`uV_)*FkB;q^nHOyo)V@pxzk!{ zY3Yvr)YO#MBG0@&?f<5BPfdOtVq~N7MKw0er_yGYG|zC)`*K%A@~_&C=>wP8YVK3Y zWVZ(S`SS&VzJ|j=MZ2w731je$_A}cMZK!srF z!c)z*a1NOJ7026!GZ>7Qy>CTSzq8YT1Hf(-w*YeTqFtcq*^@Ot+Wy&!P!Z05k;x}k zk$=Pp@wh02dB#+Ae7v^%XkhDmxGJGnRzK5GSk-jW;jXo$D(4Io7&Ny@CcS$n_BPN< zVg(A_M!?!|T|ZcUegfxMCWUhZ1oV(MPlX-DAC5#yNLnKnyZZ~Ho_Z6ARHEbJ&Vvu83ZwgY2R%N#f8wKE zfCTzTRIJeXZ@j3bnC%Or!Ti%bZ-#2&J7ldTd}rd!bWW*EjXfX$rz-}B9UsaisnV$cJvf^1lrqwuvu&oOrZ||wVkH}0M#4Q+{{mbE{wRGz9ZcM`+>p7 zY8!E~{W>3IcgHb&h-;khpl#BcT?f$O^uBJI zG1$7PQlhQft2zf(!#4pYXk~59=(G+9uYm1P31xVMi!)W~flsw7-U^xB z#2xKp2Qj?eOMb<0uIUC_Es8W!wZAtdI@$5yFDYLKoR7Gs@8ygC5d7!=OG&f&-sbS2 z*0i8lp(Z&k;NcHxmgr;svXw!LsOhk(`8GMcu1jG2hlE{B;Iu+rO-%Bt1%n5nOTj_L z>`tbc;(0-HUF-=0h`>xKE*#|x6lbWlH&Z}`A)T?gffv(^*@Sk;lo^nXV6(lE-vH(8 zo-9N~DhV`nI0F#v9+P`;Z?6QHQqYkr6Nv4l?PhYJdmWS(*SqfWiv|Y+J;3{UUkix- zQ&|Uzi|}HWf)tzn*7d~7Xjd0F4NdEGqP>TO7qqr|GtE;WsqO4oTiA~(?D}ZNXAERb zL^km#I#5XRsqWKbie85HuFc&7Kx7Dk?0-)Vng8Q=b5i{u0ou|q%sZt&X+5X7Q%S%nmyo(!P(e)O6EI8tZ zN=nj$Sd|J)sNwHF_g=lZ`7JCzTDcZn-msqp40O&5!517nlk-b|#jFj8Ix%&EWj&Sp zKn8ba20q{12}b?sB2KBaWAc|UY^R+Qih`m^t`T8{rH$7$RJe%*9vck^%%qz}!h_WN z-A(E)Ye8lHH?eY;YoKPr;CzD$1$wFLv)H{ijg(kSOEfGWP?5tFBtGrecHC(KCq5CW z{>QgbMT+7(@W+kAj-$haX;qb+gquia+l;p9Oh983ysJ=vg!6&*lsIM ze^`w1^qew{c6>#dsd@SF#Z0H>{BnsXz2;xqDjKCWo#5dx-43P7C6M$>4^8*Tj>_Cd z^3DUl-4+}P$L%w&*>g;V9`O#B>e_AZ)|^EfP^y3X-40)hbL-z6S>(%H3Fx*XVRy5$J$OZTJlTNbV*DJwC@t~QWudgvC z+5$gw2oFL#Cxeznpb@{@pgeVM9xb| zmE1Xlk~`EvXkgRK0{Y;W7EreY$Y|BHB%lbl9R>gB%F6Z;#dt6TB7#JBw(#*iVLR#t`H`jcU=4ThIjM!KL zE!z!3@iAbVNd3->b}M!sFW`!�e^n88&jgDmewdK5WeYM1U8d;hR)xRDR8>S0~d3 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 diff --git a/docs/development/navigation/images/isolated-build.png b/docs/development/navigation/images/isolated-build.png deleted file mode 100644 index 8d4ae7e4410d30f989de20f8cd5b587b2bd896bf..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 42909 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�k%8O2fvvPFB;=lx}`*9B0eFG zBXi-wBE_sR(Xbf~@hLT8irnPd5U}NjF|~A5&nhax7k~ZJi_MU8+TQS_O zbJPQ@Is-stBgQ@Vu>3kWPW^@GVInd_lyNMS&uAjOE9aMi#pwbwW6jrN_M&A5>5O*F!o5B_`omdT7PWn z`UKDxehHvR?m?^K>|O1g87@~?pZ4{{!cXV(%}( z{3-3^TlPJv?nnD#ig4*2Wskv@KKb($rTPy1iYL68B>{N>3H*_Va%2aAhws3CJ_7y# zNn(G*!-R>i@E&XgK;eF&B9%KUVIjT}zCZQGvpP9_pgN2WCOHgSIjJzzw7RRv8!=KJ z9{&1{996(^XHZCf@W<}~Jx z#pk4}=FcXo7-{p?6Teu^%>$pO%P@%crlqEq)QvpJ8yY2+@fDbv9;dxxsZ6uot0&D} z{VuLf?U?9w>2Z}2G=H)Fr~1wysj2(>Mv(S^7`{}arynY2hwOs z;pw39a2a#a@V0kXtfT`7*RR>Vk1IyuI{VdAF*_3bL>1jPOJR)YZaw~Sd*qE>J?HC54M-Jbp~OJ%+oOcWH5OKowkJ5zWa;RPWPNR)X8yritFTH` zc72C%Qb6WQHac!@xZRK#8h7kY5kZT>8hBt*<&dyEaX*`EMp<7Gb8WsMnwr)i+i_U@ zgmEh4b4#?gSV=A3WWQXuIx{hM?{Q7teU|;@p?zK2H{-D1hRkgpxSaS%NeHo%q|+KaC-Kn9hKB{e@qdsw?xXokb{j;lMq@~u?us~7DyS7=V^T4$> zH?k0kUL_YVA#%o#QD}7gmC%}D5Ctn799&glB^q@#;&q}etzJK(S*9&r^W=!|`8xKA zrB-u*c~YZnmui|AaxU)rFZaUFd<pZHCcDD}jLykHH|?MdmAxz4*iQSXu8-@KUC(D_6Gu9-?J79W_6+RjVsYew z(}kv}Bck_y2U3ce^4mpYdG$n%)7i=z-_B?1A6fg1XFVT|iYRuPjfog_YJT?Xj^>5l zzWhKq=QAgkPXkH))wIj*Lt|pNL!~npUd}BjQ~R+gB{m>eGHKB9ac8TLxdo*Wjo{!Or zY4ft*=UH0gN8?xCW@;nO)fO)%fnq@cSo{guBQ2!X4y=Ra8IMd25ZLhM=EOjk3Y;jz zkamFZ`oTGi_bl-T)iv{YH9P0fuSZv2sI?}`^0qV`SEk%G%7%y1~1)~HKVoKpf84T^$@!+GaiwuvAe-} zC!2Qqc&KG;OV&S652_EBaH*ys<5jAhf-{q57FgP8X$8IIRAhL0O4qr<7ySJ7Rz77g zA$1?_*F|y@Z+)h6%ocB2T@RI4tE}w>w@KH3H{Jg^D@T-hJ{PWv=6kXe3yHFdPX6*s zf<#zu((f;eLDf6VEJ3sC%g2pLRSyO11WmOXg0Pcg3_Ym%S-S%`yruGVv8MsAksY3X zp(Xc%{S9T+L>f1}DB53U3&|1O7q{r2%CsBUEgnR7n|nqY`lw0sn;K$)#@B4lH#dJC zZ+dok2rX`XIQHpRc%pL*R^L=1q3gyzt@1x*1%kkrVyu6V#Nr_GT{Y%wLPggPBE!`U zDk)231LQPd-3a?Ef*^NP#RW+Lw{Z~WE7MXULh%*DC|X{U*r|n^1FJxB?}zg=KSt{m zWJ)~McE^z}suJfW$hXw2hP2fS8=35N6ho23(fVuC16EHC=Mt;n_#S_&<{@K!@$IZ?SNDll zT>IKs>YrPq-|LW>6qGc9LKj}F`Dmy4tXJp6Z$lq$h0a=j=%+lUwy5@3V*3BQC||E~BU8Z;1a`arR46CYTMsU48>MlI^)RagV8b`R?88Ko(_FG-VQKa9?f{#k>-GQDZ z=K(KKCtFX!Sh)#y6ewEaAfExnQfwh1#7gxhm9(GAJ&>Al#AR>_^ZE=XTd1j-Fc~Bt zXR?g1m5(NXBJMsNOW=@x0L|eMV0GF0?(}No;8m|%>wdN136bhc9nH&n@1P!*5U%)H zcUPl}oGM-OUd~uE=6)R|al7}4<4(Bg!Yq-TR9UpG{!8Y|DNn3xVm`rRpnW|pmsb|n zPE{`IG@eiH{8W!j0tY5pi8wKIXPQiJPeNLU*F1wv8GTMzg1a9^b>x2EFUu%ek>`E8nHqlOp81L8sCXtT2%0b z$>=cstg`Yh2~^9`9;(B+>IjeMQqdYGo?G^(l9a9PyyU(!={GoGRSY5*@MeVj6%gMa zzX@aMBIz2KWP-k-Se1*d4DYX&^FEK7az&Z^c@d)q!k%*>X zR6hk=74kz)O$?{B#M)r+=^WC;*b)z(eT_cC<4|8UywbAtKV1&qF%F~ZUU{>KKb1+F z5$3Lxiz#ro;g~2=P+2(tTB=fNdf~BXIUqEouaUVd+#P0ts4J}HZ9s9*K(`H#M?Un~ zY_@kOD?o{{nA^K)Nfif5Q|oidu*F?4kIc>^=i`w+zp-;d{MlEnxZW-KJ(1yQ?;Tub`^RY z$cLZDwR&SUVc{(ln23sNn)6KbKhjdgm@;bh%dUPYvh^em{vcuVy}ACRSSQPBdyvUD z_8|hMt@TN?U6Ao3w72VN0V8R9|M?`#J9ghC{_Xl_OiKEi!|lP@8A#`6M>Uv-HY_EZ z40ma51fN3U11lNum)Dp;&KR8C-+mFR(##=Dd3K3nV@hZ`$LFwAra=keAOo*t$H|!C z>ZNaRQ%7R)J5{}oGLg&$YWOOz+-`JsmHWP@Y>+A4;nR-theweZil%1&WxrU}?cs=x zcAHs(ep(8S3gUIcc(IRI4O5TtW#$gQH!~$PzFqR)JZ{pPSM%;sb0|!gL&LdS)^Hi7 zyxkS0l(2P^t*bJ_m6X zAG69y8D$O83i8JRinIM^dmRblF&@sfjf3Xgiqp7Phw_q&yp2CJ29c%@A`gUUL?Uo> z{ffUq@bAn>iki0`)F9NrZD>xwZ6Q`#Xg6jsQez5!?E<6e9W!?+f^IB*{ZyB{cKitv zK*NQ>kO7o`(m}7Y3V2o{M1*mIcK^xTzdM04|E(j#P$2n#Aawt)*6ROCDqmCZ^%dmd z;ZZg)$kg`o@ev3K4px$s4ApLV6W5<+!^0bEjk;hSme*e-o%Y8#tk98>bLlI=ff|*1 z7IVI=ICzOFy1D}`HLo=H@O?8YE4g+J<`;B}!TIbm3SWx=QTo{wSB*o-!=s>G8_)^U zcbG9RyJ0JE*vn!$Z%gv)vowFq|FM)=T9O`h@mp;vR8>{A*y@dNzR12WQ?aqOKB)Q2 zln0B5mRz4j)0Hz>XYp5o*ZVH6gN;OOer96i{%>(zxoAhk*RP}5^+aw;R%IR&7#~QA z0wb_$8`vV!SG*lvmKCrI89$M|wmpnG(^UQV&1CqP)Pbr9JdNB&UF4ndcS13|+4xmZ zLET0>md>3KEu%_S2&Bw!-B8AMT}{Q~VTfn;qg1RA6nDH&$J0OA{{-%ue>aYY#HR3C}Mi5K3iXBT#FAh1*v<0>e%8MMxc6Ha;Ux2 zKAYCy^)Q_t61#KQwtP?wu;Lb{p(J%P79{ZSQpv6spC}H=$>zWPv|QiB6@F7I7YM9s z+;n6W%D8HdXOZG~JC`XkF(>!55y9)Tm1!0Mt1^ySO$I*9tjm=}_4SD>D{aL!rArG9 zLY(n$@M7t*O2l=ZpJ=r*^!-`Gb(!To#xayDhKX>`ek6CUKYs z53s~M3TfdjB+Mlw$u3;gktH05eO7^a+F7uPmkaa~^g!kFL&p<6SZ>Tsf z_{e}?3XPAC-@Flsasd3g$e_)05|EhX|C0?Jhp0>+-&EO``*FX2iZS8gNX?}%`UmWtz7;aFXs zxQhU|QoyTcXx4_v=H`NpG%F!}tV>+hYM#~!jvNp=ZbpHEYM;1BzE{lVJ(C{u(!+Re zn2g0N->#aWuQk`%?ykf~$>=X>1Fp`_`G5?+UT55c@N#x^BmH(#ON`>SDG}xu>*Z}+u7eKv~dT1n}bRQ?#{Xj0e4sxBml`2{qoS;riXt++u9bAt< z-$I+Wg5DP{>)ZSazW3cH)olL1t2mzopWMP|lFq)$AGv+;YKP_ECaobkLr`%h_Q@Y4RHj%c+@VZ-OYK~+5S`0(;g`c z68QS!e}6M>yN2K0?`J7a7s|6Olgakmw%usB9zf+*;a?j1-iPS>Iyh7Vp`%@ITI-nd zVrpaGX`h8gziOVggrH0$;D9#n&`1k;ClVHfC_ONqY^C__^HmRq!HKGxo~bo#GIiL7 zl(>t2aHIEZsKORvk&x3GYaV^rV+OwuW)|6g3ZR%ICM0L=yDtzCenq37Q`PpMlFumU z*ZF3DE1e@HKA`L=iH2WnaRoU{;pqoymU->rW0*YI%7}@5jAQ;-2J(A**F@l5&xA;e z!Xd2S%kDc+6!%_vkz9)CxB!G$;Gz@O`MeD>b=tT-CfiL+Ozch;4mL;^v9V!%b6~s~ zp00?Q_WsI4dLHbpUjfZsu^2;P9BZH+2V-HARm542e>Bw$iB;HIk5!vK$5#35L*h1w zdK`2K+Z2X8WLe0lu0BrykuoM+;k@O7e{5mEZfLV z`kU8{H~px}{(v4&r*r3fmzV32*b#qPBvbpUv4#Sd|DnUMFZDI*>o#6MQDFIT6-V!; z=+dj$42ARc4x#@%0X49&u(Y(a5gJtnqbAlzz44rljg4GSP-{bZE%A6k(e>C2wP!~PH8LP_Kum2FZ(RU;sWMJ+re1D8-Z$-&WSm`GA$S7 z`I}&Rch!gdnlz~FaFB&vuZ&n~w~5x>97TM2CSse9m?K#3O<9@AlR{?-g?-?0$e)k@ zjgEx$h17}lWC>e_>3C#6C%)NreJ?NA3w7M*{N0k&=4p3?j5GCqOuA{Ko3lET;7Ia2 zf8jitm22!dgU--MN1ULpMOP{BEfJmFBZaXeFzc_IU7En!dbd3f^O5nipGk&;r!@Jx z_5B(Dka_8*9Y~H7()wqF#XxyO$_Vo@2_2k*{tQ8O!e3B0Ea7Ej|I)J2Cq*sG1(dlN zv`A#QVP_e_TCMLd@+#*c!vMum@HEYq4Wl?8f|}D!lL5GXoN~82iCuIAl>Iu3^?$_m<=LaZMXd%d%|NiCZJNTfI9)E9^tFD!#!H z$f;~-P=M#KN^}~ZwZj5<0y7>PN;JzTwCTe0GOa;;K`l|v(@Ns4nBf~L#YE*uDYNQh zG_@F^=j&;PDQO?Enaf}fwS=_7=}Qur*HNzZ3vZs%+4I&b1w1EtX%5UV*f1mG$|oR> z&m4X4#f4vKwD?F_g6@nfmz_uJ3Y67U`a>)mWc8!}&M3em1vY!}W&rej(=bp)PUtxW zQ<4ywpPJ!*drc7O)ofK(;FFqDGS-8XY@7+}2(YsLgeU1Umrg0l&nL+GQ;6GOv)mA) zI_mCCml;;+H~H1Yg@a5}RTWgON5*Cvy(s9%nGC8dFXf?__bLo1uqi0O{imh&wi%rk z_zp3@A3mxjE=%HS+o%~n>_`0Oq&_=4t6ZO9%*@O@nWK1Q%T8fXd8wQZ6x5c|6g&; zt&53?4;%wH6bW&?|Eg&m%>RDzN9nDd!hq`%{eN)5)BF4~Nc4XtG%4mKTlk+pUwp$n z?Ct;f{(38lowv#{FM~K@>guy2=XTVm+G}`AZJw_T>^y7O+1Y;p#vvX)KDT?v#1Cwy zN^ztDG_ z{2DVQWjA++zLJu#t*xyykSiwG#`(pT8|{h?4$QY5sHmt~E9}*lX^Q8_)~Ke76!)EPM^e}h*SyXPP?*5}c#Jx|%XmzB z(p;;~*!mv3iO#@lU6}#a6g5M`@nu~d9rLb0c;~;2jT3;YO5Sb70a4#+l!M)<6G|b^ z4mf|0?f<#{`fxQT8rOZT_#uGfr)hL(XlUW{TXQ4TV*9C78){{Z z{RTdH#?_QBAZI3&I)l0!vi!v=hYMWc#>LToi^ zh=roy{U?Ffr>m20A0MAXApI6yZVyav_K--pAibr6ujH~1jwIjyUAUq5WgmZ1MZmp7_|03@+vWu+ zH1vB;+^=}NIhNvD_hu_vxcc`GaJxqLeTXILMq>#Jnx8t&Zf#AMApSd;n#LE1EWaW@ zef(c^GB9ZLc7zWKGGR)^FDI^jP0(1*KhMS1;a#oUD9_CeB=xyguB4mqB@&Gt?+k#o z*cpsJX@C}AT3gF}fpolHzDURAVF^%C^?}>yG22uXOkMik6TncN*%VnHj@@NEX z{*2K54tAyB0O7f!&~w5W0O8G>p+xR&T*CD{a1W(rm0Qldr8Cv&OG=5A&|Q+k{-rm7 z71BxRq8D3nVt~wVWmk0t*lM^OJ_eoM054zM)egSVyE;zqmorSoBoFGzi9485zF78V zgq7>_j@L2S!E2zQn3yV%ZMu6ud!%K8M@DAXpa}H$RV)L7#hhWajq!^2%9~qT7UCYa9z0Ui z7DKW>Tt81}Uk5#SbGa8j*vey@R%NHRQa&3GNPs{eXXoZzvA*Wl+}IE*Bdxl_s-hO3dKRns@XM_Sxh~GS z!$)>_2rLAdoS+FXLQA|ciPRXFc8HYKmO)%Td=sg@#GGlqgn~uWMdKSE*q4?9hUrRg z!rWvnE$N z7zzUea{xHOcCCOmCythZVdUiQ`4KX$Y3S1j*)A!{G=UDo?%4S$`ycK4wpi3WjA0?o ze9-fP7#{PZF93ji0_He8Z;m8@dpMo}sD3kqdp=zb98SqH7wrB`x)QwF8_j!t+80V6 zp7X!zbHLCB%Z1O{G_LlWgbKb9HgC9#LO%wrGy;$KFfy$;jjhNpvhMrH*aQYE85xlm zB~i<#emf!a2A1m0IIiUB$!qowSXV*3ncu(h)?zP>)0sP-SmiVMQy}M)d5AY>t7-Zk zj*hJQfYqDXj=A6dUh5Tv}92m2%z?V!}L5*AHEnt2~<>)h#)Tgw8?5VZss!CGulqhTtb-@fHnlcAnC0rkVh z^F+U%fx+0i11U2z^L9HD-b83LECHV@gT8$id>z1IZVL{|WpkrLzW0Qp_5(hb;96YF zOV@Inxd}HhQ&%jT=lua=r-OY=C)$pQ4Wt&>ZUi$Vhu|+MtPWe^uA|alVZlmBni3N5 zep$RObS-Y-SivuuUT2%^9Kjg0R!_kbdCx$~H4_Q>6;@19Hk|d8f{7Z!Y)MA;vf2- zuARmOq{zH4pj%AxE6#yAq_xD2=3jH0mU7T)VOA?d`Q!^7nA{_i!gh z--HM3{9rMp&7bv7ThuBK(pKoC{K=4@g^w;V4j;+81d({(=B19Ni_i_ZRCYb%=YR_3 zZI;D+f*eEygB8wgi6j?9CWpH5@MRzU4@p2w`yjdr^Xwsj+VlYRyj?dIML%^DSn-;z zVLbG%F%lrp_@G!7khe zCsA+d4-uJ$J?O&X(AlPP6$cIh=`;BhHv;WGkm`vVk0rd98*KOmt*oqSown97G)_G+ z;Dk3E1f=W5yrD^nVJ{^cpaP)a!1U~xQc5`v44CtgFb;++rfoe^pJ5&&u<_wG^@PP+ zGoCLk_}44&ZJvuD!d8xY$0Yz0>HBmkr2xts)`7hmf8!Weou}pXGAb^o6@{v6#py_L zD+C1S~qGDp&`Q$(B=d>i><{K`t`;@-h zHt!%y=-F>zjAtd>rPTn#YfNZjm>;Hw*4EZe^5g$C)YtF&kek|%uSRf}F2tXO77GH? zQ>Kg1;oO120QWo(?zLX{W6Y3B1MXQ+dPWQAy)h4f)oOO8jYBU7u zbE*LRT`|ZN{vCx1Mm=t$s5^iSjL78-Px{=r8&scLGSPe-;^UUqH0X!I0DHji0AZ-W~Qcuh~us{r_U| z`#~6zxVvSnqHwDKyxQ0{=}}bJ|~B2 zlkv)p%c(Ja`vj{tp;WN$JZ6%FD?0x;o;|a)?FCwi@HcCmpXW+$lQ{bpp~x@Y(?6Dd zF$nu0ZwgGaB@3XuRKbmB%^RdU6+%O2ark{$L3&0UdF@}lAoz#pE}Nw9jfBi z?ug)z#5&-QQ?AP+SLis5rmkp6M(Ar&m9}X1omQq*gZ}&Dj&VmBqiVgEL6y>XzA~ls zRh4-DrP*vF|CPzUjI5V zc;KX>=wvvajfS+5e`7NNo&OL}}2%|3%x}?Am z7u{&DFInG);XWhc6-~e!fQDGuk~`93_y0V&#WWk__sMq&UM7Ye{LH|%P^l?ebReK_ zpReas@ldVM_9K+HsmSP$tK*A>1^1tw5s8}|$?+j0y=QCKOeIW9Ef(QXQ*9i9{F$Ag z_C1myV;-`wv@AS2(y=`I{2cYNx6D2Ho5c6C4axIXj0<<;p-&%6OpLrfuNxA34N@U! zvfrX*BqTJpp>V0$^JOg&skzyvh$^G*|G8sFKh*}6i zC3p}VnFkSQ4zw5UcirO6YRcP?%+9)zTx|GmyA=mqI{Rcb+u{e;qGDc7Lj_;Vl*V|k# z7}HjQAs`mkvOI4NzggUqT!c#?+G69syZ3E50PEeVXTa#*inqhI>%foKj zZJkEqk{}a7% zI{B;9j0bO_Oz!P~liymq$lG)yA*&-{$Vt?mjr}d#K2yOa$ZYEU?%JjPM}F4d`q#ym zwH7BkO*jP6uT0@vSm;yVtcdU~0(vU$*5{l~ONznUYXScvW!lGrN>chcxgCjIo_hwy zHUI_v1FOUkBHP^(S}#$1Qo`@6+tt&q@tjssV5T@#GCJ++ucBood#!}(abQTRomy9Y zW9yFziQ#(c%`y#hhZ4L}6fIyEKG!S?LgD2Q$ULX<|4V48(VWDO-J3bXRgOO`~V!mz!XBYX5dKM`+BzTQ+*@=?_Y^_3UNCUznw5bxNuOMG>%et7Y{ zLv|cmlL96DfKMcNnGh;Ue~FUG@7n9;erOq>`N#Yz*hrtwb?9KIdEr)&-S#criW-y@OrD%B`o z0CS2?YEzPTyzkKF^sG+LvUnX?Q=Be-4&;K5|kStEgG-Km0}}8Zl=GY^3_pi~Grn zeCArqqJCT&YnF8q@0D*c_1!L{y8oCv`S(y@tW~mIZ8yybLDlq8%nDf`(#h3v=Oqr9 zD}5lQGry!MB`6rszezvBggphC2pV9FnTq$me%l~jSB{-S|Mj|a{%cLDIYWVWVvJ7F_fux_+kzZ#<2fCV2m1dxKar?>w+0@(y)$XvNIwe8_ zT~6@EnaWmHrTEk5*FzlDM5$NRTpr~nOE=sv*J_n~jg@LmyG_^SSk5CuV%g^l9KQ_E z^oO6(V%9}pZ!$y+M(c45UBa0(VLI|xyMhk>SkFPK_=X40}Ko(|L$n762N1- zI4r@>MK7QPbAlR_%r!5qL6rpAS%1|RmkBuJcWm4isx>v})d%=^NvZXS2w_egUblql zb&|qH#mI#H|FkIcw#_rJUbECNpp$?B8Sq-Qr8UMu2ywD zhRcNIS{02T-)u+8@P)K{J&CshWESmJJ>`KNMKyKzmG;m(eVdMpW6b^EEy|WdOPLW8 zPRa4J&%rlGwe3mRu$X9Q7-%@fpu~k#Q(E#B)p8s2fK!>gaLxIdsUOPI=D_@^w_)Jy zv;PEhoo-jhbo_9J**3G5FyvXb&k=-woZ*xS$&4N#+gLFsr4lX@=pYrFVIJRh z3FpaJ!HqZf)z|3bEW`4jRU5Eot$ZrXZ$*N@9CaL%0>jmQc)P>v9U7u@vNuHqE%F}9 z5qDOnlHQqI(TJs$(Sz1XS zHX+j`#fPZrX-Jcb`9kc>7k=7Jl)|tY-krG(Y0!_C&bs_qP!q0wgR&sxANwvdduqw5 z54BS08B&H3Tz>xckAFF$3}|_hB^6vC(H*0B%d^)Xk9rK6-;bNn!1l6$mS+pF01AduX)i}(s+9A%6)ld z9^$OjjN%oOr8Hn)3eG<3dot~ev#RxXjUpZzEC{haB)o2ribu4uu*qAnga@Ld$=uLe z#D4N=^XcM5{jqZrRfYnih#@Nihi^C)F{i1Uwj?%aJ% zo$5H{%0itKK5JXrDO|pb&v!3pgyV@LaP7KDMU6( zMqX^=dbsO{a>D*dcuM(+#(ZG4W!$vfUwG)J6f;GaK=N1BR_|P6JK^^%tySvTNSZ5M z8aj4KcZkJZ8#&ZnC2nsfLCb!9wmxC;R6dBOyqx>41GH0L?_@}DM&O7SvbA&RAc4uR z4_sO(#F1hgHb)%FeU3zYq)5$0;uRa78;Wc`){GJKOaUK)gZfF9;HJhOL%!ri4&50) zqis9*0P*wvgNvjjAr=*T004>gTRzQS-97#cLVymqw$P;1vrG@_ML#b#jDFK zBKQT|`|a`}Jq2zQX*lDFK6Jc(K`F9gSZq**&cCkyT*_?q6?`{~(+NC%JP|?K2G@h<}H= zM)I*L-=d+fV(Q5_F5c^=w?+$L>d5012|Z^ckTDJ4>#op1y=zFHBfX~^s84NXh+*Fc z+4Az(#=SF|R+;ZmvfY)D-{HEIRjq#Qb$3uQDm4$8V9C|t`jd83^lEkC}N znKK1V$XB}D$=+NPfI4U~IG26oBEPawAbc}!Pk}uMq^Zm%(V;>v8ZseA;cB>nmEf6j z3;`y(_X&u`Gvidz+J>{v-Jy zzid3xsh~UuT}NbcfowcMgxNx#XKutB3Pf1uwj_!zYnCNomf53kEHHHF5g zI1+Sc*PsxAI=rd=i_1ewgJ|H#>x*^VA`3KzXepZzW2tXmLr!GXw9W+`gPSmj(06BD zt;Vm&exHnFNKxOy_7N__o%iKSmTxtY#J7+7!Dt<~m!e^xKB=>e-`}5Lb3WbpX1%(h zfJg81+p^bxkmu>n7NY0SZS70<(clr{r;Co3L+v$Q2;fns~#Uj%Nc-NRDw`FEf?yrMHctLrT$nO7istt zkfb%iJ9Z>}=PKJH0@AjhxOl{Ywj$^{vZg??nil&1puu`cVE_L_T13*avKY)-Re?a< zW^*=vH8wKx79c8z2KHdJ>A<~;-K24Xqvl?7b8)JO8{^m3Y1dUygg-oZHf<;9>~*42 z<%NZzKbnObtu#4b{`Pqc2eP-^wYNZZh#WDT_zXbg8Nlz7trME3A@4m%c{&pcSRe&u zLY=DmN@Z)0rR&fzO#uM`3&6wlU?v2-#!QzJ#!i)6!%6o|!HMKD_Goz}a((T2~!I5-?K|Ncr+jmorM84JA zFqz39-MQsDZNKiCHtaM4=jFtSLLTmNi44KkP2K1DAXLN2%}uuSrL!?cu%i`$jLRB$ z(}4HxGmx+2Pzi})Te(cs%ar;6$=vYx_W<*$_0KMG2AajfEjmU3eE{f6@^tK4j%F90epXkr%cb$e+P2-99eV)D+)Eycj*kBE7^BX5p=;mqAu!4N zv=jAmD_WQonb6n~0?DGwaP{x^0#K@{m$5Rd0hGKvT&d_}r{~2gr&qI`y{s{gfUNzu(*p3=a<r3auEFB+%Sen1}dbdmX75%9;CCWu6=Vcn>^t7{K1aAZkQx&>NWTWglS zcw~rXZ*N_N`lhFUzSyORW)t<@LtnT6FbN)>Xri9W8z1g`g-oGkb9Hs1=H(Ddcc{M4 zqjT<7uN!qRmDeR+wEf|H<>lJ*Q{t-6vzJRN5B4VZ*SU^{G5!+)_kTr&5o2C2Y1|9C z0vJ#tfHaPd&auMJ>h533@7uIB&kHO)_x*fJ8_&h->f3jjszo@9DmziC#5W}x)E zLEr*nnWr|#U3qyL8fkjwqX7buI0LtBLL&9NInl3QK>${*cM04N>GVG|-1Tpg@s~d9GZ_t@h8_M z{)i25Hp{`4B2atG<+i>!1{I!PrxzDnC%K;@HxDH1L=d<7?(9$gdo8XtXUTY7Hj+)~2xpLVq4;Tl%M+KSzFa%+Hx#>H9;3dcx;0}$4 zUL-rB!ImHVMbPF#x;TBAkgq4#bRs_QykKbZ*uc|BcCm^!=$YBo1V>kwTPka=X=9f6xL9Wa?b-;!g z4x6&SdlX?LJ{QLX*;l`3qXx7I8C>K&!^bAO7*AS586bHPk$sA7-_7vQR7J%k5y|v< z;*G{YSgp4+=oWFZ07soAA>eUSP)$harn-ng0waReCko)t`n>=5l^4|;))3yjW?wih z^`*A3_?zR|L;wc|IYSTL%uK;Z3TJqbS$`D8gP!9OqyYfRUdXQLVzm-iGUa1jdxDJ4 z(Huq0%;I83Aygsh(A+%?aGTS<^0W%ud{h=3JhJjWt_WILuzpYNAAyf3(w$bjrDbK? zm;qdNQ6k6}3qRk)QYJEAoA@TQLPi73OE3Q(QdY+8^y(LPlSBJ5z{{M30i4??JE14o3{K zNQ&P7{Rr4Iu{mn*_JSjw8ueSwM4PD?KQf{*B?JQ*a(*3W zMIh$IFk}P9Ym`?KTNgaQs^5|{eX3a(EXG<^fpKxLM?5^&gn;k>pc|*W6{WK89T0&N zt0O_LWsacDA_tLNUT@_WEK7HSG!X7Y*esj`fgk$LDW`ou2OI(%lqqbhMhMjMOLZ=d z7ie9SZ1;YQc!cM1&pVcrB+q%F)}~=s2Y38qK|?NiH&RjLgV^M|Vz4KG**KYfeA!5> z7*~R^|B`nli85HLEAqJV(x{7iZ`)xXh-M^m-a%0HY#Fc5DmNT-jt zQeKdUXZ}z#?|GwXV^a<>Kr)=jzQZ@)fRu-|3RC+UF31CosYZ+!s}x^~vlYnRcJ|q@ zOReTaAORl!a0tm*d6%Gbl9GLxGe9YA+*VlNq2l%q%o9{OFMgpZJ+z#V;7_TW2&%ga zR+KaikxoaG_R96==j4o^-d}-d3&M4|`pW_LRT7=js^+TKfd@6}=y=c!4#%NW2u4@D z;VJD*6EhKTX0!}Bhi$<{_3FB5Y;}^iU7wQpay0E%FWCQPqx2F9Xqzb9_nPRlU8L%0 zYoJS6-y(-yyyR>BA-&-Z403YJGKMKdwO-g7FSyy5*%;%0U~8b`tN#Je`hUr(jX1@% zS+QQZ+CT0+r`EUZgxy=VR?INA-@#J5vr6ItcUyJ>sH$4~MYmRdOQIW++&q9rL&<2)0uuE-*Lcfcf$_l>3Ty4FBFuA{aabKS8`lT1s zh)dR-`hIGwNEk@*{xnef@she(|H@gNEU#hVmT3>w`bqXV8hNbnk1(j>6&hx4V|ap! zEHSGSuM*ILJq38J=)rvQfLQd;lWPi>72C{#{%gmAiRCFVpnyZGQEvm210LTPc>M13 zQbl*(o($3@zNzvVW-QgYMTsZcqRZzcevDI|@;tpYkLUdLVD6C-vSsV4uLYw!IMxH8 z5PW_!vlx>F@Nh!1mf7$RS}n{38?6y0t&xbjgISGS%E(VY(s@ERKmQwLj~4K1QplYC zY2F6=r>gMmcB8qdPq5|;*IXY~*QcM(Z?V1=rv2~NzVw{RaLV19s~=5&l+4<#N8J6l zjUyW={$Fj~e++1Ey#6q%McNDyX-*CGb9}D}O)cHL<_bUC()A zZ8EAcp>9@I`(dDxg*fcUeg^rlhHi2%Fa4wZR+C|=YqXWh=T& zJP^G`9Q42F3;C_o)z{T3=}}}1%@Rsy;iUD$?e^X^A9Zh)Y;QqYXcLTkh4#pBjysnl zjyjUU*%U4#Io$QUPWGML7|F+f59VNvwdUq}sKaYvUEVvIa1%)0T`A(71id3l^a&cx zcMfUR-R64Q%;g!w8!Q&l% zDM&kdxqSYSXmQ+#d&=3veCo>MXfL&46g=M{v=W_xbCZSlN@t%@XlMIZuj=YI_qPZ9 zsOl3o@YqgmCnim_9T~?v7jL|bzC2^vXOkgN?77=|I6o`9%$QDb34Fy^8SB?u=MNgU zl)5?94W>=Xz|WZ06jcanoJQ;%yOiG3b%7Nai!4pLq{QyXaw9KJb3UEed1Xt1YEl!_Sx^N^T3xOEq7Jn#z zwAkbz)~>fNeCQPdZQSQ$A%t%A$!Caf74l7Y3Z=O!JD^0SbEJOz*FYW_H(|SGm?2(pX*t3`ck>}Kb5kNaT{L1YgPN3gRdmPCP5Vdc_ayQ(qDi2x zZTTlqt0o)~4+#S!p_GR8{DbTO<1? z{_y3#?vTB~(>tf8ao0s9>(>Z*$*dQdEH=hJTlbhw&dmI77(eH*1HHWB2NnvSKQ@{L zDe_=&`c6M<+zR+ZPZ;G2o_9G@uHylb6Mm`!V#nHJhE}6_Wx@zmpH2&$tOTCfBrj z#%^{Bf@OFa?-$vNYb9Mdp=kEdrycHi6xx(PzT%Im0#)LsOZViZ>n%!Zyi@AqEKALA zV^JIw?McVkv&%K>0@RRE_ncfFr(Kz>Zr6w*ryrn*2)G^oUa({POZ0IYwI^@B#NE@e zY|bB(AAH}}rndTeB6f`|F*2463R9;|eRQDb6x5M$D98=rfh9GVzz$O@Ic98GBtP{| zaCm$t<#R_-)0hj(jg*rHjoYkV_I}@@=XmgKR6smrpEIU)v*py2Rn%2x$$>fvDWBpl zv*A+|WoL(?V-ttpx>w)&%jvjGXMI)aRU;-$oXWye88klAmNEOP-(cN}1TWcJK#eN) zyq$Fh9uV>deat`%*(_=*NNA(rO6Ib@{_*H}6FkTZVTsXDlmOwOT1=cWaMQyl9d0u$!if&V?w>IrS(#e?(}|pzHrx^AZR;&YuNEC z`;Uc(sOOxG@d2Q`&4A(Qed|>pN&UDQ!Pi`g8Ef^>DDop2N%6ftxn%JxtO6!P5qCiz zrm%$4d1>1Vjj*gP=ejD9-b!hl2eSxX!ec5DmYj@%<3;KqiVqT#kh%nu?Z49&$0aQ} zLoHbMUQb6nGV+{_C%+KwUn3b><9b11xZU=xhCle7V;BrJaV@|@s z#eijZscBkcr)|}d_y{ZPZ{?`zO(yLj)E2@XdUvGonp6oDT86(1icjyKE;QfBFEVw_z!Vb@tiYMM zw8SmlXT-e}K(0p={xkEaF7En!!5`43!8wn6$q$@YOXKBUjQI|k-F+j6_E;5dLR~UF zv6Rp518-9}y5q_3bfZ{g|E767i1SZBI1V_b;>acaV{+`7>calrO4Q02W)%Jlssl%S z3G>@F$(($YWzR2%QXHAU_`IumuJAtd??*Ga@`vL~@SLE#7VPddO_2H{{ z{nfmWXmtv{;W=mdT#(xzrD&>y7+;$*3woxgjXO5c&>OY&es^*s^`6d_d9xmgN72%e z`H_7&JtIO_`ilOWaiMO%&-8)ZCNGnHbLmeSnwZ6KDAlGA;0ekcj3RXut<*h;#cm2k z3_umvef1;iu#3>6GGmo?voVrzbF4jd4eZD@`(Q61u^6f+AR!PHfVoAIcg=d8I%NEw zbLLIp_*Rx_8#%se!aHzV8NK?oS0A9mnX0w~l+&etItUy7Xk@l-O#uw1wosn5+o|}} zx6UnQ-Pm}BrG#76lJ>;p98ow%**zY)6(tqF0x4P%DOe7@!*zAV7`^(84(qwUT(8yP zTmE|$H(E|%VzxVYw~+*D=xe~1pgXlU?j79XA;^o5OOTBv*~Ca6gpKxWVsV?ZB7 z46q{Qz?+aM$N^+udfKNO+!jp1rTlkZeQ>V4v_&}qKjv#k)6;4eT2*GXBCJ29HHW?~ zGOPhP_E7l*J3}sLbR`JU=w>aT#Kh*_k|;Ywd}Z(1Ju+uB=pDTFAoimGQC>**?o=cI z+yA4#MW9#qpdV2SeB>q4CundZq?1Y2YsxGQ5GL6ad&YF$7Lp2|H|LI<3~KR_P{VE= z6XT2Ph2W!NYE(guut*P%q?3&!$&2jr7TTdVaYF6eB3rL*OU&>T%9(Sjd3=-NR|(^& zyH6G8b%ngE2;vsZlM>J%Sq*lc=e7$` zPERzRg0_F*5UtK|PIWW>TEz|c;q=@mbZDKs(~rpQnCP;%Pnp2B$|wJ7S-fVuJfjcy zl-j+co;~0?`r)I4V2=4+Fhq}eHWf88=~F-k8d zQ4&cjX)9x9Qc3$@8iQf~cE~(S4hm?nm#_{SSlW zhBlO0UCVB(4ltKJ6Uu|v4iIf=n!bf<>eMjOSc==*VI_jstL$YymwRlTmy>>Yi%4Y!Y zXf5v!`CD9+>COAR!#I_8*|+8W8aKP&BmTbHQnFLjevW56=YgIH8VQsJ4|*~hzv!^e~EEtw_KMIh{i+_dYp%^1vfn_3vkxw z*@AO^lz;P$FQW*DZ3-3Vhci9T}NL2i21Id$7(lB;=k|O^(>ZYQ(Jl+aS znnU)yGo)^oR)NilplT1lrG=d-8&+<&I*abUkMeZ2#Dh0eh!KA8SMAlID{+w~K`1gF zwx$w6Z-OG8hBtebzvq;bMwslOTYF;;{g53Vq5U>@fKrPYP_zlld6-qg(;$!pz9uCl zb(Mf0tO*G1fnOynl~j8OfggH5{ZIaYd0>3J(BB{7U{vte@85-si;K3=z}DZ=#wH)A z&|R>XygWocH}I^A%F7E-DP2YMaVNIUfQtM}#6aKw@OlJLKfJgxlmQun^K^6{Y++6q zS)jPO?D6(-BYBo?arZ_*-)xb5*;=V{ZX%J#VLr33ZF_^zg|uFd^uavJpf(h&zh z^UP5Xh3nh+5@(^7v#AOx0U@s~M*ApcQ=9P`g@fyk&Z}2Fr^umEg*6d}woBSNOTX43 z_P;k>8R0Ci7x5)nE)u3O`|nhBH0)Y2?vAl?fi3gFXjl%taF{po*`Y6&_G9KZht?AR zG`sh7g*wB7+fnG+rx04?B~C@1?xGWUpx1-92~e*1;NguA59c0~H%9@@J+>>Cx%1lBU3QeY#kxQCv_-8t3 zl&0Ao2E51o^@RhVULh6>|3+t1fw#e%;E{3jjZoR&Cy^lN^|D8E`|^`M$iK})Q?Qo% z83MAiw$4*4QJ(~A`IfND{kWe$#fyrHOw7&ceEizmg-0_4QyCcNKaS43-fleer>Z_7gFVL-ur5HhI`<8 zK9GQsnO%>nwD)9*>V1)+qN3)PX04=v?(5VW?vC4B?4HS;qh)g!%25}+5wT2Kpsdl~ z^WwYICm=RyT0i8rU69KFSU#HfI6niX#lG2CRof~9#fZ+!R48{G zdA{kmdhBqzAoB7#GALTl=5;haypku-%ex266e%eA-IihtxZGj=!cQPE_eIOX*uuiX zLvE7+T-brZvkYLISc&ohM&j(1Hy?i_js;pPyj}vh)am`~>};SRgnN>AH%**-)s;ri zr zh-y_!L0Mu1AZ<7!p;2#2)OQ6O5D-R!fe1+?iTTfl?7Imxqt`tfGpF+r8f$Q)j#bM^xkd|5M@Pp8m=*Rf#)T7$&x3UEex8$)^T)rJ{=r@b;mZkeGrVV@ z{Jjq(Hd)RGv!NE<@j%xTAhX>aORvJILNZUYM*$MqK@T2SkxJnd@X)M}q%E+2C6{}- z(K5jsCca6$<9eSq?sJ-L#s~SoOluKDUM);O5q}i-xemWC=9{yXLm)_uVLWJrWT4d)6(jjZS2kMjkn9G*T$gj;!nv& zZnH+F<5*Ma&%-&%OjDn(*A=b73)SnC8mg@M2`+5z`%#K zBN8;0W!^N7o~DY57~6&o6nCI+j@Ao9BeMM^MS6)#f!F?BKQ9lDbjQZ=F$=sOeA`20 zoIB8`xB|4oza~gyy1K&J#0-}mYO)ET(XF*jz4JnA{u15$F7Zuf+^2HzVnGduE^S&l zEhO7fP<>Jf<9oYvA!}JJy^}Z!+!$Huj-y=KvCOc#yNt|&SLZi}uS!6;Gp^ab!_siMFaL^Q(5t^yPF4yBi}m9%CM#fTVd72cv9c=|&p-Q)9mlu?IZp zAy!ycknG&>ZcwpfsWS~>n`y{zCU%0vxSvZ8@jD|J2^8 z_PFtG9vZGgLT_8&aqItpJg{=3(NC@6>K%M@DAmh!+erpqw%eS(MGlF%$;rwkHeB7j zX?t6qXJDMw)OMl)%l>s?hP%w-1Zo;zpD&o_66;K=msj_J;`_4bi}(uYO#R+0hqdp) z<6b(N$wV&{&|O|sbX4bs+}x|T>bG+vt8Sf_o_-R`XkZFjq?DaiGL(+H6MQ*s&ek#e z@!X$R>YZ>8Eb=BD)dl7lzm^=K_J{mpW`0L(3&)1}YE2>c-AE1~c)M^Z)v{Zdq#a1; z*k~Tp0bwt_z6r9D_=3e{cYz125Z~{dF$_yPdB8lsqz&h+8F+||8jsFjlUO$YgX6eK4-wCI zR~tLC;}stw%L(TMocv8nJ!TA>zb=EJxzeLQ+9b%uY@iLTtzPxU6`7#T+eu<5&uoPO z&^P?KIad+;H!#3N8jmb&4l^YDvEFWt*A}kz&7Th)wQWl$8vg)H%$*(i2JwF7e#8-4iu|4!$KlbY~?k&dAnoBjAJn|SLya4 z#zMHvXuOND{!|>EjI@PjGGyeu%A3;S^{4My+b7Zh%bJlVy;FCYk@S0GEb_Pnjj2nm zi(kb}9=JxnrU-dgcqAm{l+@B%O+zYuKl%{DL{N@Nl4d6bO&a0)4Q)w&c7j}vsJw)x z4r`c2;ZBbx25XGJ&n>zrS`d^i)u_z&ne(|r49b-447K8;5%?Q!r>bO<>rLAS623S> z3Lm&hcHoSf{Gc>D;Yz6Z9yivn?5M3d@>dPN$&jsZWf4mw#UnOcvG6iK znuZ+`Q>@g=W8Pqc`l6dP!+Dg`?CtHrV?dX9-5p6xax%ldBY8-TtCCV|X=iT77-`;2 zq(>_tjeC3gL1fWuD(|zhZ@t+l=|V77Y-bYF^pP-88a}e_Y`v*kHT(AqP01*xztZqD zGZ>wy^cJZC-SN7+zfDAMhzl`xBig9{NC$}%RALMGoM~5EjS1_%g-0Z(oB9{U622_} zx0~5*OMcKLZ7NQ^;o8YUBmt$Gy3kyU`r65^S#L=koqcjY0=lNBPu*^jzGhDVB;Dst|C$ zA`t&O0 zEn4+ZOS&ipf82X zAML>*8Gg0{)G@0SxZ0svI`?yucy{C-)~Ia)ny~fKp;{F;Dbs6+Ws!1jrIn5~));mV zZW{|_%b`MCUJNlDgsBTkD#iWGUXvgvp!inUtDyd4+GZO(Zw9~XGaTJsp@QRVos-*d zfPI28wE!9JJ2KTY*KhZ3C8)DmsFJ+)n}51rdrq*S4+O5CUmyEhS2Z)aY}vYhS7hVl zek32G*muv{QOa`xTrM&*yhsmqOSbH5-=my(U?DNNAFwtf;o$x44g=aCLaNQe=L0l9 z`lNgS;N(ZG5y4dFU2{`Y%DZcPUzNItcy=h(Y_k0`$MXASM#f}OG|{rQZ{C$Y4j;DC1)&#Ixa+(g88P1D8- zs-tH?lvW@sx0uY4+=QG3yhp)RS@sU5vh6H0>RX;+s;KqxUs9`#*%|yEmgs! zq^V`8K7=1Xex#?S_WgHopwK{pW6ulhA-9XH3&F0dd>PJdz&sG!ah>NPk%x!J$D)RY z`KJB6yB8D2vg7#CoYdsz2B$0K1HcWHr2^-8Z#LkW0-mEme(JCSp0d2({S6blG`kZ6 z`xuskO)CKB_pQ~V{iFAT>zz?_c=$n`7eJL&QH%?5TiDqZ0vbxdcUsY#QAh+FJy`{T z&n#ehF->{Lm_5dJATaSDr$_2kPV9^$Yl*=nJ~z(2FI%7FJ~fdUu~InaJK z_hVmq~+wzYmolzu)BXt;{PZa7kq^r-CstF&%O#AU?GFx^*0F zU3?NL06tnWKE~DH)k*t6-303b9L)~Ua%-y)<)(;1v~zaa164+`Uc0y8%X!V|Wro?y zuvUCJ`8>k+>JyxEy5az0$#z0aKp>$Yct!An9y)a*6A!QfBLic((2&Rv@)PwLfB$zn z1lY&VlET;X#ydwEdd`F^4#Uh~0$j#JcDHkreZ;Y2V18u0vw90PF3I$Zw6xjfm?Ihy zq1s_v22T?SWzP{fQkSrXCf0T4DAI@gP^rGLgnXmAB;UcAp5R8FwSvdLQvMh}feO^bk zb3ZaOGszJ}B`X4sUQ{?QqOKRxPX=P*dx)F?dZGG;m(RWr{-EV>+Ed)GX2lEH!zTG4 zA|xDf0f=J~*;c%6R%$$MY{McW;;^Cd(TE={_BMnH}+QG;|}Hm6u7VLJJ$faiVBi9KoZxO4in)~6X>;`^^sMrOin6A zNxGAE!Ha&FRn5)Gk-!C=zd(sL%MGzRBgr%f7?s>rNP9TDtruhbgEKSnfH&sT?>1O5 zlyB@Sbo0!&`FXDWiFS9f#_}hWD-Z+Q4VX8&8H2aH-n0yWkl;nlX;dCzBwWWuV2-I9 zV6*`&D~w`P`1myU4Gj$wz^R@F{OT-?|9klejx!vf<^5rWaTCD{&gul_yyZs!C1Da2 zp~m|nstS6rK&7Ol{MZK&rqWMGhlflTzVzak(R7=@ikgB%@gm$|2BzIoyGm+u>CVI}z60G#omx8TBs6kQKp7)!LL>Oix)Pe$8!_|QLgFXTP-fNKm zaIHyK;HxSM23H-}r1-V`E`)+DW;rkNZ4T zd)lG^f|Zy`>UlT_3^;fPdt#&Pod8!|PW!_)>&y4a<$JF8o9rvI6H;qSH@HWu`k9!i zl5Qc<27iNP?4f^z7iP3Hes*t&@@(+M3IVc`kltF+ycn?%GI2kmWfF;A&2|cI+caqH zhBOwn{JbqgOlv4l6~gSy7!Oq%C{2+3zvGsIz!_4lCX`-;ksNyg*PiDRSTr=!nDl{l zvJDFE!F{Aw^!w!nz;xhJ&80e|i(o{_;$;{Nt`tYSWkVk{iw4Y31PIcc)ukhi1{YjO zV+MqdBI79BPr4dgFfnP+Eo2M;Xnz!S2$YJd!->wGNswE5HP{YoqcqrfjsXG_b~|w@ z>3tvAe&IQ#affqQR20_<;s@@LNxoA>;H#guQVTDW)9w-H^_jxi_dps#g5BXH{5Ao|dYA!z{Ec53nhI~524C+Nl^v#9nGz;*rv z^+u|C>lEH|`$DwCZUJytq6XJuJ*c!3;L&X|-b>g6GN&J5eLU1LxE@gUJ=vaEIT08z zZTEMgw>=hwA{M!W1n$YGEnxv6U{!)VibNC+R^8S{g4n~w#-`*?UYe>CP8y+nEg@8?z#Rz%(?bhe_;# z9QbX(O-V}4!Ms@FU?Q@8Ud8ew-@eD*CTWdmcl_P~8B7DZ*fG77ucgl(F> zV%)=%&IS%iOqL=RKCkek65!#{ZBc8Gv`9T!{sCK}eFbavo@$5izZsCNx~ew)U-}01 zqpE?<;PnO&{jGn$HoY!AV|QgAdZKA*C`~VxUm!cT?*Nl#g$V4!Y|lmx2)z$h3m^Dq zNlC;E@)3+3v_|pxqqjm8*|5I(e!hlDsdY42u zJAtJ*fC}I5BHmNUdd>FnEgj9~C%%=2r(fY85HOgesFQe(YNnm!fK}hS*G!V!dj6}! zme(s@^tHZ4*6iYcTg0_*)`v;tG5oP6{eHDQoDYM^UG0{+kcpwS7J%c4g>gZN9{Gj` zAp0H9W&yrfVkG8uUn2i89%=*a$UJs`xU*J+a~kn3>aW#LS%yc4)b8|cT}_<6@1FqW zYu@b;3Hz)^<*Lnct~WS_G&l>SDauGzWW%GtBmdx81kcz9GK4h!(nkH{%!@=^i#U%u zj5Qy8fKWkkQH!aKNj*Zs&$tykCgo1K2|e&VcvTo({GgQkDfHr#;@9Qz$RTfCnm?ed zCJL*)h62|OK9Yr3(2qz&3$HFC$590g_Pn_tKK^~Zv-f+;iNrgVk z4N8!XHNqk<(ud-%lD(y+{apOUYRVNyD>I}+ApDnv1LF$YR>U;d3XRGP z?@jq&YtF7)b+*L``OAY14rc2B#!8ujEIPHh$m=-wsF3}6sD53_K;Yo$G(Ss-s=_`j z^C2)oxe*Z11p1I;>z^d~d5qEc1IY>R8WPt!J4MU$l?lrc(>7XM>eS9ykJZ0wIH76gNds!Y`}7Wh4%aEq>zTjQ0vr;_quK8>#Ie3=F%H+Z7RoV z7OonkVPcZ%Sof17&)Kab8!F>wPOqcz@zwLWeb}M`P{vE#M=rDesD zltLp_8I<$rVgC-ehZT?&Yl6DC^8DG%O2@%iYfF#IFXt~iRKhbM--Aztbd3?w`8Oadp%gj^&cEqTli= z9$jMcL+h*bO{k%cw{-YSSax-ez;u+Hi0Gb=7H`gzvJ9)aDuR2FB{#BY_{DEcI7}S+ytmZstcTIMo)9CWvi_;YW7^XJl zA@COkKC0$y9$+)T_1meiLr*yDa>B8oMsBVYAxA`Ax|SGPdPbImSY{Dn+I+$(;;4}M zM_dRq4l&WNb4dyJ;)&(HP%D`URO3Nm%h$F z%$4;|9rtuVHcL%zgDelx-{2R*dGA^Tm6W79W9&NFkmyX6sLh;)cq+e5WsmWBpW~ey zaV-U>*PV!UVZrgn5&NEzZ979pR2O4a-ccFi@cz}~Fq+uzxL>E*vX7!0%68?RASJ*Z zMkYydBah|4Pg?E07ILwe!sca(4hWsQZ5VzmIJY9bsa6go{35>~hl9dDy&Ez+CCSu; z<0u@$zx0%tet9Jjbo zn*5^q4R*K#Y`3pP&r)@t50KuQ*0K)5+(Zb0`pgX2gnj z_suK0n)5jw`>XxktI}MI3CrBzF<3lXtQVg3#P$pw-(o#x)gr0HNjn@k!#2ai*J~oh z$fWE)*S-+8H@OAVF-q(fM}6r#_S9D|W$u)3yYc|7Fw6-_Y5v3HdymsMsYr?V5g(UZ zw>c|@{^HClw%a)U8+YPO=I8Fa#V^(06W6Y}{i{HpWgp~gaCUq`K9R?5`FG{~^=x<| zd0+Le&sn_KGvNZfu)}AEjQSCuwpmF@jXAS=k}j&hQs2Y>FAq7+_s5kA1_!zH%mk>Yxr9Fu9})$X<1WnZTn2aPf7O25&<)TB&ik8PIK zohKes3+|(=CS3dOQzta<=n_>Z)5C>+bffYi`MH&Et>%eLM@2-oVI1T7OU@a&MWEw{ z*g@$b^#rlx#_>Ul+VBEmK z6K*?k#=Gg?pN3O714#@Px89B8Wd|FIE(eIo-EUz~^=}PRW#TXI+ddgRJ50EuV{v;4 zQ8RV@UFWhnOAtcE%4E!0>e}s3eBc&;Hyy^A`oCH`&wn=Khi#jswTmcXS5>8UiCHT~ zjZz&HwW(TBdvA>u)hey6R)?*`o(Zb8TYJ*VXCGE?9!x)9ovWoa92!aB&E1CdNOP8c!}s5#yn6 z@rNS;2(DMYEeP8&bohSn{;m=CoSu71Y-Iu}X4com@|C<^iLIR6@U~(4bCUWY8%}|a zy;(fp=t|UwgsLD`$8DhzmqbOeOFviio!%6MTHlsekVuh2>J!gm*(8rU6uIvxX#a)0 zjzleN2XwduCIo-_HtVh?8nPPNcT12zyfXlm;hReauLrwm>%$yDN$JUgD)HMelSRQ+ zntz=Q$iS4HJ4zFP$XvK!B?~g~KD_7E5+2TmC}#$YxKj7A7^L?%xmo1>#CKzF!ZOo1 zcsEnZyDlsbuI+$sz)rHPTOpsU*~>tGs;RDxBY|1sy^$vz%4psS%%#4Z`ml`WPruOB2XNJ;_Fd4+ZP0q1 z@%chi%0_voGI~|0q&M2ET<8rmM0lITOY76R9gFe|U~7Hqx0i@I!iB@!m;Njm*%O;8 zV)%^SXD&^w!%4jv*0!hAPJ2j&mRt3ou(GsN)kKFU*g?)MXrs>mN@Rv}40#5@03VD0 z(>2PXR!)@;pF&`NSbPrPx&LlYq%3V+*;=4SDi%IzjC`+!o$bXES9|97@?4{5X+H|x zPxBfWf&E;}*l>byNrz4GJ*W=lIa#iB?T9hipZv=wOrwl_MtkFRa%6aGs6I`-GV` zD^iBi;?<)IrhnlmzB5;v2i?dfdQsB1C=qM?U6W~z`BPZLH3~KQk#4xRPJlSgsb5@C z{gcO!6D7cDUO@4mxNVjGZ!d}kjmG9#ZjJN=^^$y-1=7+FeV0YO_cGoLD=ddz6ESZz z9zIR9Y*k#9m7q9;79#bdTBsw#hOZ;DcaOa1^F#-TE;d_=`NGQ`zDhgWY`!YL3Lk=?XFLzxs9dR1f==E@7_&UN#bJis0 z@Vpz!u+0qNMT=mijgWmd44Ii5_t|}dM#yFRwGFhFHSuiT>Vd|EDDUWK{*p0E-ns7? zv>Xj}u3}(Mww5lAhFC*YBNla7tH|i!Z-3>?xQVvU7zH1k`KL#P_yY?y;¶*^3m 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 Mh0s$gQLza7f6y){vj6}9 diff --git a/docs/development/navigation/node-types.md b/docs/development/navigation/node-types.md index 8db44087a..91c3285d3 100644 --- a/docs/development/navigation/node-types.md +++ b/docs/development/navigation/node-types.md @@ -2,7 +2,7 @@ This document provides a detailed reference for each navigation node type in `Elastic.Documentation.Navigation`. -> **Context:** For the acyclic graph structure that these nodes form, see [Functional Principles #8](functional-principles.md#8-acyclic-graph-structure). +> **Context:** For the acyclic graph structure that these nodes form, see [Functional Principles #8](functional-principles.md#8.-acyclic-graph-structure). ## Type Hierarchy diff --git a/docs/development/navigation/two-phase-loading.md b/docs/development/navigation/two-phase-loading.md index 134a42f0a..15c649614 100644 --- a/docs/development/navigation/two-phase-loading.md +++ b/docs/development/navigation/two-phase-loading.md @@ -2,7 +2,7 @@ Navigation construction splits into two distinct phases: configuration resolution and navigation building. -> **Overview:** For a high-level understanding, see [Functional Principles #1](functional-principles.md#1-two-phase-loading). This document provides detailed implementation information. +> **Overview:** For a high-level understanding, see [Functional Principles #1](functional-principles.md#1.-two-phase-loading). This document provides detailed implementation information. ## Why Two Phases? diff --git a/src/Elastic.Documentation.Navigation/README.md b/src/Elastic.Documentation.Navigation/README.md index ee93b8fcc..e526cecdb 100644 --- a/src/Elastic.Documentation.Navigation/README.md +++ b/src/Elastic.Documentation.Navigation/README.md @@ -1,173 +1,331 @@ -# Elastic.Documentation.Navigation - -This library provides a way to build navigation trees for documentation sets. - -## Documentation Sets - -When building single documentation sets you use docset.yml to declare the toc. - -When we mention urls these are rooted at `/` unless `--canonical-base-url` is specified in which case the root is `/` - -```yaml -toc: - - file: index.md -``` - -It supports the following children - -### A single file - -```yaml -toc: - - file: index.md - - file: getting-started.md -``` -These would result in the following Url's `/` and `/getting-started` - -From here on out the expected url appears as comment in the example - -### Folders - -```yaml -toc: - - folder: getting-started # /getting-started - - folder: syntax # /syntax - children: - - file: index.md #/syntax - - file: blocks.md #/syntax/blocks -``` - -* if `folder` does not specify children the folder will be scanned for Markdown files. -* The url for the folder is the same as its index. -* The index is determined by having an `index.md` otherwise it's the first file that is listed/found. -* `children` paths are scope to the folder. - * Here we are including the files `blocks.md` and `index.md` in the `syntax` folder. - -### Folders with a file - -If you don't want to follow the `folder/index.md` pattern but instead want to have the index file one level up e.g - -``` -getting-started.md -getting-started/ - install.md -``` - -You can do this by specifying the `file` property directly on the folder. - - -```yaml -toc: - - folder: getting-started # /getting-started - file: getting-started.md - children: - - file: install.md # /getting-started/install -``` - -* `file` is the index file for the folder. -* `children` paths are scope to the folder. - * Here we are including the files `install.md` in the `getting-started` folder. -* deep linking on the folder `file` is NOT supported -* It's best practice to name the file like the folder. We emit a hint if this is not the case. - -### Virtual Files - -```yaml -toc: - - file: index.md # / - children: - - file: getting-started.md # /getting-started - - file: setup.md # /setup - - folder: syntax # /syntax - children: - - file: blocks.md # /syntax/blocks - - file: index.md # /syntax +# Navigation Documentation + +Welcome to the documentation for `Elastic.Documentation.Navigation`, the library that powers documentation navigation for Elastic's documentation sites. + +## What This Is + +This library builds hierarchical navigation trees for documentation sites with a unique capability: navigation built for isolated repositories can be **efficiently re-homed** during site assembly without rebuilding the entire tree. + +**Why does this matter?** + +Individual documentation teams can build and test their docs in isolation with URLs like `/api/overview/`, then those same docs can be assembled into a unified site with URLs like `/elasticsearch/api/overview/` - with **zero tree reconstruction**. It's an O(1) operation. + +## Documentation Map + +Start with any document based on what you want to learn: + +### 🎯 [navigation.md](https://docs-v3-preview.elastic.dev/elastic/docs-builder/tree/main/development/navigation/navigation.md) - Start Here +**Overview of the navigation system** + +Read this first to understand: +- The two build modes (isolated vs assembler) +- Core concepts at a high level +- Quick introduction to re-homing +- Links to detailed documentation + +### 🎨 [visual-walkthrough.md](https://docs-v3-preview.elastic.dev/elastic/docs-builder/tree/main/development/navigation/visual-walkthrough.md) - See It In Action +**Visual tour with diagrams showing navigation structures** + +Read this to understand: +- What different node types look like in the tree +- How isolated builds differ from assembler builds visually +- How the same content appears with different URLs +- How to split and reorganize documentation across sites +- Common patterns for multi-repository organization +- Includes actual tree diagrams from this repository + +### 🧭 [first-principles.md](https://docs-v3-preview.elastic.dev/elastic/docs-builder/tree/main/development/navigation/first-principles.md) - Design Philosophy +**Core principles that guide the architecture** + +Read this to understand: +- Why two-phase loading (configuration → navigation) +- Why URLs are calculated dynamically, not stored +- Why navigation roots can be re-homed +- Design patterns used (factory, provider, visitor) +- Performance characteristics and invariants + +### 🔄 [two-phase-loading.md](https://docs-v3-preview.elastic.dev/elastic/docs-builder/tree/main/development/navigation/two-phase-loading.md) - The Loading Process +**Deep dive into Phase 1 (configuration) and Phase 2 (navigation)** + +Read this to understand: +- What happens in Phase 1: Configuration resolution +- What happens in Phase 2: Navigation construction +- Why these phases are separate +- Data flow diagrams +- How to test each phase independently + +### 🏠 [home-provider-architecture.md](https://docs-v3-preview.elastic.dev/elastic/docs-builder/tree/main/development/navigation/home-provider-architecture.md) - The Re-homing Magic +**How O(1) re-homing works** + +Read this to understand: +- The problem: naive re-homing requires O(n) tree traversal +- The solution: HomeProvider pattern with indirection +- How `INavigationHomeProvider` and `INavigationHomeAccessor` work +- Why URLs are lazily calculated and cached +- Detailed examples of re-homing in action +- Performance analysis + +**This is the most important technical concept in the system.** + +### 📦 [node-types.md](https://docs-v3-preview.elastic.dev/elastic/docs-builder/tree/main/development/navigation/node-types.md) - Node Type Reference +**Complete reference for every navigation node type** + +Read this to understand: +- All 7 node types in detail: + - **Leaves**: FileNavigationLeaf, CrossLinkNavigationLeaf + - **Nodes**: FolderNavigation, VirtualFileNavigation + - **Roots**: DocumentationSetNavigation, TableOfContentsNavigation, SiteNavigation +- Constructor signatures +- URL calculation for each type +- Factory methods +- Model types (IDocumentationFile) + +### 🔨 [assembler-process.md](https://docs-v3-preview.elastic.dev/elastic/docs-builder/tree/main/development/navigation/assembler-process.md) - Building Unified Sites +**How multiple repositories become one site** + +Read this to understand: +- The assembler build process step-by-step +- How `SiteNavigation` works +- Re-homing in practice during assembly +- Path prefix requirements +- Phantom nodes +- Nested re-homing +- Error handling + +## Suggested Reading Order + +**If you're new to the codebase:** +1. [navigation.md](https://docs-v3-preview.elastic.dev/elastic/docs-builder/tree/main/development/navigation/navigation.md) - Get the overview +2. [visual-walkthrough.md](https://docs-v3-preview.elastic.dev/elastic/docs-builder/tree/main/development/navigation/visual-walkthrough.md) - See it visually +3. [first-principles.md](https://docs-v3-preview.elastic.dev/elastic/docs-builder/tree/main/development/navigation/first-principles.md) - Understand the why +4. [home-provider-architecture.md](https://docs-v3-preview.elastic.dev/elastic/docs-builder/tree/main/development/navigation/home-provider-architecture.md) - Understand the how +5. [node-types.md](https://docs-v3-preview.elastic.dev/elastic/docs-builder/tree/main/development/navigation/node-types.md) - Reference as needed + +**If you're debugging an issue:** +1. [node-types.md](https://docs-v3-preview.elastic.dev/elastic/docs-builder/tree/main/development/navigation/node-types.md) - Find the node type +2. [home-provider-architecture.md](https://docs-v3-preview.elastic.dev/elastic/docs-builder/tree/main/development/navigation/home-provider-architecture.md) - Understand URL calculation +3. [two-phase-loading.md](https://docs-v3-preview.elastic.dev/elastic/docs-builder/tree/main/development/navigation/two-phase-loading.md) - Check which phase + +**If you're adding a feature:** +1. [first-principles.md](https://docs-v3-preview.elastic.dev/elastic/docs-builder/tree/main/development/navigation/first-principles.md) - Ensure design consistency +2. [node-types.md](https://docs-v3-preview.elastic.dev/elastic/docs-builder/tree/main/development/navigation/node-types.md) - See existing patterns +3. [two-phase-loading.md](https://docs-v3-preview.elastic.dev/elastic/docs-builder/tree/main/development/navigation/two-phase-loading.md) - Determine which phase +4. [assembler-process.md](https://docs-v3-preview.elastic.dev/elastic/docs-builder/tree/main/development/navigation/assembler-process.md) - Consider assembler impact + +**If you're optimizing performance:** +1. [home-provider-architecture.md](https://docs-v3-preview.elastic.dev/elastic/docs-builder/tree/main/development/navigation/home-provider-architecture.md) - Understand caching +2. [first-principles.md](https://docs-v3-preview.elastic.dev/elastic/docs-builder/tree/main/development/navigation/first-principles.md) - See performance characteristics +3. [two-phase-loading.md](https://docs-v3-preview.elastic.dev/elastic/docs-builder/tree/main/development/navigation/two-phase-loading.md) - Find expensive operations + +## Key Concepts Summary + +### Two Build Modes + +1. **Isolated Build** + - Single repository + - URLs relative to `/` + - `DocumentationSetNavigation` is the root + - Fast iteration for doc teams + +2. **Assembler Build** + - Multiple repositories + - Custom URL prefixes + - `SiteNavigation` is the root + - Docsets/TOCs are re-homed + +### Two-Phase Loading + +1. **Phase 1: Configuration** (`Elastic.Documentation.Configuration`) + - Parse YAML files + - Resolve all relative paths to absolute paths from docset root + - Validate structure and file references + - Load nested `toc.yml` files + - Output: Fully resolved configuration + +2. **Phase 2: Navigation** (`Elastic.Documentation.Navigation`) + - Build tree from resolved configuration + - Establish parent-child relationships + - Set up home providers + - Calculate navigation indexes + - Output: Complete navigation tree + +### Home Provider Pattern + +The secret to O(1) re-homing: + +```csharp +// Provider defines URL context +public interface INavigationHomeProvider +{ + string PathPrefix { get; } + IRootNavigationItem<...> NavigationRoot { get; } +} + +// Accessor references provider +public interface INavigationHomeAccessor +{ + INavigationHomeProvider HomeProvider { get; set; } +} + +// Nodes calculate URLs from current provider +public string Url => + $"{_homeAccessor.HomeProvider.PathPrefix}/{_relativePath}/"; ``` -A file can specify `children` without having baring on it's children path on disk or url structure - -```yaml -toc: - - file: getting-started.md # /getting-started - children: - - file: setup.md # /setup +**Re-homing:** +```csharp +// Change provider → all URLs update instantly! +node.HomeProvider = new NavigationHomeProvider("/new-prefix", newRoot); ``` -#### Deeplinking virtual files - -```yaml -toc: - - file: a/b/c/getting-started.md # /a/b/c/getting-started - children: - - file: a/b/c/setup.md # /a/b/c/setup - - file: c/b/c/setup.md # /c/b/c/setup +### Node Types + +7 types organized by capabilities: + +**Leaves** (no children): +- `FileNavigationLeaf` - Markdown file +- `CrossLinkNavigationLeaf` - External link + +**Nodes** (have children): +- `FolderNavigation` - Directory +- `VirtualFileNavigation` - File with YAML-defined children + +**Roots** (can be re-homed): +- `DocumentationSetNavigation` - Docset root +- `TableOfContentsNavigation` - Nested TOC +- `SiteNavigation` - Assembled site root + +## Code Organization + +The library is organized into: + +### `Elastic.Documentation.Navigation/` +Root namespace - shared types: +- `IDocumentationFile.cs` - Base interface for documentation files +- `NavigationModels.cs` - Common model types (CrossLinkModel, SiteNavigationNoIndexFile) + +### `Elastic.Documentation.Navigation/Isolated/` +Isolated build navigation: +- `DocumentationSetNavigation.cs` - Docset root +- `TableOfContentsNavigation.cs` - Nested TOC +- `FolderNavigation.cs` - Folder nodes +- `FileNavigationLeaf.cs` - File leaves +- `VirtualFileNavigation.cs` - Virtual file nodes +- `CrossLinkNavigationLeaf.cs` - Crosslink leaves +- `DocumentationNavigationFactory.cs` - Factory for creating nodes +- `NavigationArguments.cs` - Constructor argument records +- `NavigationHomeProvider.cs` - Home provider implementation + +### `Elastic.Documentation.Navigation/Assembler/` +Assembler build navigation: +- `SiteNavigation.cs` - Unified site root + +### Supporting Files +- `README.md` - High-level overview (in src/) +- `url-building.md` - URL building rules (in src/) + +## Testing + +Tests are in `tests/Navigation.Tests/`: + +**Isolated build tests:** +- `Isolation/ConstructorTests.cs` - Basic navigation construction +- `Isolation/FileNavigationTests.cs` - File leaf behavior +- `Isolation/FolderIndexFileRefTests.cs` - Folder navigation +- `Isolation/PhysicalDocsetTests.cs` - Real docset loading + +**Assembler build tests:** +- `Assembler/SiteNavigationTests.cs` - Site assembly +- `Assembler/SiteDocumentationSetsTests.cs` - Multiple docsets +- `Assembler/ComplexSiteNavigationTests.cs` - Complex scenarios + +**Test pattern:** +```csharp +[Fact] +public void FeatureUnderTest_Scenario_ExpectedBehavior() +{ + // Arrange: Create mock file system and configuration + var fileSystem = new MockFileSystem(); + var config = CreateConfig(...); + + // Act: Build navigation + var nav = new DocumentationSetNavigation(...); + + // Assert: Verify behavior + Assert.Equal("/expected/url/", nav.Index.Url); +} ``` -While supported, this is not recommended. -* Favor `folder` over `file` when possible. -* Navigation should follow the file structure as much as possible. -* Virtual files are primarily intended to group sibling files together. - -`docs-builder` will hint when these guidelines are not followed. +## Common Tasks +### Adding a New Node Type -### Nested Table Of Contents +1. Create class in `Isolated/` namespace +2. Implement appropriate interface (`ILeafNavigationItem` or `INodeNavigationItem`) +3. Add factory method if needed +4. Update `ConvertToNavigationItem` in `DocumentationSetNavigation` +5. Add tests in `Isolation/` +6. Update [node-types.md](https://docs-v3-preview.elastic.dev/elastic/docs-builder/tree/main/development/navigation/node-types.md) -A `docset.yml` may include a `toc.yml` that itself contains a `toc` and may include more `toc.yml` files. +### Changing URL Calculation -Given this `docset.yml` -```yaml -toc: - - toc: getting-started # /getting-started - - file: index.md # / -``` +1. Review [first-principles.md](https://docs-v3-preview.elastic.dev/elastic/docs-builder/tree/main/development/navigation/first-principles.md) - ensure consistency +2. Update `FileNavigationLeaf.Url` property +3. Consider cache invalidation +4. Update tests +5. Update [home-provider-architecture.md](https://docs-v3-preview.elastic.dev/elastic/docs-builder/tree/main/development/navigation/home-provider-architecture.md) -`docs-builder` will include the `getting-started/toc.yml` file. +### Modifying Configuration -```yaml -toc: - - file: index.md # / getting-started - - file: install.md # / getting-started/install -``` +1. Update classes in `Elastic.Documentation.Configuration` +2. Update `LoadAndResolve` methods +3. Update Phase 2 consumption in navigation classes +4. Update tests for both phases +5. Update [two-phase-loading.md](https://docs-v3-preview.elastic.dev/elastic/docs-builder/tree/main/development/navigation/two-phase-loading.md) -Note that the `toc` creates a scope both for paths and urls. +### Debugging Re-homing Issues -A `toc` reference may **NOT** have children of its own and must appear at the top level of a `toc:` section inside a `docset.yml` or `toc.yml` file. +1. Check `HomeProvider` assignments in [assembler-process.md](https://docs-v3-preview.elastic.dev/elastic/docs-builder/tree/main/development/navigation/assembler-process.md) +2. Verify `PathPrefix` values +3. Check `NavigationRoot` points to correct root +4. Look for cache issues (HomeProvider ID changed?) +5. Review [home-provider-architecture.md](https://docs-v3-preview.elastic.dev/elastic/docs-builder/tree/main/development/navigation/home-provider-architecture.md) -`docset.yml` defines how many levels deep we may include a `toc` reference. The default is `1` +## Related Documentation -```yaml -max_toc_depth: 2 -``` +- `Elastic.Documentation.Configuration` - Phase 1 (configuration resolution) +- `Elastic.Documentation.Links` - Cross-link resolution +- `Elastic.Markdown` - Markdown processing +## Source Reference -## Global Navigation. +For the actual implementation, see: +- Library: `src/Elastic.Documentation.Navigation/` +- Tests: `tests/Navigation.Tests/` +- Configuration: `src/Elastic.Documentation.Configuration/` -The global navigation is defined in `config/navigation.yml` and is used to build a single global navigation for all documentation sets defined in `config/assembler.yml`. +## Contributing -The global navigation is built by -* `docs-builder assemble` -* or calling `docs-builder assmembler clone` and `docs-builder assmembler build` +When making changes: -`config/navigation.yml` links ALL `docset.yml` and `toc.yml` files. +1. **Maintain invariants** from [first-principles.md](https://docs-v3-preview.elastic.dev/elastic/docs-builder/tree/main/development/navigation/first-principles.md) +2. **Keep phases separate** - don't mix configuration and navigation +3. **Preserve O(1) re-homing** - don't add tree traversals +4. **Add tests** for both isolated and assembler scenarios +5. **Update documentation** in `docs/development/navigation/` +6. **Run all 111+ tests** - they should all pass -Repositories in `config/assembler.yml` MAY be included in the global navigation. Once they do ALL their `docset.yml` and `toc.yml` files MUST be configured. +## Questions? -```yaml -toc: - - toc: get-started - - toc: extend - children: - - toc: kibana://extend - path_prefix: extend/kibana - - toc: logstash://extend - path_prefix: extend/logstash -``` +- **"How do URLs get calculated?"** → [home-provider-architecture.md](https://docs-v3-preview.elastic.dev/elastic/docs-builder/tree/main/development/navigation/home-provider-architecture.md) +- **"Why two phases?"** → [two-phase-loading.md](https://docs-v3-preview.elastic.dev/elastic/docs-builder/tree/main/development/navigation/two-phase-loading.md) +- **"What is re-homing?"** → [navigation.md](https://docs-v3-preview.elastic.dev/elastic/docs-builder/tree/main/development/navigation/navigation.md) then [home-provider-architecture.md](https://docs-v3-preview.elastic.dev/elastic/docs-builder/tree/main/development/navigation/home-provider-architecture.md) +- **"Which node type do I need?"** → [node-types.md](https://docs-v3-preview.elastic.dev/elastic/docs-builder/tree/main/development/navigation/node-types.md) +- **"How does the assembler work?"** → [assembler-process.md](https://docs-v3-preview.elastic.dev/elastic/docs-builder/tree/main/development/navigation/assembler-process.md) +- **"What are the design principles?"** → [first-principles.md](https://docs-v3-preview.elastic.dev/elastic/docs-builder/tree/main/development/navigation/first-principles.md) -The toc follows a `://` scheme. +--- -* If `` is not defined it's the narrative repository (`docs-builder`). -`path_prefix` is mandatory. - * unless `` is not defined in which it defaults to ``. -* `path_prefix`'s MUST be unique +**Welcome to Elastic.Documentation.Navigation!** +The library that makes it possible to build documentation in isolation and efficiently assemble it into unified sites with custom URL structures - no rebuilding required. 🚀 From 5ca4d6e67a62a05db62084987ad7a18014f3096b Mon Sep 17 00:00:00 2001 From: Martijn Laarman Date: Mon, 3 Nov 2025 15:44:36 +0100 Subject: [PATCH 157/171] fix cross links --- docs/testing/cross-links.md | 4 +--- .../Elastic.Documentation.Navigation.csproj | 1 + .../Isolated/Node/DocumentationSetNavigation.cs | 16 +++++++++++----- src/Elastic.Markdown/IO/DocumentationSet.cs | 4 +--- .../AssembleSources.cs | 3 +-- 5 files changed, 15 insertions(+), 13 deletions(-) diff --git a/docs/testing/cross-links.md b/docs/testing/cross-links.md index 4f0863d4c..e55ce8ded 100644 --- a/docs/testing/cross-links.md +++ b/docs/testing/cross-links.md @@ -1,7 +1,5 @@ # Cross Links -[Elasticsearch](docs-content://index.md) - -[Kibana][1] +[docs-content](docs-content://index.md) [1]: docs-content://index.md diff --git a/src/Elastic.Documentation.Navigation/Elastic.Documentation.Navigation.csproj b/src/Elastic.Documentation.Navigation/Elastic.Documentation.Navigation.csproj index 5448edb21..4a57206c2 100644 --- a/src/Elastic.Documentation.Navigation/Elastic.Documentation.Navigation.csproj +++ b/src/Elastic.Documentation.Navigation/Elastic.Documentation.Navigation.csproj @@ -8,6 +8,7 @@ + diff --git a/src/Elastic.Documentation.Navigation/Isolated/Node/DocumentationSetNavigation.cs b/src/Elastic.Documentation.Navigation/Isolated/Node/DocumentationSetNavigation.cs index 0086adb8d..8b4e0566c 100644 --- a/src/Elastic.Documentation.Navigation/Isolated/Node/DocumentationSetNavigation.cs +++ b/src/Elastic.Documentation.Navigation/Isolated/Node/DocumentationSetNavigation.cs @@ -6,6 +6,7 @@ using System.IO.Abstractions; using Elastic.Documentation.Configuration.DocSet; using Elastic.Documentation.Extensions; +using Elastic.Documentation.Links.CrossLinks; using Elastic.Documentation.Navigation.Isolated.Leaf; namespace Elastic.Documentation.Navigation.Isolated.Node; @@ -22,6 +23,7 @@ public class DocumentationSetNavigation where TModel : class, IDocumentationFile { private readonly IDocumentationFileFactory _factory; + private readonly ICrossLinkResolver _crossLinkResolver; public DocumentationSetNavigation( DocumentationSetFile documentationSet, @@ -29,11 +31,13 @@ public DocumentationSetNavigation( IDocumentationFileFactory factory, IRootNavigationItem? parent = null, IRootNavigationItem? root = null, - string? pathPrefix = null + string? pathPrefix = null, + ICrossLinkResolver? crossLinkResolver = null ) { _context = context; _factory = factory; + _crossLinkResolver = crossLinkResolver ?? NoopCrossLinkResolver.Instance; _pathPrefix = pathPrefix ?? string.Empty; // Initialize root properties _navigationRoot = root ?? this; @@ -139,7 +143,7 @@ public DocumentationSetNavigation( public IReadOnlyCollection NavigationItems { get; private set; } void IAssignableChildrenNavigation.SetNavigationItems(IReadOnlyCollection navigationItems) => SetNavigationItems(navigationItems); - internal void SetNavigationItems(IReadOnlyCollection navigationItems) + private void SetNavigationItems(IReadOnlyCollection navigationItems) { var indexNavigation = navigationItems.QueryIndex(this, $"{PathPrefix}/index.md", out navigationItems); Index = indexNavigation; @@ -280,7 +284,7 @@ INavigationHomeAccessor homeAccessor return fileNavigation; } - private INavigationItem CreateCrossLinkNavigation( + private INavigationItem? CreateCrossLinkNavigation( CrossLinkRef crossLinkRef, int index, INodeNavigationItem? parent, @@ -288,11 +292,13 @@ INavigationHomeAccessor homeAccessor ) { var title = crossLinkRef.Title ?? crossLinkRef.CrossLinkUri.OriginalString; - var model = new CrossLinkModel(crossLinkRef.CrossLinkUri, title); + if (!_crossLinkResolver.TryResolve(s => _context.EmitError(_context.ConfigurationPath, s), crossLinkRef.CrossLinkUri, out var resolvedUri)) + return null; + var model = new CrossLinkModel(resolvedUri, title); return new CrossLinkNavigationLeaf( model, - crossLinkRef.CrossLinkUri.OriginalString, + resolvedUri.ToString(), crossLinkRef.Hidden, parent, homeAccessor diff --git a/src/Elastic.Markdown/IO/DocumentationSet.cs b/src/Elastic.Markdown/IO/DocumentationSet.cs index 0d7be63b6..6e6ece4ef 100644 --- a/src/Elastic.Markdown/IO/DocumentationSet.cs +++ b/src/Elastic.Markdown/IO/DocumentationSet.cs @@ -13,7 +13,6 @@ using Elastic.Documentation.Links; using Elastic.Documentation.Links.CrossLinks; using Elastic.Documentation.Navigation; -using Elastic.Documentation.Navigation.Isolated; using Elastic.Documentation.Navigation.Isolated.Leaf; using Elastic.Documentation.Navigation.Isolated.Node; using Elastic.Documentation.Site.Navigation; @@ -22,7 +21,6 @@ using Elastic.Markdown.IO.NewNavigation; using Elastic.Markdown.Myst; using Microsoft.Extensions.Logging; -using YamlDotNet.Serialization.TypeInspectors; namespace Elastic.Markdown.IO; @@ -77,7 +75,7 @@ ICrossLinkResolver linkResolver MarkdownParser = new MarkdownParser(context, resolver); var fileFactory = new MarkdownFileFactory(context, MarkdownParser, EnabledExtensions); - Navigation = new DocumentationSetNavigation(context.ConfigurationYaml, context, fileFactory, pathPrefix: context.UrlPathPrefix); + Navigation = new DocumentationSetNavigation(context.ConfigurationYaml, context, fileFactory, null, null, context.UrlPathPrefix, CrossLinkResolver); Name = Context.Git != GitCheckoutInformation.Unavailable ? Context.Git.RepositoryName diff --git a/src/services/Elastic.Documentation.Assembler/AssembleSources.cs b/src/services/Elastic.Documentation.Assembler/AssembleSources.cs index 78e3820ca..0a87b4252 100644 --- a/src/services/Elastic.Documentation.Assembler/AssembleSources.cs +++ b/src/services/Elastic.Documentation.Assembler/AssembleSources.cs @@ -86,8 +86,7 @@ IReadOnlySet availableExporters AssembleContext = assembleContext; AssembleSets = checkouts .Where(c => c.Repository is { Skip: false }) - .Select(c => new AssemblerDocumentationSet(logFactory, assembleContext, c, crossLinkResolver, configurationContext, - availableExporters)) + .Select(c => new AssemblerDocumentationSet(logFactory, assembleContext, c, crossLinkResolver, configurationContext, availableExporters)) .ToDictionary(s => s.Checkout.Repository.Name, s => s) .ToFrozenDictionary(); } From bc87360b81576507e97bd7938fcdd6f159a1c8a8 Mon Sep 17 00:00:00 2001 From: Martijn Laarman Date: Mon, 3 Nov 2025 15:56:29 +0100 Subject: [PATCH 158/171] Fix tests expecting crosslink navigation item --- .../Isolation/ConstructorTests.cs | 2 +- .../Isolation/NavigationStructureTests.cs | 4 ++-- .../Isolation/PhysicalDocsetTests.cs | 5 ++--- .../TestDocumentationSetContext.cs | 21 +++++++++++++++++++ 4 files changed, 26 insertions(+), 6 deletions(-) diff --git a/tests/Navigation.Tests/Isolation/ConstructorTests.cs b/tests/Navigation.Tests/Isolation/ConstructorTests.cs index e746af080..1438c7069 100644 --- a/tests/Navigation.Tests/Isolation/ConstructorTests.cs +++ b/tests/Navigation.Tests/Isolation/ConstructorTests.cs @@ -127,7 +127,7 @@ public void ConstructorCreatesCrossLinkNavigation() var context = CreateContext(fileSystem); var docSet = DocumentationSetFile.LoadAndResolve(context.Collector, yaml, fileSystem.NewDirInfo("docs")); - var navigation = new DocumentationSetNavigation(docSet, context, GenericDocumentationFileFactory.Instance); + var navigation = new DocumentationSetNavigation(docSet, context, GenericDocumentationFileFactory.Instance, crossLinkResolver: TestCrossLinkResolver.Instance); navigation.NavigationItems.Should().HaveCount(0); var crossLink = navigation.Index.Should().BeOfType().Subject; diff --git a/tests/Navigation.Tests/Isolation/NavigationStructureTests.cs b/tests/Navigation.Tests/Isolation/NavigationStructureTests.cs index a73e72f79..f76b59961 100644 --- a/tests/Navigation.Tests/Isolation/NavigationStructureTests.cs +++ b/tests/Navigation.Tests/Isolation/NavigationStructureTests.cs @@ -151,7 +151,7 @@ public async Task ComplexNestedStructureBuildsCorrectly() var docSet = DocumentationSetFile.LoadAndResolve(context.Collector, yaml, fileSystem.NewDirInfo("docs")); _ = context.Collector.StartAsync(TestContext.Current.CancellationToken); - var navigation = new DocumentationSetNavigation(docSet, context, TestDocumentationFileFactory.Instance); + var navigation = new DocumentationSetNavigation(docSet, context, TestDocumentationFileFactory.Instance, crossLinkResolver: TestCrossLinkResolver.Instance); await context.Collector.StopAsync(TestContext.Current.CancellationToken); @@ -295,7 +295,7 @@ public void AllNavigationItemsHaveNavigationRootSet() var context = CreateContext(fileSystem); var docSet = DocumentationSetFile.LoadAndResolve(context.Collector, yaml, fileSystem.NewDirInfo("docs")); - var navigation = new DocumentationSetNavigation(docSet, context, TestDocumentationFileFactory.Instance); + var navigation = new DocumentationSetNavigation(docSet, context, TestDocumentationFileFactory.Instance, crossLinkResolver: TestCrossLinkResolver.Instance); // Helper to recursively visit all navigation items var allItems = new List(); diff --git a/tests/Navigation.Tests/Isolation/PhysicalDocsetTests.cs b/tests/Navigation.Tests/Isolation/PhysicalDocsetTests.cs index 1ddfb7e1c..c21906c98 100644 --- a/tests/Navigation.Tests/Isolation/PhysicalDocsetTests.cs +++ b/tests/Navigation.Tests/Isolation/PhysicalDocsetTests.cs @@ -6,7 +6,6 @@ using Elastic.Documentation.Configuration; using Elastic.Documentation.Configuration.DocSet; using Elastic.Documentation.Diagnostics; -using Elastic.Documentation.Navigation.Isolated; using Elastic.Documentation.Navigation.Isolated.Leaf; using Elastic.Documentation.Navigation.Isolated.Node; using FluentAssertions; @@ -30,7 +29,7 @@ public async Task PhysicalDocsetCanBeNavigated() var docSet = DocumentationSetFile.LoadAndResolve(context.Collector, configPath, fileSystem); _ = context.Collector.StartAsync(TestContext.Current.CancellationToken); - var navigation = new DocumentationSetNavigation(docSet, context, TestDocumentationFileFactory.Instance); + var navigation = new DocumentationSetNavigation(docSet, context, TestDocumentationFileFactory.Instance, crossLinkResolver: TestCrossLinkResolver.Instance); await context.Collector.StopAsync(TestContext.Current.CancellationToken); @@ -171,7 +170,7 @@ public async Task PhysicalDocsetNavigationHandlesCrossLinks() var docSet = DocumentationSetFile.LoadAndResolve(context.Collector, configPath); _ = context.Collector.StartAsync(TestContext.Current.CancellationToken); - var navigation = new DocumentationSetNavigation(docSet, context, TestDocumentationFileFactory.Instance); + var navigation = new DocumentationSetNavigation(docSet, context, TestDocumentationFileFactory.Instance, crossLinkResolver: TestCrossLinkResolver.Instance); await context.Collector.StopAsync(TestContext.Current.CancellationToken); diff --git a/tests/Navigation.Tests/TestDocumentationSetContext.cs b/tests/Navigation.Tests/TestDocumentationSetContext.cs index cd0c21dce..d7fb32605 100644 --- a/tests/Navigation.Tests/TestDocumentationSetContext.cs +++ b/tests/Navigation.Tests/TestDocumentationSetContext.cs @@ -2,10 +2,12 @@ // Elasticsearch B.V licenses this file to you under the Apache 2.0 License. // See the LICENSE file in the project root for more information +using System.Diagnostics.CodeAnalysis; using System.IO.Abstractions; using Elastic.Documentation; using Elastic.Documentation.Diagnostics; using Elastic.Documentation.Extensions; +using Elastic.Documentation.Links.CrossLinks; using Elastic.Documentation.Navigation.Isolated; using Markdig; using Markdig.Parsers; @@ -46,6 +48,25 @@ public override void Write(Diagnostic diagnostic) public override Task StopAsync(Cancel cancellationToken) => Task.CompletedTask; } +/// A cross link resolver that always resolves to a fixed URL +public class TestCrossLinkResolver : ICrossLinkResolver +{ + public static TestCrossLinkResolver Instance { get; } = new(); + + /// + public bool TryResolve(Action errorEmitter, Uri crossLinkUri, [NotNullWhen(true)] out Uri? resolvedUri) + { + resolvedUri = new Uri("https://docs-v3-preview.elastic.dev/elastic/docs-builder/tree/main"); + return true; + } + + /// + public IUriEnvironmentResolver UriResolver { get; } = new IsolatedBuildEnvironmentUriResolver(); + + private TestCrossLinkResolver() { } + +} + public class TestDocumentationSetContext : IDocumentationSetContext { public TestDocumentationSetContext(IFileSystem fileSystem, From 9ffca0bbeae8626b2f7ba375a4abaff9d6383d9e Mon Sep 17 00:00:00 2001 From: Martijn Laarman Date: Mon, 3 Nov 2025 16:02:27 +0100 Subject: [PATCH 159/171] fix bad test assertion --- tests/Navigation.Tests/Isolation/ConstructorTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Navigation.Tests/Isolation/ConstructorTests.cs b/tests/Navigation.Tests/Isolation/ConstructorTests.cs index 1438c7069..37b5739b7 100644 --- a/tests/Navigation.Tests/Isolation/ConstructorTests.cs +++ b/tests/Navigation.Tests/Isolation/ConstructorTests.cs @@ -132,7 +132,7 @@ public void ConstructorCreatesCrossLinkNavigation() navigation.NavigationItems.Should().HaveCount(0); var crossLink = navigation.Index.Should().BeOfType().Subject; crossLink.NavigationTitle.Should().Be("External Guide"); - crossLink.Url.Should().Be("docs-content://guide.md"); + crossLink.Url.Should().Be("https://docs-v3-preview.elastic.dev/elastic/docs-builder/tree/main"); } [Fact] From 02c9b4a0aeb77b71b8dbf5eab8dae8f2c811e41f Mon Sep 17 00:00:00 2001 From: Martijn Laarman Date: Thu, 6 Nov 2025 13:42:24 +0100 Subject: [PATCH 160/171] post merge fixes --- build/build.fsproj | 4 ---- docs-builder.sln | 16 ++-------------- .../SiteNavigationTests.cs | 6 +++--- tests/Directory.Build.props | 1 - 4 files changed, 5 insertions(+), 22 deletions(-) diff --git a/build/build.fsproj b/build/build.fsproj index dfe992edf..1f1caa585 100644 --- a/build/build.fsproj +++ b/build/build.fsproj @@ -14,10 +14,6 @@ - - - - diff --git a/docs-builder.sln b/docs-builder.sln index 2a0d05497..f144e0b0d 100644 --- a/docs-builder.sln +++ b/docs-builder.sln @@ -176,7 +176,6 @@ Global {4D198E25-C211-41DC-9E84-B15E89BD7048}.Release|x86.ActiveCfg = Release|Any CPU {4D198E25-C211-41DC-9E84-B15E89BD7048}.Release|x86.Build.0 = Release|Any CPU {13387C19-03EC-41AC-A0FC-B5A7E3761DA6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {13387C19-03EC-41AC-A0FC-B5A7E3761DA6}.Debug|Any CPU.Build.0 = Debug|Any CPU {13387C19-03EC-41AC-A0FC-B5A7E3761DA6}.Debug|x64.ActiveCfg = Debug|Any CPU {13387C19-03EC-41AC-A0FC-B5A7E3761DA6}.Debug|x64.Build.0 = Debug|Any CPU {13387C19-03EC-41AC-A0FC-B5A7E3761DA6}.Debug|x86.ActiveCfg = Debug|Any CPU @@ -199,18 +198,6 @@ Global {01F05AD0-E0E0-401F-A7EC-905928E1E9F0}.Release|x64.Build.0 = Release|Any CPU {01F05AD0-E0E0-401F-A7EC-905928E1E9F0}.Release|x86.ActiveCfg = Release|Any CPU {01F05AD0-E0E0-401F-A7EC-905928E1E9F0}.Release|x86.Build.0 = Release|Any CPU - {1A8659C1-222A-4824-B562-ED8F88658C05}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {1A8659C1-222A-4824-B562-ED8F88658C05}.Debug|Any CPU.Build.0 = Debug|Any CPU - {1A8659C1-222A-4824-B562-ED8F88658C05}.Debug|x64.ActiveCfg = Debug|Any CPU - {1A8659C1-222A-4824-B562-ED8F88658C05}.Debug|x64.Build.0 = Debug|Any CPU - {1A8659C1-222A-4824-B562-ED8F88658C05}.Debug|x86.ActiveCfg = Debug|Any CPU - {1A8659C1-222A-4824-B562-ED8F88658C05}.Debug|x86.Build.0 = Debug|Any CPU - {1A8659C1-222A-4824-B562-ED8F88658C05}.Release|Any CPU.ActiveCfg = Release|Any CPU - {1A8659C1-222A-4824-B562-ED8F88658C05}.Release|Any CPU.Build.0 = Release|Any CPU - {1A8659C1-222A-4824-B562-ED8F88658C05}.Release|x64.ActiveCfg = Release|Any CPU - {1A8659C1-222A-4824-B562-ED8F88658C05}.Release|x64.Build.0 = Release|Any CPU - {1A8659C1-222A-4824-B562-ED8F88658C05}.Release|x86.ActiveCfg = Release|Any CPU - {1A8659C1-222A-4824-B562-ED8F88658C05}.Release|x86.Build.0 = Release|Any CPU {B27C5107-128B-465A-B8F8-8985399E4CFB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {B27C5107-128B-465A-B8F8-8985399E4CFB}.Debug|Any CPU.Build.0 = Debug|Any CPU {B27C5107-128B-465A-B8F8-8985399E4CFB}.Debug|x64.ActiveCfg = Debug|Any CPU @@ -224,7 +211,6 @@ Global {B27C5107-128B-465A-B8F8-8985399E4CFB}.Release|x86.ActiveCfg = Release|Any CPU {B27C5107-128B-465A-B8F8-8985399E4CFB}.Release|x86.Build.0 = Release|Any CPU {10857974-6CF1-42B5-B793-AAA988BD7348}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {10857974-6CF1-42B5-B793-AAA988BD7348}.Debug|Any CPU.Build.0 = Debug|Any CPU {10857974-6CF1-42B5-B793-AAA988BD7348}.Debug|x64.ActiveCfg = Debug|Any CPU {10857974-6CF1-42B5-B793-AAA988BD7348}.Debug|x64.Build.0 = Debug|Any CPU {10857974-6CF1-42B5-B793-AAA988BD7348}.Debug|x86.ActiveCfg = Debug|Any CPU @@ -238,6 +224,8 @@ Global {10857974-6CF1-42B5-B793-AAA988BD7348}.Release|x64.Build.0 = Release|Any CPU {10857974-6CF1-42B5-B793-AAA988BD7348}.Release|x86.ActiveCfg = Release|Any CPU {10857974-6CF1-42B5-B793-AAA988BD7348}.Release|x86.Build.0 = Release|Any CPU + {10857974-6CF1-42B5-B793-AAA988BD7348}.Release|Any CPU.Build.0 = Release|Any CPU + {10857974-6CF1-42B5-B793-AAA988BD7348}.Debug|Any CPU.Build.0 = Debug|Any CPU {018F959E-824B-4664-B345-066784478D24}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {018F959E-824B-4664-B345-066784478D24}.Debug|Any CPU.Build.0 = Debug|Any CPU {018F959E-824B-4664-B345-066784478D24}.Debug|x64.ActiveCfg = Debug|Any CPU diff --git a/tests-integration/Elastic.Assembler.IntegrationTests/SiteNavigationTests.cs b/tests-integration/Elastic.Assembler.IntegrationTests/SiteNavigationTests.cs index 9f3a06deb..c42441c2a 100644 --- a/tests-integration/Elastic.Assembler.IntegrationTests/SiteNavigationTests.cs +++ b/tests-integration/Elastic.Assembler.IntegrationTests/SiteNavigationTests.cs @@ -206,13 +206,13 @@ public async Task UriResolving() resolvedUri = uriResolver.Resolve(new Uri("apm-agent-nodejs://reference/instrumentation.md"), "reference/instrumentation"); resolvedUri.Should().Be("https://www.elastic.co/docs/reference/apm/agents/nodejs/instrumentation"); - resolvedUri = uriResolver.Resolve(new Uri("apm-agent-dotnet://reference/a/file.md"), "/reference/a/file"); + resolvedUri = uriResolver.Resolve(new Uri("apm-agent-dotnet://reference/a/file.md"), "reference/a/file"); resolvedUri.Should().Be("https://www.elastic.co/docs/reference/apm/agents/dotnet/a/file"); - resolvedUri = uriResolver.Resolve(new Uri("elasticsearch-net://reference/b/file.md"), "/reference/b/file"); + resolvedUri = uriResolver.Resolve(new Uri("elasticsearch-net://reference/b/file.md"), "reference/b/file"); resolvedUri.Should().Be("https://www.elastic.co/docs/reference/elasticsearch/clients/dotnet/b/file"); - resolvedUri = uriResolver.Resolve(new Uri("elasticsearch://extend/c/file.md"), "/extend/c/file"); + resolvedUri = uriResolver.Resolve(new Uri("elasticsearch://extend/c/file.md"), "extend/c/file"); resolvedUri.Should().Be("https://www.elastic.co/docs/extend/elasticsearch/c/file"); } diff --git a/tests/Directory.Build.props b/tests/Directory.Build.props index 2339f7c53..48c968500 100644 --- a/tests/Directory.Build.props +++ b/tests/Directory.Build.props @@ -27,5 +27,4 @@ - From ae1431ecd06035416ebac6e79cf782ae06be7840 Mon Sep 17 00:00:00 2001 From: Martijn Laarman Date: Thu, 6 Nov 2025 13:57:53 +0100 Subject: [PATCH 161/171] post merge fixes --- docs-builder.sln | 5 ----- 1 file changed, 5 deletions(-) diff --git a/docs-builder.sln b/docs-builder.sln index f144e0b0d..de9409744 100644 --- a/docs-builder.sln +++ b/docs-builder.sln @@ -217,14 +217,9 @@ Global {10857974-6CF1-42B5-B793-AAA988BD7348}.Debug|x86.Build.0 = Debug|Any CPU {10857974-6CF1-42B5-B793-AAA988BD7348}.Release|Any CPU.ActiveCfg = Release|Any CPU {10857974-6CF1-42B5-B793-AAA988BD7348}.Release|x64.ActiveCfg = Release|Any CPU - {10857974-6CF1-42B5-B793-AAA988BD7348}.Release|x64.Build.0 = Release|Any CPU {10857974-6CF1-42B5-B793-AAA988BD7348}.Release|x86.ActiveCfg = Release|Any CPU - {10857974-6CF1-42B5-B793-AAA988BD7348}.Release|x86.Build.0 = Release|Any CPU {10857974-6CF1-42B5-B793-AAA988BD7348}.Release|x64.ActiveCfg = Release|Any CPU - {10857974-6CF1-42B5-B793-AAA988BD7348}.Release|x64.Build.0 = Release|Any CPU {10857974-6CF1-42B5-B793-AAA988BD7348}.Release|x86.ActiveCfg = Release|Any CPU - {10857974-6CF1-42B5-B793-AAA988BD7348}.Release|x86.Build.0 = Release|Any CPU - {10857974-6CF1-42B5-B793-AAA988BD7348}.Release|Any CPU.Build.0 = Release|Any CPU {10857974-6CF1-42B5-B793-AAA988BD7348}.Debug|Any CPU.Build.0 = Debug|Any CPU {018F959E-824B-4664-B345-066784478D24}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {018F959E-824B-4664-B345-066784478D24}.Debug|Any CPU.Build.0 = Debug|Any CPU From a463915512cd3d312bf0d7f0ac907eacbbb928f3 Mon Sep 17 00:00:00 2001 From: Martijn Laarman Date: Thu, 6 Nov 2025 15:38:58 +0100 Subject: [PATCH 162/171] Ensure url has no trailing slash --- docs-builder.sln | 5 ----- .../Isolated/Leaf/FileNavigationLeaf.cs | 6 +++--- .../Isolated/Node/TableOfContentsNavigation.cs | 4 ++-- .../Isolated/Node/VirtualFileNavigation.cs | 2 +- src/Elastic.Markdown/IO/DocumentationSet.cs | 3 ++- 5 files changed, 8 insertions(+), 12 deletions(-) diff --git a/docs-builder.sln b/docs-builder.sln index de9409744..7f4d34084 100644 --- a/docs-builder.sln +++ b/docs-builder.sln @@ -177,15 +177,10 @@ Global {4D198E25-C211-41DC-9E84-B15E89BD7048}.Release|x86.Build.0 = Release|Any CPU {13387C19-03EC-41AC-A0FC-B5A7E3761DA6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {13387C19-03EC-41AC-A0FC-B5A7E3761DA6}.Debug|x64.ActiveCfg = Debug|Any CPU - {13387C19-03EC-41AC-A0FC-B5A7E3761DA6}.Debug|x64.Build.0 = Debug|Any CPU {13387C19-03EC-41AC-A0FC-B5A7E3761DA6}.Debug|x86.ActiveCfg = Debug|Any CPU - {13387C19-03EC-41AC-A0FC-B5A7E3761DA6}.Debug|x86.Build.0 = Debug|Any CPU {13387C19-03EC-41AC-A0FC-B5A7E3761DA6}.Release|Any CPU.ActiveCfg = Release|Any CPU - {13387C19-03EC-41AC-A0FC-B5A7E3761DA6}.Release|Any CPU.Build.0 = Release|Any CPU {13387C19-03EC-41AC-A0FC-B5A7E3761DA6}.Release|x64.ActiveCfg = Release|Any CPU - {13387C19-03EC-41AC-A0FC-B5A7E3761DA6}.Release|x64.Build.0 = Release|Any CPU {13387C19-03EC-41AC-A0FC-B5A7E3761DA6}.Release|x86.ActiveCfg = Release|Any CPU - {13387C19-03EC-41AC-A0FC-B5A7E3761DA6}.Release|x86.Build.0 = Release|Any CPU {01F05AD0-E0E0-401F-A7EC-905928E1E9F0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {01F05AD0-E0E0-401F-A7EC-905928E1E9F0}.Debug|Any CPU.Build.0 = Debug|Any CPU {01F05AD0-E0E0-401F-A7EC-905928E1E9F0}.Debug|x64.ActiveCfg = Debug|Any CPU diff --git a/src/Elastic.Documentation.Navigation/Isolated/Leaf/FileNavigationLeaf.cs b/src/Elastic.Documentation.Navigation/Isolated/Leaf/FileNavigationLeaf.cs index fa84c5723..e126786e7 100644 --- a/src/Elastic.Documentation.Navigation/Isolated/Leaf/FileNavigationLeaf.cs +++ b/src/Elastic.Documentation.Navigation/Isolated/Leaf/FileNavigationLeaf.cs @@ -51,12 +51,12 @@ string DetermineUrl() if (path.EndsWith("/index", StringComparison.OrdinalIgnoreCase)) path = path[..^6]; // Remove "/index" else if (path.Equals("index", StringComparison.OrdinalIgnoreCase)) - return string.IsNullOrEmpty(rootUrl) ? "/" : $"{rootUrl}/"; + return string.IsNullOrEmpty(rootUrl) ? "/" : $"{rootUrl}"; if (string.IsNullOrEmpty(path)) - return string.IsNullOrEmpty(rootUrl) ? "/" : $"{rootUrl}/"; + return string.IsNullOrEmpty(rootUrl) ? "/" : $"{rootUrl}"; - return $"{rootUrl}/{path.TrimEnd('/')}/"; + return $"{rootUrl}/{path.TrimEnd('/')}"; } } } diff --git a/src/Elastic.Documentation.Navigation/Isolated/Node/TableOfContentsNavigation.cs b/src/Elastic.Documentation.Navigation/Isolated/Node/TableOfContentsNavigation.cs index 28bf8afd8..07349fc7a 100644 --- a/src/Elastic.Documentation.Navigation/Isolated/Node/TableOfContentsNavigation.cs +++ b/src/Elastic.Documentation.Navigation/Isolated/Node/TableOfContentsNavigation.cs @@ -48,7 +48,7 @@ INavigationHomeProvider homeProvider /// /// The path prefix for this TOC - same as parent per url-building.md. /// Implements INavigationHomeProvider.PathPrefix. - /// TOC doesn't change PathPrefix from parent. + /// TOC doesn't change PathPrefix from the parent. /// public string PathPrefix { get; } @@ -83,7 +83,7 @@ INavigationHomeProvider homeProvider public string ParentPath { get; } - /// + /// public string Id { get; } /// diff --git a/src/Elastic.Documentation.Navigation/Isolated/Node/VirtualFileNavigation.cs b/src/Elastic.Documentation.Navigation/Isolated/Node/VirtualFileNavigation.cs index ce1c647a7..5e5e17904 100644 --- a/src/Elastic.Documentation.Navigation/Isolated/Node/VirtualFileNavigation.cs +++ b/src/Elastic.Documentation.Navigation/Isolated/Node/VirtualFileNavigation.cs @@ -28,7 +28,7 @@ public class VirtualFileNavigation(TModel model, IFileInfo fileInfo, Vir public INodeNavigationItem? Parent { get; set; } = args.Parent; /// - public bool Hidden { get; init; } = args.Hidden; + public bool Hidden { get; } = args.Hidden; /// public int NavigationIndex { get; set; } diff --git a/src/Elastic.Markdown/IO/DocumentationSet.cs b/src/Elastic.Markdown/IO/DocumentationSet.cs index 6e6ece4ef..17c2400a2 100644 --- a/src/Elastic.Markdown/IO/DocumentationSet.cs +++ b/src/Elastic.Markdown/IO/DocumentationSet.cs @@ -243,7 +243,7 @@ public async Task ResolveDirectoryTree(Cancel ctx) public RepositoryLinks CreateLinkReference() { var redirects = Configuration.Redirects; - var crossLinks = Context.Collector.CrossLinks.ToHashSet().ToArray(); + var crossLinks = Context.Collector.CrossLinks.ToHashSet().OrderBy(l => l).ToArray(); var leafs = NavigationIndexedByOrder.Values .OfType>().ToArray(); @@ -268,6 +268,7 @@ public RepositoryLinks CreateLinkReference() return (Path: path, tuple.Markdown, tuple.Navigation); }) .DistinctBy(tuple => tuple.Path) + .OrderBy(tuple => tuple.Path) .ToDictionary( tuple => tuple.Path, tuple => From 74ec7ab3de7a2aec287759c513e2f2bfb8260a2a Mon Sep 17 00:00:00 2001 From: Martijn Laarman Date: Thu, 6 Nov 2025 15:56:55 +0100 Subject: [PATCH 163/171] Give SiteNavigation a fixed navigation title --- .../Assembler/SiteNavigation.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Elastic.Documentation.Navigation/Assembler/SiteNavigation.cs b/src/Elastic.Documentation.Navigation/Assembler/SiteNavigation.cs index 87e0b73c1..016793722 100644 --- a/src/Elastic.Documentation.Navigation/Assembler/SiteNavigation.cs +++ b/src/Elastic.Documentation.Navigation/Assembler/SiteNavigation.cs @@ -34,6 +34,7 @@ public SiteNavigation( Phantoms = siteNavigationFile.Phantoms; DeclaredPhantoms = [.. siteNavigationFile.Phantoms.Select(p => new Uri(p.Source))]; DeclaredTableOfContents = SiteNavigationFile.GetAllDeclaredSources(siteNavigationFile); + NavigationTitle = "Elastic Docs"; _nodes = []; foreach (var setNavigation in documentationSetNavigations) @@ -106,7 +107,7 @@ public SiteNavigation( public string Url => string.IsNullOrEmpty(_sitePrefix) ? "/" : _sitePrefix; /// - public string NavigationTitle => Index.NavigationTitle; + public string NavigationTitle { get; } /// public IRootNavigationItem NavigationRoot { get; } From 79fc59b651a29ce08bac03d5d89b463d08ee5425 Mon Sep 17 00:00:00 2001 From: Martijn Laarman Date: Thu, 6 Nov 2025 16:00:47 +0100 Subject: [PATCH 164/171] Fix smoke test check for trailing slash --- .github/workflows/smoke-test.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/smoke-test.yml b/.github/workflows/smoke-test.yml index ac35b31e6..2a21b055e 100644 --- a/.github/workflows/smoke-test.yml +++ b/.github/workflows/smoke-test.yml @@ -12,11 +12,11 @@ jobs: matrix: include: - repository: elastic/docs-content - landing-page-path-output: /docs/ + landing-page-path-output: /docs - repository: elastic/elasticsearch - landing-page-path-output: /docs/reference/ + landing-page-path-output: /docs/reference - repository: elastic/opentelemetry - landing-page-path-output: /docs/reference/ + landing-page-path-output: /docs/reference # This is a random repository that should not have a docset.yml - repository: elastic/oblt-actions From 3c579d4c09aed7a38086042159cfacfb6da2c532 Mon Sep 17 00:00:00 2001 From: Martijn Laarman Date: Thu, 6 Nov 2025 17:00:26 +0100 Subject: [PATCH 165/171] fix tests expecting trailing slash --- .../journeys/navigation-test.journey.ts | 8 ++-- .../Inline/InlineLinkTests.cs | 4 +- .../Assembler/ComplexSiteNavigationTests.cs | 20 ++++---- .../Assembler/SiteDocumentationSetsTests.cs | 38 +++++++-------- .../Assembler/SiteNavigationTests.cs | 32 ++++++------- .../Isolation/ConstructorTests.cs | 18 ++++---- .../Isolation/DynamicUrlTests.cs | 46 +++++++++---------- .../Isolation/FileNavigationTests.cs | 34 +++++++------- .../Isolation/FolderIndexFileRefTests.cs | 2 +- .../Isolation/NavigationStructureTests.cs | 24 +++++----- .../Isolation/PhysicalDocsetTests.cs | 6 +-- 11 files changed, 116 insertions(+), 116 deletions(-) diff --git a/src/Elastic.Documentation.Site/synthetics/journeys/navigation-test.journey.ts b/src/Elastic.Documentation.Site/synthetics/journeys/navigation-test.journey.ts index d08c4a567..72544d6a5 100644 --- a/src/Elastic.Documentation.Site/synthetics/journeys/navigation-test.journey.ts +++ b/src/Elastic.Documentation.Site/synthetics/journeys/navigation-test.journey.ts @@ -31,7 +31,7 @@ journey('navigation test', ({ page, params }) => { .getByRole('link', { name: 'Elastic Fundamentals' }) .first() .click() - await expect(page).toHaveURL(`${host}/docs/get-started/`) + await expect(page).toHaveURL(`${host}/docs/get-started`) await expect(page).toHaveTitle(/Elastic fundamentals/) await expect( page.getByRole('heading', { name: 'Elastic fundamentals' }) @@ -44,7 +44,7 @@ journey('navigation test', ({ page, params }) => { .first() .click() await expect(page).toHaveURL( - `${host}/docs/get-started/deployment-options/` + `${host}/docs/get-started/deployment-options` ) await expect(page).toHaveTitle(/Deployment options/) await expect( @@ -59,7 +59,7 @@ journey('navigation test', ({ page, params }) => { .first() .click() await expect(page).toHaveURL( - `${host}/docs/deploy-manage/deploy/elastic-cloud/` + `${host}/docs/deploy-manage/deploy/elastic-cloud` ) await expect(page).toHaveTitle(/Elastic Cloud/) }) @@ -71,6 +71,6 @@ journey('navigation test', ({ page, params }) => { await pagesDropdown .getByRole('link', { name: 'Reference', exact: true }) .click() - await expect(page).toHaveURL(`${host}/docs/reference/`) + await expect(page).toHaveURL(`${host}/docs/reference`) }) }) diff --git a/tests/Elastic.Markdown.Tests/Inline/InlineLinkTests.cs b/tests/Elastic.Markdown.Tests/Inline/InlineLinkTests.cs index b074b76e6..142f453f5 100644 --- a/tests/Elastic.Markdown.Tests/Inline/InlineLinkTests.cs +++ b/tests/Elastic.Markdown.Tests/Inline/InlineLinkTests.cs @@ -320,10 +320,10 @@ public void GeneratesHtml() => """

Links:

"""); diff --git a/tests/Navigation.Tests/Assembler/ComplexSiteNavigationTests.cs b/tests/Navigation.Tests/Assembler/ComplexSiteNavigationTests.cs index 72e4ff6a5..f30b9b499 100644 --- a/tests/Navigation.Tests/Assembler/ComplexSiteNavigationTests.cs +++ b/tests/Navigation.Tests/Assembler/ComplexSiteNavigationTests.cs @@ -67,45 +67,45 @@ public void ComplexNavigationWithMultipleNestedTocsAppliesPathPrefixToRootUrls() // Test 1: Observability - verify root URL has path prefix var observability = siteNavigation.NavigationItems.ElementAt(0) as INodeNavigationItem; observability.Should().NotBeNull(); - observability.Url.Should().Be("/serverless/observability/"); + observability.Url.Should().Be("/serverless/observability"); observability.NavigationTitle.Should().Be(observability.Index.NavigationTitle); // Test 2: Serverless Search - verify root URL has path prefix var search = siteNavigation.NavigationItems.ElementAt(1); search.Should().NotBeNull(); - search.Url.Should().Be("/serverless/search/"); + search.Url.Should().Be("/serverless/search"); // Test 3: Platform - verify root URL has path prefix var platform = siteNavigation.NavigationItems.ElementAt(2) as INodeNavigationItem; platform.Should().NotBeNull(); - platform.Url.Should().Be("/platform/"); + platform.Url.Should().Be("/platform"); platform.NavigationItems.Should().HaveCount(2, "platform should only show the two nested TOCs as children"); // Verify nested TOC URLs have their specified path prefixes var deploymentGuide = platform.NavigationItems.ElementAt(0) as INodeNavigationItem; deploymentGuide.Should().NotBeNull(); - deploymentGuide.Url.Should().Be("/platform/deployment/"); + deploymentGuide.Url.Should().Be("/platform/deployment"); deploymentGuide.NavigationTitle.Should().Be(deploymentGuide.Index.NavigationTitle); var cloudGuide = platform.NavigationItems.ElementAt(1); cloudGuide.Should().NotBeNull(); - cloudGuide.Url.Should().Be("/platform/cloud/"); + cloudGuide.Url.Should().Be("/platform/cloud"); cloudGuide.NavigationTitle.Should().Be("Cloud Guide"); // Test 4: Elasticsearch Reference - verify root URL has path prefix var elasticsearch = siteNavigation.NavigationItems.ElementAt(3) as INodeNavigationItem; elasticsearch.Should().NotBeNull(); - elasticsearch.Url.Should().Be("/elasticsearch/reference/"); + elasticsearch.Url.Should().Be("/elasticsearch/reference"); elasticsearch.NavigationItems.Should().HaveCount(2, "elasticsearch should have read its toc"); // rest-apis is a folder (not a TOC) var restApis = elasticsearch.NavigationItems.ElementAt(0).Should().BeOfType>().Subject; - restApis.Url.Should().Be("/elasticsearch/reference/rest-apis/"); + restApis.Url.Should().Be("/elasticsearch/reference/rest-apis"); restApis.NavigationItems.Should().HaveCount(2, "rest-apis folder should have 2 files"); // Verify the file inside the folder has the correct path prefix var documentApisFile = restApis.NavigationItems.ElementAt(0).Should().BeOfType>().Subject; - documentApisFile.Url.Should().Be("/elasticsearch/reference/rest-apis/document-apis/"); + documentApisFile.Url.Should().Be("/elasticsearch/reference/rest-apis/document-apis"); documentApisFile.NavigationTitle.Should().Be("Document APIs"); } @@ -142,7 +142,7 @@ public void DeeplyNestedNavigationMaintainsPathPrefixThroughoutHierarchy() var platform = siteNavigation.NavigationItems.First() as INodeNavigationItem; platform.Should().NotBeNull(); - platform.Url.Should().Be("/docs/platform/"); + platform.Url.Should().Be("/docs/platform"); // Platform should have its children (deployment-guide, cloud-guide) platform.NavigationItems.Should().HaveCount(2); @@ -151,7 +151,7 @@ public void DeeplyNestedNavigationMaintainsPathPrefixThroughoutHierarchy() var deploymentGuide = platform.NavigationItems.ElementAt(0) as INodeNavigationItem; deploymentGuide.Should().NotBeNull(); deploymentGuide.Should().BeOfType>(); - deploymentGuide.Url.Should().StartWith("/docs/platform/"); + deploymentGuide.Url.Should().StartWith("/docs/platform"); // Walk through the entire tree and verify every single URL starts with a path prefix var allUrls = CollectAllUrls(platform.NavigationItems); diff --git a/tests/Navigation.Tests/Assembler/SiteDocumentationSetsTests.cs b/tests/Navigation.Tests/Assembler/SiteDocumentationSetsTests.cs index e6660952c..d007f694c 100644 --- a/tests/Navigation.Tests/Assembler/SiteDocumentationSetsTests.cs +++ b/tests/Navigation.Tests/Assembler/SiteDocumentationSetsTests.cs @@ -103,14 +103,14 @@ public void SiteNavigationIntegratesWithDocumentationSets() siteNavigation.NavigationItems.Should().HaveCount(3); var observability = siteNavigation.NavigationItems.ElementAt(0); - observability.Url.Should().Be("/serverless/observability/"); + observability.Url.Should().Be("/serverless/observability"); observability.NavigationTitle.Should().NotBeNullOrEmpty(); var search = siteNavigation.NavigationItems.ElementAt(1); - search.Url.Should().Be("/serverless/search/"); + search.Url.Should().Be("/serverless/search"); var security = siteNavigation.NavigationItems.ElementAt(2); - security.Url.Should().Be("/serverless/security/"); + security.Url.Should().Be("/serverless/security"); } [Fact] @@ -137,8 +137,8 @@ public void SiteNavigationWithNestedTocs() var platformNav = new DocumentationSetNavigation(platformDocset, platformContext, GenericDocumentationFileFactory.Instance); platformNav.Url.Should().Be("/"); platformNav.Index.Url.Should().Be("/"); - platformNav.NavigationItems.ElementAt(0).Url.Should().Be("/deployment-guide/"); - platformNav.NavigationItems.ElementAt(1).Url.Should().Be("/cloud-guide/"); + platformNav.NavigationItems.ElementAt(0).Url.Should().Be("/deployment-guide"); + platformNav.NavigationItems.ElementAt(1).Url.Should().Be("/cloud-guide"); var documentationSets = new List { platformNav }; @@ -150,14 +150,14 @@ public void SiteNavigationWithNestedTocs() var platform = siteNavigation.NavigationItems.First() as INodeNavigationItem; platform.Should().NotBeNull(); - platform.Url.Should().Be("/platform/"); + platform.Url.Should().Be("/platform"); platform.NavigationItems.Should().HaveCount(2); var deployment = platform.NavigationItems.ElementAt(0); - deployment.Url.Should().Be("/platform/deployment/"); + deployment.Url.Should().Be("/platform/deployment"); var cloud = platform.NavigationItems.ElementAt(1); - cloud.Url.Should().Be("/platform/cloud/"); + cloud.Url.Should().Be("/platform/cloud"); } [Fact] @@ -216,21 +216,21 @@ public void SiteNavigationWithAllRepositories() // Verify top-level items var observability = siteNavigation.NavigationItems.ElementAt(0); - observability.Url.Should().Be("/serverless/observability/"); + observability.Url.Should().Be("/serverless/observability"); var search = siteNavigation.NavigationItems.ElementAt(1); - search.Url.Should().Be("/serverless/search/"); + search.Url.Should().Be("/serverless/search"); var security = siteNavigation.NavigationItems.ElementAt(2); - security.Url.Should().Be("/serverless/security/"); + security.Url.Should().Be("/serverless/security"); var platform = siteNavigation.NavigationItems.ElementAt(3) as INodeNavigationItem; platform.Should().NotBeNull(); - platform.Url.Should().Be("/platform/"); + platform.Url.Should().Be("/platform"); platform.NavigationItems.Should().HaveCount(2); var elasticsearch = siteNavigation.NavigationItems.ElementAt(4); - elasticsearch.Url.Should().Be("/elasticsearch/reference/"); + elasticsearch.Url.Should().Be("/elasticsearch/reference"); } [Fact] @@ -281,13 +281,13 @@ public void DocumentationSetWithNestedTocs() var deploymentGuide = platformNav.NavigationItems.ElementAt(0); deploymentGuide.Should().BeOfType>(); - deploymentGuide.Url.Should().Be("/deployment-guide/"); + deploymentGuide.Url.Should().Be("/deployment-guide"); var deploymentToc = (TableOfContentsNavigation)deploymentGuide; deploymentToc.NavigationItems.Should().HaveCount(1); // self-managed folder var cloudGuide = platformNav.NavigationItems.ElementAt(1); cloudGuide.Should().BeOfType>(); - cloudGuide.Url.Should().Be("/cloud-guide/"); + cloudGuide.Url.Should().Be("/cloud-guide"); var cloudToc = (TableOfContentsNavigation)cloudGuide; cloudToc.NavigationItems.Should().HaveCount(2); // aws folder, azure folder } @@ -375,14 +375,14 @@ public void SiteNavigationWithNestedTocsAppliesCorrectPathPrefixes() var platform = siteNavigation.NavigationItems.First() as INodeNavigationItem; platform.Should().NotBeNull(); - platform.Url.Should().Be("/platform/"); + platform.Url.Should().Be("/platform"); // Verify child TOCs have their specific path prefixes var deployment = platform.NavigationItems.ElementAt(0); - deployment.Url.Should().StartWith("/platform/deployment/"); + deployment.Url.Should().StartWith("/platform/deployment"); var cloud = platform.NavigationItems.ElementAt(1); - cloud.Url.Should().StartWith("/platform/cloud/"); + cloud.Url.Should().StartWith("/platform/cloud"); } [Fact] @@ -411,7 +411,7 @@ public void SiteNavigationRequiresPathPrefix() toc.Should().NotBeNull(); toc.HomeProvider.PathPrefix.Should().Be("/bad-mapping-observability"); // toc has no `path_prefix` so it will use a default ugly one to avoid clashes and emit an error - toc.Url.Should().Be("/bad-mapping-observability/"); + toc.Url.Should().Be("/bad-mapping-observability"); } [Fact] diff --git a/tests/Navigation.Tests/Assembler/SiteNavigationTests.cs b/tests/Navigation.Tests/Assembler/SiteNavigationTests.cs index b07b78ea5..19c255b10 100644 --- a/tests/Navigation.Tests/Assembler/SiteNavigationTests.cs +++ b/tests/Navigation.Tests/Assembler/SiteNavigationTests.cs @@ -55,7 +55,7 @@ public void ConstructorCreatesSiteNavigation() navigation.Should().NotBeNull(); navigation.Url.Should().Be("/"); - navigation.NavigationTitle.Should().Be(observabilityNav.NavigationTitle); + navigation.NavigationTitle.Should().Be("Elastic Docs"); navigation.NavigationItems.Should().HaveCount(2); } @@ -130,16 +130,16 @@ public void SitePrefixNormalizesSlashes(string? sitePrefix, string expectedRootU } [Theory] - [InlineData(null, "/observability/")] - [InlineData("", "/observability/")] - [InlineData("docs", "/docs/observability/")] - [InlineData("/docs", "/docs/observability/")] - [InlineData("docs/", "/docs/observability/")] - [InlineData("/docs/", "/docs/observability/")] - [InlineData("api/docs", "/api/docs/observability/")] - [InlineData("/api/docs", "/api/docs/observability/")] - [InlineData("api/docs/", "/api/docs/observability/")] - [InlineData("/api/docs/", "/api/docs/observability/")] + [InlineData(null, "/observability")] + [InlineData("", "/observability")] + [InlineData("docs", "/docs/observability")] + [InlineData("/docs", "/docs/observability")] + [InlineData("docs/", "/docs/observability")] + [InlineData("/docs/", "/docs/observability")] + [InlineData("api/docs", "/api/docs/observability")] + [InlineData("/api/docs", "/api/docs/observability")] + [InlineData("api/docs/", "/api/docs/observability")] + [InlineData("/api/docs/", "/api/docs/observability")] public void SitePrefixAppliedToNavigationItemUrls(string? sitePrefix, string expectedObservabilityUrl) { // language=yaml @@ -167,11 +167,11 @@ public void SitePrefixAppliedToNavigationItemUrls(string? sitePrefix, string exp } [Theory] - [InlineData(null, "/observability/", "/search/")] - [InlineData("docs", "/docs/observability/", "/docs/search/")] - [InlineData("/docs", "/docs/observability/", "/docs/search/")] - [InlineData("docs/", "/docs/observability/", "/docs/search/")] - [InlineData("/docs/", "/docs/observability/", "/docs/search/")] + [InlineData(null, "/observability", "/search")] + [InlineData("docs", "/docs/observability", "/docs/search")] + [InlineData("/docs", "/docs/observability", "/docs/search")] + [InlineData("docs/", "/docs/observability", "/docs/search")] + [InlineData("/docs/", "/docs/observability", "/docs/search")] public void SitePrefixAppliedToMultipleNavigationItems(string? sitePrefix, string expectedObsUrl, string expectedSearchUrl) { // language=yaml diff --git a/tests/Navigation.Tests/Isolation/ConstructorTests.cs b/tests/Navigation.Tests/Isolation/ConstructorTests.cs index 37b5739b7..88f483132 100644 --- a/tests/Navigation.Tests/Isolation/ConstructorTests.cs +++ b/tests/Navigation.Tests/Isolation/ConstructorTests.cs @@ -82,7 +82,7 @@ public void ConstructorCreatesFileNavigationLeafFromFileRef() navigation.NavigationItems.Should().HaveCount(0); var fileNav = navigation.Index.Should().BeOfType>().Subject; fileNav.NavigationTitle.Should().Be("getting-started"); - fileNav.Url.Should().Be("/getting-started/"); + fileNav.Url.Should().Be("/getting-started"); fileNav.Hidden.Should().BeFalse(); fileNav.NavigationRoot.Should().BeSameAs(navigation); fileNav.Parent.Should().BeSameAs(navigation); // Top-level files have DocumentationSetNavigation as parent @@ -108,7 +108,7 @@ public void ConstructorCreatesHiddenFileNavigationLeaf() navigation.NavigationItems.Should().HaveCount(0); var fileNav = navigation.Index.Should().BeOfType>().Subject; fileNav.Hidden.Should().BeTrue(); - fileNav.Url.Should().Be("/404/"); + fileNav.Url.Should().Be("/404"); } [Fact] @@ -157,15 +157,15 @@ public void ConstructorCreatesFolderNavigationWithChildren() navigation.NavigationItems.Should().HaveCount(1); var folder = navigation.NavigationItems.First().Should().BeOfType>().Subject; - folder.Url.Should().Be("/setup/"); + folder.Url.Should().Be("/setup"); folder.NavigationItems.Should().HaveCount(1); var firstFile = folder.Index.Should().BeOfType>().Subject; - firstFile.Url.Should().Be("/setup/"); // index.md becomes /setup + firstFile.Url.Should().Be("/setup"); // index.md becomes /setup firstFile.Parent.Should().BeSameAs(folder); var secondFile = folder.NavigationItems.ElementAt(0).Should().BeOfType>().Subject; - secondFile.Url.Should().Be("/setup/install/"); + secondFile.Url.Should().Be("/setup/install"); } [Fact] @@ -195,11 +195,11 @@ public void ConstructorCreatesTableOfContentsNavigationWithChildren() navigation.NavigationItems.Should().HaveCount(1); var toc = navigation.NavigationItems.First().Should().BeOfType>().Subject; - toc.Url.Should().Be("/api/"); + toc.Url.Should().Be("/api"); toc.NavigationItems.Should().HaveCount(0); var file = toc.Index.Should().BeOfType>().Subject; - file.Url.Should().Be("/api/"); // index.md becomes /api + file.Url.Should().Be("/api"); // index.md becomes /api file.Parent.Should().BeSameAs(toc); file.NavigationRoot.Should().BeSameAs(navigation); } @@ -236,10 +236,10 @@ public void ConstructorReadsTableOfContentsFromTocYmlFile() toc.NavigationItems.Should().HaveCount(1); var overview = toc.Index.Should().BeOfType>().Subject; - overview.Url.Should().Be("/api/overview/"); + overview.Url.Should().Be("/api/overview"); var reference = toc.NavigationItems.ElementAt(0).Should().BeOfType>().Subject; - reference.Url.Should().Be("/api/reference/"); + reference.Url.Should().Be("/api/reference"); } [Fact] diff --git a/tests/Navigation.Tests/Isolation/DynamicUrlTests.cs b/tests/Navigation.Tests/Isolation/DynamicUrlTests.cs index 05dfef4cd..92941cad5 100644 --- a/tests/Navigation.Tests/Isolation/DynamicUrlTests.cs +++ b/tests/Navigation.Tests/Isolation/DynamicUrlTests.cs @@ -37,23 +37,23 @@ public void DynamicUrlUpdatesWhenRootUrlChanges() var file = folder.Index; // Initial URL - file.Url.Should().Be("/setup/install/"); + file.Url.Should().Be("/setup/install"); // Change root URL navigation.HomeProvider = new NavigationHomeProvider("/v8.0", navigation.NavigationRoot); // URLs should update dynamically // Since folder has no index child, its URL is the first child's URL - folder.Url.Should().Be("/v8.0/setup/install/"); - file.Url.Should().Be("/v8.0/setup/install/"); + folder.Url.Should().Be("/v8.0/setup/install"); + file.Url.Should().Be("/v8.0/setup/install"); // Change root URL navigation.HomeProvider = new NavigationHomeProvider("/v9.0", navigation.NavigationRoot); // URLs should update dynamically // Since folder has no index child, its URL is the first child's URL - folder.Url.Should().Be("/v9.0/setup/install/"); - file.Url.Should().Be("/v9.0/setup/install/"); + folder.Url.Should().Be("/v9.0/setup/install"); + file.Url.Should().Be("/v9.0/setup/install"); } [Fact] @@ -80,12 +80,12 @@ public void UrlRootPropagatesCorrectlyThroughFolders() var innerFolder = outerFolder!.NavigationItems.First() as FolderNavigation; var file = innerFolder!.Index; - file.Url.Should().Be("/outer/inner/deep/"); + file.Url.Should().Be("/outer/inner/deep"); // Change root URL navigation.HomeProvider = new NavigationHomeProvider("/base", navigation.NavigationRoot); - file.Url.Should().Be("/base/outer/inner/deep/"); + file.Url.Should().Be("/base/outer/inner/deep"); } [Fact] @@ -110,7 +110,7 @@ public void FolderWithoutIndexUsesFirstChildUrl() var folder = navigation.NavigationItems.First() as FolderNavigation; // Folder has no index.md, so URL should be the first child's URL - folder!.Url.Should().Be("/guides/getting-started/"); + folder!.Url.Should().Be("/guides/getting-started"); } [Fact] @@ -136,14 +136,14 @@ public void FolderWithNestedChildren() var folder = navigation.NavigationItems.First() as FolderNavigation; // Folder has no index.md, so URL should be the first child's URL - folder!.Url.Should().Be("/guides/getting-started/"); + folder!.Url.Should().Be("/guides/getting-started"); var gettingStarted = folder.NavigationItems.First() as VirtualFileNavigation; gettingStarted.Should().NotBeNull(); - gettingStarted.Url.Should().Be("/guides/getting-started/"); + gettingStarted.Url.Should().Be("/guides/getting-started"); var advanced = gettingStarted.NavigationItems.First() as FileNavigationLeaf; advanced.Should().NotBeNull(); - advanced.Url.Should().Be("/guides/advanced/"); + advanced.Url.Should().Be("/guides/advanced"); advanced.Parent.Should().BeSameAs(gettingStarted); gettingStarted.Parent.Should().BeSameAs(folder); @@ -172,14 +172,14 @@ public void FolderWithNestedDeeplinkedChildren() var folder = navigation.NavigationItems.First() as FolderNavigation; // Folder has no index.md, so URL should be the first child's URL - folder!.Url.Should().Be("/guides/clients/getting-started/"); + folder!.Url.Should().Be("/guides/clients/getting-started"); var gettingStarted = folder.NavigationItems.First() as VirtualFileNavigation; gettingStarted.Should().NotBeNull(); - gettingStarted.Url.Should().Be("/guides/clients/getting-started/"); + gettingStarted.Url.Should().Be("/guides/clients/getting-started"); var advanced = gettingStarted.NavigationItems.First() as FileNavigationLeaf; advanced.Should().NotBeNull(); - advanced.Url.Should().Be("/guides/advanced/"); + advanced.Url.Should().Be("/guides/advanced"); advanced.Parent.Should().BeSameAs(gettingStarted); gettingStarted.Parent.Should().BeSameAs(folder); @@ -208,14 +208,14 @@ public void FolderWithNestedDeeplinkedOfIndexChildren() var folder = navigation.NavigationItems.First() as FolderNavigation; // Folder has no index.md, so URL should be the first child's URL - folder!.Url.Should().Be("/guides/clients/"); + folder!.Url.Should().Be("/guides/clients"); var gettingStarted = folder.NavigationItems.First() as VirtualFileNavigation; gettingStarted.Should().NotBeNull(); - gettingStarted.Url.Should().Be("/guides/clients/"); + gettingStarted.Url.Should().Be("/guides/clients"); var advanced = gettingStarted.NavigationItems.First() as FileNavigationLeaf; advanced.Should().NotBeNull(); - advanced.Url.Should().Be("/guides/advanced/"); + advanced.Url.Should().Be("/guides/advanced"); advanced.Parent.Should().BeSameAs(gettingStarted); gettingStarted.Parent.Should().BeSameAs(folder); @@ -243,7 +243,7 @@ public void FolderWithIndexUsesOwnUrl() var folder = navigation.NavigationItems.First() as FolderNavigation; // Folder has index.md, so URL should be the folder path - folder!.Url.Should().Be("/guides/"); + folder!.Url.Should().Be("/guides"); } [Fact] @@ -276,15 +276,15 @@ public void UrlRootChangesForTableOfContentsNavigation() var file = toc!.Index; // The TOC becomes the new URL root, so the file URL is based on TOC's URL - toc.Url.Should().Be("/guides/api/reference/"); - file.Url.Should().Be("/guides/api/reference/"); + toc.Url.Should().Be("/guides/api/reference"); + file.Url.Should().Be("/guides/api/reference"); // Change root URL navigation.HomeProvider = new NavigationHomeProvider("/v2", navigation.NavigationRoot); // Both TOC and file URLs should update - navigation.Url.Should().Be("/v2/guides/api/reference/"); - toc.Url.Should().Be("/v2/guides/api/reference/"); - file.Url.Should().Be("/v2/guides/api/reference/"); + navigation.Url.Should().Be("/v2/guides/api/reference"); + toc.Url.Should().Be("/v2/guides/api/reference"); + file.Url.Should().Be("/v2/guides/api/reference"); } } diff --git a/tests/Navigation.Tests/Isolation/FileNavigationTests.cs b/tests/Navigation.Tests/Isolation/FileNavigationTests.cs index 65c7ea41c..43c06ed02 100644 --- a/tests/Navigation.Tests/Isolation/FileNavigationTests.cs +++ b/tests/Navigation.Tests/Isolation/FileNavigationTests.cs @@ -33,7 +33,7 @@ public void FileWithNoChildrenCreatesFileNavigationLeaf() navigation.NavigationItems.Should().HaveCount(0); var fileNav = navigation.Index.Should().BeOfType>().Subject; - fileNav.Url.Should().Be("/getting-started/"); + fileNav.Url.Should().Be("/getting-started"); } [Fact] @@ -58,15 +58,15 @@ public void FileWithChildrenCreatesFileNavigation() navigation.NavigationItems.Should().HaveCount(1); var fileNav = navigation.NavigationItems.First().Should().BeOfType>().Subject; - fileNav.Url.Should().Be("/guide/"); + fileNav.Url.Should().Be("/guide"); fileNav.NavigationItems.Should().HaveCount(2); var section1 = fileNav.NavigationItems.ElementAt(0).Should().BeOfType>().Subject; - section1.Url.Should().Be("/section1/"); + section1.Url.Should().Be("/section1"); section1.Parent.Should().BeSameAs(fileNav); var section2 = fileNav.NavigationItems.ElementAt(1).Should().BeOfType>().Subject; - section2.Url.Should().Be("/section2/"); + section2.Url.Should().Be("/section2"); section2.Parent.Should().BeSameAs(fileNav); } @@ -92,15 +92,15 @@ public void FileWithChildrenDeeplinksPreservesPaths() navigation.NavigationItems.Should().HaveCount(1); var fileNav = navigation.NavigationItems.First().Should().BeOfType>().Subject; - fileNav.Url.Should().Be("/nest/guide/"); + fileNav.Url.Should().Be("/nest/guide"); fileNav.NavigationItems.Should().HaveCount(2); var section1 = fileNav.NavigationItems.ElementAt(0).Should().BeOfType>().Subject; - section1.Url.Should().Be("/nest/section1/"); + section1.Url.Should().Be("/nest/section1"); section1.Parent.Should().BeSameAs(fileNav); var section2 = fileNav.NavigationItems.ElementAt(1).Should().BeOfType>().Subject; - section2.Url.Should().Be("/nest/section2/"); + section2.Url.Should().Be("/nest/section2"); section2.Parent.Should().BeSameAs(fileNav); } @@ -127,16 +127,16 @@ public void FileWithNestedChildrenBuildsCorrectly() navigation.NavigationItems.Should().HaveCount(1); var guideFile = navigation.NavigationItems.First().Should().BeOfType>().Subject; - guideFile.Url.Should().Be("/guide/"); + guideFile.Url.Should().Be("/guide"); guideFile.NavigationItems.Should().HaveCount(1); var chapter1 = guideFile.NavigationItems.First().Should().BeOfType>().Subject; - chapter1.Url.Should().Be("/chapter1/"); + chapter1.Url.Should().Be("/chapter1"); chapter1.Parent.Should().BeSameAs(guideFile); chapter1.NavigationItems.Should().HaveCount(1); var subsection = chapter1.NavigationItems.First().Should().BeOfType>().Subject; - subsection.Url.Should().Be("/subsection/"); + subsection.Url.Should().Be("/subsection"); subsection.Parent.Should().BeSameAs(chapter1); } @@ -162,15 +162,15 @@ public void FileNavigationUrlUpdatesWhenRootChanges() var child = fileNav!.NavigationItems.First(); // Initial URLs - fileNav.Url.Should().Be("/guide/"); - child.Url.Should().Be("/section1/"); + fileNav.Url.Should().Be("/guide"); + child.Url.Should().Be("/section1"); // Change root URL navigation.HomeProvider = new NavigationHomeProvider("/v2", navigation.NavigationRoot); // URLs should update dynamically - fileNav.Url.Should().Be("/v2/guide/"); - child.Url.Should().Be("/v2/section1/"); + fileNav.Url.Should().Be("/v2/guide"); + child.Url.Should().Be("/v2/section1"); } [Fact] @@ -199,13 +199,13 @@ public void FileNavigationMixedWithFolderChildren() guideFile.NavigationItems.Should().HaveCount(2); var intro = guideFile.NavigationItems.ElementAt(0).Should().BeOfType>().Subject; - intro.Url.Should().Be("/intro/"); + intro.Url.Should().Be("/intro"); var advancedFolder = guideFile.NavigationItems.ElementAt(1).Should().BeOfType>().Subject; - advancedFolder.Url.Should().Be("/advanced/topics/"); // No index, uses first child + advancedFolder.Url.Should().Be("/advanced/topics"); // No index, uses first child advancedFolder.NavigationItems.Should().HaveCount(0); var topics = advancedFolder.Index.Should().BeOfType>().Subject; - topics.Url.Should().Be("/advanced/topics/"); + topics.Url.Should().Be("/advanced/topics"); } } diff --git a/tests/Navigation.Tests/Isolation/FolderIndexFileRefTests.cs b/tests/Navigation.Tests/Isolation/FolderIndexFileRefTests.cs index 13abc9dd9..43e31aa67 100644 --- a/tests/Navigation.Tests/Isolation/FolderIndexFileRefTests.cs +++ b/tests/Navigation.Tests/Isolation/FolderIndexFileRefTests.cs @@ -43,7 +43,7 @@ public async Task FolderWithFileCreatesCorrectStructure() var folder = navigation.NavigationItems.First().Should().BeOfType>().Subject; // Children should be scoped to the folder - folder.Url.Should().Be("/getting-started/getting-started/"); + folder.Url.Should().Be("/getting-started/getting-started"); folder.NavigationItems.Should().HaveCount(2); // install.md, configure.md // Verify no errors diff --git a/tests/Navigation.Tests/Isolation/NavigationStructureTests.cs b/tests/Navigation.Tests/Isolation/NavigationStructureTests.cs index f76b59961..71d40b2ab 100644 --- a/tests/Navigation.Tests/Isolation/NavigationStructureTests.cs +++ b/tests/Navigation.Tests/Isolation/NavigationStructureTests.cs @@ -102,7 +102,7 @@ public void CanQueryNavigationForBothInterfaceAndConcreteTypes() // Demonstrate type-safe LINQ queries work with the interface type var firstItem = allLeafItems.FirstOrDefault(l => l.Model.NavigationTitle == "first"); firstItem.Should().NotBeNull(); - firstItem.Url.Should().Be("/first/"); + firstItem.Url.Should().Be("/first"); } [Fact] @@ -165,31 +165,31 @@ public async Task ComplexNestedStructureBuildsCorrectly() // Second item: complex nested structure var setupFolder = navigation.NavigationItems.ElementAt(0).Should().BeOfType>().Subject; setupFolder.NavigationItems.Should().HaveCount(1); - setupFolder.Url.Should().Be("/setup/"); + setupFolder.Url.Should().Be("/setup"); var setupIndex = setupFolder.Index.Should().BeOfType>().Subject; - setupIndex.Url.Should().Be("/setup/"); // index.md becomes /setup + setupIndex.Url.Should().Be("/setup"); // index.md becomes /setup var advancedToc = setupFolder.NavigationItems.ElementAt(0).Should().BeOfType>().Subject; - advancedToc.Url.Should().Be("/setup/advanced/"); + advancedToc.Url.Should().Be("/setup/advanced"); // Advanced TOC has index.md and the nested performance TOC as children advancedToc.NavigationItems.Should().HaveCount(1); var advancedIndex = advancedToc.Index.Should().BeOfType>().Subject; - advancedIndex.Url.Should().Be("/setup/advanced/"); + advancedIndex.Url.Should().Be("/setup/advanced"); var performanceToc = advancedToc.NavigationItems.ElementAt(0).Should().BeOfType>().Subject; - performanceToc.Url.Should().Be("/setup/advanced/performance/"); + performanceToc.Url.Should().Be("/setup/advanced/performance"); performanceToc.NavigationItems.Should().HaveCount(2); var performanceIndex = performanceToc.Index.Should().BeOfType>().Subject; - performanceIndex.Url.Should().Be("/setup/advanced/performance/"); + performanceIndex.Url.Should().Be("/setup/advanced/performance"); var tuning = performanceToc.NavigationItems.ElementAt(0).Should().BeOfType>().Subject; - tuning.Url.Should().Be("/setup/advanced/performance/tuning/"); + tuning.Url.Should().Be("/setup/advanced/performance/tuning"); var benchmarks = performanceToc.NavigationItems.ElementAt(1).Should().BeOfType>().Subject; - benchmarks.Url.Should().Be("/setup/advanced/performance/benchmarks/"); + benchmarks.Url.Should().Be("/setup/advanced/performance/benchmarks"); // Third item: crosslink _ = navigation.NavigationItems.ElementAt(1).Should().BeOfType().Subject; @@ -240,21 +240,21 @@ public void NestedTocUrlsDoNotDuplicatePath() var navigation = new DocumentationSetNavigation(docSet, context, TestDocumentationFileFactory.Instance); var setupFolder = navigation.NavigationItems.First().Should().BeOfType>().Subject; - setupFolder.Url.Should().Be("/setup/"); + setupFolder.Url.Should().Be("/setup"); // Setup folder has index.md and advanced TOC setupFolder.NavigationItems.Should().HaveCount(1); var advancedToc = setupFolder.NavigationItems.ElementAt(0).Should().BeOfType>().Subject; // Verify the URL is /setup/advanced and not /setup/setup/advanced - advancedToc.Url.Should().Be("/setup/advanced/"); + advancedToc.Url.Should().Be("/setup/advanced"); // Advanced TOC has index.md and performance TOC advancedToc.NavigationItems.Should().HaveCount(1); var performanceToc = advancedToc.NavigationItems.ElementAt(0).Should().BeOfType>().Subject; // Verify the URL is /setup/advanced/performance and not /setup/advanced/setup/advanced/performance - performanceToc.Url.Should().Be("/setup/advanced/performance/"); + performanceToc.Url.Should().Be("/setup/advanced/performance"); context.Diagnostics.Should().BeEmpty(); } diff --git a/tests/Navigation.Tests/Isolation/PhysicalDocsetTests.cs b/tests/Navigation.Tests/Isolation/PhysicalDocsetTests.cs index c21906c98..f3baa1452 100644 --- a/tests/Navigation.Tests/Isolation/PhysicalDocsetTests.cs +++ b/tests/Navigation.Tests/Isolation/PhysicalDocsetTests.cs @@ -47,7 +47,7 @@ public async Task PhysicalDocsetCanBeNavigated() // Check by URL since folder names derive from index file titles var folderUrls = folders.Select(f => f.Url).ToList(); - folderUrls.Should().Contain("/contribute/"); + folderUrls.Should().Contain("/contribute"); // No errors or warnings should be emitted during navigation construction // Hints are acceptable for best practice guidance @@ -81,7 +81,7 @@ public async Task PhysicalDocsetNavigationHasCorrectUrls() // Find the contribute folder by URL var contributeFolder = navigation.NavigationItems.OfType>() - .FirstOrDefault(f => f.Url == "/contribute/"); + .FirstOrDefault(f => f.Url == "/contribute"); contributeFolder.Should().NotBeNull(); // Verify nested structure @@ -118,7 +118,7 @@ public async Task PhysicalDocsetNavigationIncludesNestedTocs() tocNavs.Should().NotBeEmpty(); // development TOC should exist (check by URL) - var developmentToc = tocNavs.FirstOrDefault(t => t.Url == "/development/"); + var developmentToc = tocNavs.FirstOrDefault(t => t.Url == "/development"); developmentToc.Should().NotBeNull(); developmentToc.NavigationItems.Should().HaveCount(3); From 8afb87e10dea4a106b9ab55ebb99d4ea045f62a0 Mon Sep 17 00:00:00 2001 From: Martijn Laarman Date: Fri, 7 Nov 2025 14:11:00 +0100 Subject: [PATCH 166/171] Re-enable the detection rule extension. --- .../BuildContext.cs | 2 +- .../Builder/ConfigurationFile.cs | 17 +- .../ConfigurationFileProvider.cs | 2 +- .../DocSet/DocumentationSetFile.cs.bak | 695 ----------------- .../DocSet/DocumentationSetFile.cs.bak2 | 696 ------------------ .../DetectionRulesReference.cs | 57 +- .../TableOfContents/RuleReference.cs | 14 +- .../Serialization/YamlStaticContext.cs | 2 +- .../{DocSet => Toc}/DocumentationSetFile.cs | 83 ++- .../{DocSet => Toc}/SiteNavigationFile.cs | 2 +- .../Assembler/SiteNavigation.cs | 2 +- .../Isolated/Leaf/FileNavigationLeaf.cs | 1 + .../Node/DocumentationSetNavigation.cs | 9 +- .../DetectionRules/DetectionRuleFile.cs | 47 +- .../DetectionRulesDocsBuilderExtension.cs | 61 +- .../Extensions/IDocsBuilderExtension.cs | 8 +- src/Elastic.Markdown/IO/DocumentationSet.cs | 24 +- .../AssembleSources.cs | 2 +- .../Building/AssemblerBuildService.cs | 2 +- .../Links/PublishEnvironmentUriResolver.cs | 2 +- .../Navigation/GlobalNavigationService.cs | 2 +- .../Navigation/NavigationPrefixChecker.cs | 2 +- .../NavigationBuildingTests.cs | 2 +- .../SiteNavigationTests.cs | 2 +- .../DocumentationSetFileTests.cs | 2 +- .../PhysicalDocsetTests.cs | 2 +- .../SiteNavigationFileTests.cs | 2 +- .../Assembler/ComplexSiteNavigationTests.cs | 2 +- .../Assembler/IdentifierCollectionTests.cs | 2 +- .../Assembler/SiteDocumentationSetsTests.cs | 2 +- .../Assembler/SiteNavigationTests.cs | 2 +- .../Isolation/ConstructorTests.cs | 2 +- .../Isolation/DynamicUrlTests.cs | 2 +- .../Isolation/FileInfoValidationTests.cs | 2 +- .../Isolation/FileNavigationTests.cs | 2 +- .../Isolation/FolderIndexFileRefTests.cs | 2 +- .../Isolation/NavigationStructureTests.cs | 2 +- .../Isolation/PhysicalDocsetTests.cs | 2 +- .../Isolation/ValidationTests.cs | 2 +- 39 files changed, 219 insertions(+), 1547 deletions(-) delete mode 100644 src/Elastic.Documentation.Configuration/DocSet/DocumentationSetFile.cs.bak delete mode 100644 src/Elastic.Documentation.Configuration/DocSet/DocumentationSetFile.cs.bak2 rename src/Elastic.Documentation.Configuration/{DocSet => Toc}/DocumentationSetFile.cs (88%) rename src/Elastic.Documentation.Configuration/{DocSet => Toc}/SiteNavigationFile.cs (99%) diff --git a/src/Elastic.Documentation.Configuration/BuildContext.cs b/src/Elastic.Documentation.Configuration/BuildContext.cs index 3cfbe23eb..0b650a703 100644 --- a/src/Elastic.Documentation.Configuration/BuildContext.cs +++ b/src/Elastic.Documentation.Configuration/BuildContext.cs @@ -6,10 +6,10 @@ using System.Reflection; using Elastic.Documentation.Configuration.Assembler; using Elastic.Documentation.Configuration.Builder; -using Elastic.Documentation.Configuration.DocSet; using Elastic.Documentation.Configuration.LegacyUrlMappings; using Elastic.Documentation.Configuration.Products; using Elastic.Documentation.Configuration.Synonyms; +using Elastic.Documentation.Configuration.Toc; using Elastic.Documentation.Configuration.Versions; using Elastic.Documentation.Diagnostics; diff --git a/src/Elastic.Documentation.Configuration/Builder/ConfigurationFile.cs b/src/Elastic.Documentation.Configuration/Builder/ConfigurationFile.cs index 21ce07128..8b3cf896f 100644 --- a/src/Elastic.Documentation.Configuration/Builder/ConfigurationFile.cs +++ b/src/Elastic.Documentation.Configuration/Builder/ConfigurationFile.cs @@ -4,8 +4,8 @@ using System.IO.Abstractions; using DotNet.Globbing; -using Elastic.Documentation.Configuration.DocSet; using Elastic.Documentation.Configuration.Products; +using Elastic.Documentation.Configuration.Toc; using Elastic.Documentation.Configuration.Versions; using Elastic.Documentation.Links; @@ -43,20 +43,11 @@ public record ConfigurationFile public IReadOnlyDictionary? OpenApiSpecifications { get; } - /// This is a documentation set that is not linked to by assembler. + /// This is a documentation set not linked to by assembler. /// Setting this to true relaxes a few restrictions such as mixing toc references with file and folder reference public bool DevelopmentDocs { get; } - // TODO ensure project key is `docs-content` - public bool IsNarrativeDocs => - Project is not null - && Project.Equals("Elastic documentation", StringComparison.OrdinalIgnoreCase); - - public ConfigurationFile( - DocumentationSetFile docSetFile, - IDocumentationSetContext context, - VersionsConfiguration versionsConfig, - ProductsConfiguration productsConfig) + public ConfigurationFile(DocumentationSetFile docSetFile, IDocumentationSetContext context, VersionsConfiguration versionsConfig, ProductsConfiguration productsConfig) { _context = context; ScopeDirectory = context.ConfigurationPath.Directory!; @@ -85,7 +76,7 @@ public ConfigurationFile( CrossLinkRepositories = [.. docSetFile.CrossLinks]; // Extensions - assuming they're not in DocumentationSetFile yet - Extensions = new([]); + Extensions = new EnabledExtensions(docSetFile.Extensions); // Read substitutions _substitutions = new(docSetFile.Subs, StringComparer.OrdinalIgnoreCase); diff --git a/src/Elastic.Documentation.Configuration/ConfigurationFileProvider.cs b/src/Elastic.Documentation.Configuration/ConfigurationFileProvider.cs index 9e80f413c..bc0909aac 100644 --- a/src/Elastic.Documentation.Configuration/ConfigurationFileProvider.cs +++ b/src/Elastic.Documentation.Configuration/ConfigurationFileProvider.cs @@ -6,8 +6,8 @@ using System.Text.RegularExpressions; using Elastic.Documentation.Configuration.Assembler; using Elastic.Documentation.Configuration.Converters; -using Elastic.Documentation.Configuration.DocSet; using Elastic.Documentation.Configuration.Serialization; +using Elastic.Documentation.Configuration.Toc; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using YamlDotNet.Serialization; diff --git a/src/Elastic.Documentation.Configuration/DocSet/DocumentationSetFile.cs.bak b/src/Elastic.Documentation.Configuration/DocSet/DocumentationSetFile.cs.bak deleted file mode 100644 index b0cf68b09..000000000 --- a/src/Elastic.Documentation.Configuration/DocSet/DocumentationSetFile.cs.bak +++ /dev/null @@ -1,695 +0,0 @@ -// Licensed to Elasticsearch B.V under one or more agreements. -// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. -// See the LICENSE file in the project root for more information - -using System.IO.Abstractions; -using Elastic.Documentation.Configuration.Products; -using Elastic.Documentation.Diagnostics; -using YamlDotNet.Core; -using YamlDotNet.Core.Events; -using YamlDotNet.Serialization; - -namespace Elastic.Documentation.Configuration.DocSet; - -[YamlSerializable] -public class TableOfContentsFile -{ - [YamlMember(Alias = "project")] - public string? Project { get; set; } - - [YamlMember(Alias = "toc")] - public TableOfContents TableOfContents { get; set; } = []; - - [YamlMember(Alias = "toc_no_hint")] - public List TocNoHint { get; set; } = []; - - /// - /// Parsed hint types to suppress. This is populated from TocNoHint during LoadAndResolve. - /// - [YamlIgnore] - public HashSet SuppressedHints { get; set; } = []; - - /// - /// Checks if a specific hint type should be suppressed. - /// - public bool ShouldSuppressHint(HintType hintType) => SuppressedHints.Contains(hintType); - - public static TableOfContentsFile Deserialize(string json) => - ConfigurationFileProvider.Deserializer.Deserialize(json); -} - -[YamlSerializable] -public class DocumentationSetFile : TableOfContentsFile -{ - [YamlMember(Alias = "max_toc_depth")] - public int MaxTocDepth { get; set; } = 2; - - [YamlMember(Alias = "dev_docs")] - public bool DevDocs { get; set; } - - [YamlMember(Alias = "cross_links")] - public List CrossLinks { get; set; } = []; - - [YamlMember(Alias = "exclude")] - public List Exclude { get; set; } = []; - - [YamlMember(Alias = "extensions")] - public List Extensions { get; set; } = []; - - [YamlMember(Alias = "subs")] - public Dictionary Subs { get; set; } = []; - - [YamlMember(Alias = "features")] - public DocumentationSetFeatures Features { get; set; } = new(); - - [YamlMember(Alias = "api")] - public Dictionary Api { get; set; } = []; - - // TODO remove this - [YamlMember(Alias = "products")] - public List Products { get; set; } = []; - - public static FileRef[] GetFileRefs(ITableOfContentsItem item) - { - if (item is FileRef fileRef) - return [fileRef]; - if (item is FolderRef folderRef) - return folderRef.Children.SelectMany(GetFileRefs).ToArray(); - if (item is IsolatedTableOfContentsRef tocRef) - return tocRef.Children.SelectMany(GetFileRefs).ToArray(); - if (item is CrossLinkRef crossLinkRef) - return []; - throw new Exception($"Unexpected item type {item.GetType().Name}"); - } - - private static new DocumentationSetFile Deserialize(string json) => - ConfigurationFileProvider.Deserializer.Deserialize(json); - - /// - /// Loads a DocumentationSetFile and recursively resolves all IsolatedTableOfContentsRef items, - /// replacing them with their resolved children and ensuring file paths carry over parent paths. - /// Validates the table of contents structure and emits diagnostics for issues. - /// - public static DocumentationSetFile LoadAndResolve(IDiagnosticsCollector collector, IFileInfo docsetPath, IFileSystem? fileSystem = null) - { - fileSystem ??= docsetPath.FileSystem; - var yaml = fileSystem.File.ReadAllText(docsetPath.FullName); - var sourceDirectory = docsetPath.Directory!; - return LoadAndResolve(collector, yaml, sourceDirectory, fileSystem); - } - - /// - /// Loads a DocumentationSetFile from YAML string and recursively resolves all IsolatedTableOfContentsRef items, - /// replacing them with their resolved children and ensuring file paths carry over parent paths. - /// Validates the table of contents structure and emits diagnostics for issues. - /// - public static DocumentationSetFile LoadAndResolve(IDiagnosticsCollector collector, string yaml, IDirectoryInfo sourceDirectory, IFileSystem? fileSystem = null) - { - fileSystem ??= sourceDirectory.FileSystem; - var docSet = Deserialize(yaml); - var docsetPath = fileSystem.Path.Combine(sourceDirectory.FullName, "docset.yml"); - - // Parse toc_no_hint configuration - docSet.SuppressedHints = ParseTocNoHint(docSet.TocNoHint); - - // Hardcode suppression for known problematic files - if (docsetPath.Contains("docs-content/solutions/toc.yml") || docsetPath.Contains("docs-content\\solutions\\toc.yml")) - { - docSet.SuppressedHints.Add(HintType.DeepLinkingVirtualFile); - } - - docSet.TableOfContents = ResolveTableOfContents(collector, docSet.TableOfContents, sourceDirectory, fileSystem, parentPath: "", context: docsetPath, docSet); - return docSet; - } - - /// - /// Parses the toc_no_hint configuration strings into HintType enums. - /// - private static HashSet ParseTocNoHint(List tocNoHint) - { - var result = new HashSet(); - foreach (var hint in tocNoHint) - { - if (Enum.TryParse(hint, ignoreCase: true, out var hintType)) - { - result.Add(hintType); - } - } - return result; - } - - - /// - /// Recursively resolves all IsolatedTableOfContentsRef items in a table of contents, - /// loading nested TOC files and prepending parent paths to all file references. - /// Preserves the hierarchy structure without flattening. - /// Validates items and emits diagnostics for issues. - /// - private static TableOfContents ResolveTableOfContents( - IDiagnosticsCollector collector, - IReadOnlyCollection items, - IDirectoryInfo baseDirectory, - IFileSystem fileSystem, - string parentPath, - string context, - TableOfContentsFile? tocFile = null - ) - { - var resolved = new TableOfContents(); - - foreach (var item in items) - { - var resolvedItem = item switch - { - IsolatedTableOfContentsRef tocRef => ResolveIsolatedToc(collector, tocRef, baseDirectory, fileSystem, parentPath, context, tocFile), - FileRef fileRef => ResolveFileRef(collector, fileRef, baseDirectory, fileSystem, parentPath, context, tocFile), - FolderRef folderRef => ResolveFolderRef(collector, folderRef, baseDirectory, fileSystem, parentPath, context, tocFile), - CrossLinkRef crossLink => ResolveCrossLinkRef(collector, crossLink, baseDirectory, fileSystem, parentPath, context), - _ => null - }; - - if (resolvedItem != null) - resolved.Add(resolvedItem); - } - - return resolved; - } - - /// - /// Resolves an IsolatedTableOfContentsRef by loading the TOC file and returning a new ref with resolved children. - /// Validates that the TOC has no children in parent YAML and that toc.yml exists. - /// The TOC's path is set to the full path (including parent path) for consistency with files and folders. - /// - private static ITableOfContentsItem? ResolveIsolatedToc( - IDiagnosticsCollector collector, - IsolatedTableOfContentsRef tocRef, - IDirectoryInfo baseDirectory, - IFileSystem fileSystem, - string parentPath, - string parentContext - ) - { - // TOC paths containing '/' are treated as relative to the context file's directory (full paths). - // Simple TOC names (no '/') are resolved relative to the parent path in the navigation hierarchy. - string fullTocPath; - if (tocRef.Path.Contains('/')) - { - // Path contains '/', treat as context-relative (full path from the context file's directory) - var contextDir = fileSystem.Path.GetDirectoryName(parentContext) ?? ""; - var contextRelativePath = fileSystem.Path.GetRelativePath(baseDirectory.FullName, contextDir); - if (contextRelativePath == ".") - contextRelativePath = ""; - - fullTocPath = string.IsNullOrEmpty(contextRelativePath) - ? tocRef.Path - : $"{contextRelativePath}/{tocRef.Path}"; - } - else - { - // Simple name, resolve relative to parent path - fullTocPath = string.IsNullOrEmpty(parentPath) ? tocRef.Path : $"{parentPath}/{tocRef.Path}"; - } - - var tocDirectory = fileSystem.DirectoryInfo.New(fileSystem.Path.Combine(baseDirectory.FullName, fullTocPath)); - var tocFilePath = fileSystem.Path.Combine(tocDirectory.FullName, "toc.yml"); - var tocYmlExists = fileSystem.File.Exists(tocFilePath); - - // Validate: TOC should not have children defined in parent YAML - if (tocRef.Children.Count > 0) - { - collector.EmitError(parentContext, - $"TableOfContents '{fullTocPath}' may not contain children, define children in '{fullTocPath}/toc.yml' instead."); - return null; - } - - // If TOC has children in parent YAML, still try to load from toc.yml (prefer toc.yml over parent YAML) - if (!tocYmlExists) - { - // Validate: toc.yml file must exist - collector.EmitError(parentContext, $"Table of contents file not found: {fullTocPath}/toc.yml"); - return new IsolatedTableOfContentsRef(fullTocPath, [], parentContext); - } - - var tocYaml = fileSystem.File.ReadAllText(tocFilePath); - var tocFile = TableOfContentsFile.Deserialize(tocYaml); - - // Recursively resolve children with the FULL TOC path as the parent path - // This ensures all file paths within the TOC include the TOC directory path - // The context for children is the toc.yml file that defines them - var resolvedChildren = ResolveTableOfContents(collector, tocFile.TableOfContents, baseDirectory, fileSystem, fullTocPath, tocFilePath); - - // Validate: TOC must have at least one child - if (resolvedChildren.Count == 0) - { - collector.EmitError(tocFilePath, $"Table of contents '{fullTocPath}' has no children defined"); - } - - // Return TOC ref with FULL path and resolved children - // The context remains the parent context (where this TOC was referenced) - return new IsolatedTableOfContentsRef(fullTocPath, resolvedChildren, parentContext); - } - - /// - /// Resolves a FileRef by prepending the parent path to the file path and recursively resolving children. - /// The parent path provides the correct context for child resolution. - /// - private static ITableOfContentsItem ResolveFileRef( - IDiagnosticsCollector collector, - FileRef fileRef, - IDirectoryInfo baseDirectory, - IFileSystem fileSystem, - string parentPath, - string context, - TableOfContentsFile? tocFile = null) - { - var fullPath = string.IsNullOrEmpty(parentPath) ? fileRef.Path : $"{parentPath}/{fileRef.Path}"; - - // Special validation for FolderIndexFileRef (folder+file combination) - // Validate BEFORE early return so we catch cases with no children - if (fileRef is FolderIndexFileRef) - { - var fileName = fileRef.Path; - var fileWithoutExtension = fileName.Replace(".md", ""); - - // Validate: deep linking is NOT supported for folder+file combination - // The file path should be simple (no '/'), or at most folder/file.md after prepending - if (fileName.Contains('/')) - { - collector.EmitError(context, - $"Deep linking on folder 'file' is not supported. Found file path '{fileName}' with '/'. Use simple file name only."); - } - - // Best practice: file name should match folder name (from parentPath) - // Only check if we're in a folder context (parentPath is not empty) - if (!string.IsNullOrEmpty(parentPath) && fileName != "index.md") - { - // Check if this hint type should be suppressed - if (tocFile?.ShouldSuppressHint(HintType.FolderFileNameMismatch) != true) - { - // Extract just the folder name from parentPath (in case it's nested like "guides/getting-started") - var folderName = parentPath.Contains('/') ? parentPath.Split('/')[^1] : parentPath; - - // Normalize for comparison: remove hyphens, underscores, and lowercase - // This allows "getting-started" to match "GettingStarted" or "getting_started" - var normalizedFile = fileWithoutExtension.Replace("-", "", StringComparison.Ordinal).Replace("_", "", StringComparison.Ordinal).ToLowerInvariant(); - var normalizedFolder = folderName.Replace("-", "", StringComparison.Ordinal).Replace("_", "", StringComparison.Ordinal).ToLowerInvariant(); - - if (!normalizedFile.Equals(normalizedFolder, StringComparison.Ordinal)) - { - collector.EmitHint(context, - $"File name '{fileName}' does not match folder name '{folderName}'. Best practice is to name the file the same as the folder (e.g., 'folder: {folderName}, file: {folderName}.md')."); - } - } - } - } - - if (fileRef.Children.Count == 0) - { - // Preserve specific types even when there are no children - return fileRef switch - { - FolderIndexFileRef => new FolderIndexFileRef(fullPath, fileRef.Hidden, [], context), - IndexFileRef => new IndexFileRef(fullPath, fileRef.Hidden, [], context), - _ => new FileRef(fullPath, fileRef.Hidden, [], context) - }; - } - - // Emit hint if file has children and uses deep-linking (path contains '/') - // This suggests using 'folder' instead of 'file' would be better - if (fileRef.Path.Contains('/') && fileRef.Children.Count > 0 && fileRef is not FolderIndexFileRef) - { - // Check if this hint type should be suppressed - if (tocFile?.ShouldSuppressHint(HintType.DeepLinkingVirtualFile) != true) - { - collector.EmitHint(context, - $"File '{fileRef.Path}' uses deep-linking with children. Consider using 'folder' instead of 'file' for better navigation structure. Virtual files are primarily intended to group sibling files together."); - } - } - - // Children of a file should be resolved in the same directory as the parent file. - // Special handling for FolderIndexFileRef (folder+file combinations from YAML): - // - These are created when both folder and file keys exist (e.g., "folder: path/to/dir, file: index.md") - // - Children should resolve to the folder path, not the parent TOC path - // Examples: - // - Top level: "nest/guide.md" (parentPath="") → children resolve to "nest/" - // - Simple file in folder: "guide.md" (parentPath="guides") → children resolve to "guides/" - // - User file with subpath: "clients/getting-started.md" (parentPath="guides") → children resolve to "guides/" - // - Folder+file (FolderIndexFileRef): "observability/apm/apm-server/index.md" → children resolve to directory of fullPath - string parentPathForChildren; - if (fileRef is FolderIndexFileRef) - { - // Folder+file combination - extract directory from fullPath - var lastSlashIndex = fullPath.LastIndexOf('/'); - parentPathForChildren = lastSlashIndex >= 0 ? fullPath[..lastSlashIndex] : ""; - } - else if (string.IsNullOrEmpty(parentPath)) - { - // Top level - extract directory from file path - var lastSlashIndex = fullPath.LastIndexOf('/'); - parentPathForChildren = lastSlashIndex >= 0 ? fullPath[..lastSlashIndex] : ""; - } - else - { - // In folder/TOC context - use parentPath directly, ignoring any subdirectory in the file reference - parentPathForChildren = parentPath; - } - - var resolvedChildren = ResolveTableOfContents(collector, fileRef.Children, baseDirectory, fileSystem, parentPathForChildren, context, tocFile); - - // Preserve the specific type when creating the resolved reference - return fileRef switch - { - FolderIndexFileRef => new FolderIndexFileRef(fullPath, fileRef.Hidden, resolvedChildren, context), - IndexFileRef => new IndexFileRef(fullPath, fileRef.Hidden, resolvedChildren, context), - _ => new FileRef(fullPath, fileRef.Hidden, resolvedChildren, context) - }; - } - - /// - /// Resolves a FolderRef by prepending the parent path to the folder path and recursively resolving children. - /// If no children are defined, auto-discovers .md files in the folder directory. - /// - private static ITableOfContentsItem ResolveFolderRef( - IDiagnosticsCollector collector, - FolderRef folderRef, - IDirectoryInfo baseDirectory, - IFileSystem fileSystem, - string parentPath, - string context) - { - // Folder paths containing '/' are treated as relative to the context file's directory (full paths). - // Simple folder names (no '/') are resolved relative to the parent path in the navigation hierarchy. - string fullPath; - if (folderRef.Path.Contains('/')) - { - // Path contains '/', treat as context-relative (full path from the context file's directory) - var contextDir = fileSystem.Path.GetDirectoryName(context) ?? ""; - var contextRelativePath = fileSystem.Path.GetRelativePath(baseDirectory.FullName, contextDir); - if (contextRelativePath == ".") - contextRelativePath = ""; - - fullPath = string.IsNullOrEmpty(contextRelativePath) - ? folderRef.Path - : $"{contextRelativePath}/{folderRef.Path}"; - } - else - { - // Simple name, resolve relative to parent path - fullPath = string.IsNullOrEmpty(parentPath) ? folderRef.Path : $"{parentPath}/{folderRef.Path}"; - } - - // If children are explicitly defined, resolve them - if (folderRef.Children.Count > 0) - { - var resolvedChildren = ResolveTableOfContents(collector, folderRef.Children, baseDirectory, fileSystem, fullPath, context); - return new FolderRef(fullPath, resolvedChildren, context); - } - - // No children defined - auto-discover .md files in the folder - var autoDiscoveredChildren = AutoDiscoverFolderFiles(collector, fullPath, baseDirectory, fileSystem, context); - return new FolderRef(fullPath, autoDiscoveredChildren, context); - } - - /// - /// Auto-discovers .md files in a folder directory and creates FileRef items for them. - /// If index.md exists, it's placed first. Otherwise, files are sorted alphabetically. - /// Files starting with '_' or '.' are excluded. - /// - private static TableOfContents AutoDiscoverFolderFiles( - IDiagnosticsCollector collector, - string folderPath, - IDirectoryInfo baseDirectory, - IFileSystem fileSystem, - string context) - { - var directoryPath = fileSystem.Path.Combine(baseDirectory.FullName, folderPath); - var directory = fileSystem.DirectoryInfo.New(directoryPath); - - if (!directory.Exists) - return []; - - // Find all .md files in the directory (not recursive) - var mdFiles = fileSystem.Directory - .GetFiles(directoryPath, "*.md") - .Select(f => fileSystem.FileInfo.New(f)) - .Where(f => !f.Name.StartsWith('_') && !f.Name.StartsWith('.')) - .OrderBy(f => f.Name) - .ToList(); - - if (mdFiles.Count == 0) - return []; - - // Separate index.md from other files - var indexFile = mdFiles.FirstOrDefault(f => f.Name.Equals("index.md", StringComparison.OrdinalIgnoreCase)); - var otherFiles = mdFiles.Where(f => !f.Name.Equals("index.md", StringComparison.OrdinalIgnoreCase)).ToList(); - - var children = new TableOfContents(); - - // Add index.md first if it exists - if (indexFile != null) - { - var indexRef = indexFile.Name.Equals("index.md", StringComparison.OrdinalIgnoreCase) - ? new IndexFileRef(indexFile.Name, false, [], context) - : new FileRef(indexFile.Name, false, [], context); - children.Add(indexRef); - } - - // Add other files sorted alphabetically - foreach (var file in otherFiles) - { - var fileRef = new FileRef(file.Name, false, [], context); - children.Add(fileRef); - } - - // Resolve the children with the folder path as parent to get correct full paths - return ResolveTableOfContents(collector, children, baseDirectory, fileSystem, folderPath, context); - } - - /// - /// Resolves a CrossLinkRef by recursively resolving children (though cross-links typically don't have children). - /// - private static ITableOfContentsItem ResolveCrossLinkRef( - IDiagnosticsCollector collector, - CrossLinkRef crossLinkRef, - IDirectoryInfo baseDirectory, - IFileSystem fileSystem, - string parentPath, - string context) - { - if (crossLinkRef.Children.Count == 0) - return new CrossLinkRef(crossLinkRef.CrossLinkUri, crossLinkRef.Title, crossLinkRef.Hidden, [], context); - - var resolvedChildren = ResolveTableOfContents(collector, crossLinkRef.Children, baseDirectory, fileSystem, parentPath, context); - - return new CrossLinkRef(crossLinkRef.CrossLinkUri, crossLinkRef.Title, crossLinkRef.Hidden, resolvedChildren, context); - } -} - -[YamlSerializable] -public class DocumentationSetFeatures -{ - [YamlMember(Alias = "primary-nav", ApplyNamingConventions = false)] - public bool? PrimaryNav { get; set; } - [YamlMember(Alias = "disable-github-edit-link", ApplyNamingConventions = false)] - public bool? DisableGithubEditLink { get; set; } -} - -public class TableOfContents : List -{ - public TableOfContents() { } - - public TableOfContents(IEnumerable items) : base(items) { } -} - - -/// -/// Represents an item in a table of contents (file, folder, or TOC reference). -/// -public interface ITableOfContentsItem -{ - /// - /// The full path of this item relative to the documentation source directory. - /// For files: includes .md extension (e.g., "guides/getting-started.md") - /// For folders: the folder path (e.g., "guides/advanced") - /// For TOCs: the path to the toc.yml directory (e.g., "development" or "guides/advanced") - /// - string Path { get; } - - /// - /// The path to the YAML file (docset.yml or toc.yml) that defined this item. - /// This provides context for where the item was declared in the configuration. - /// - string Context { get; } -} - -public record FileRef(string Path, bool Hidden, IReadOnlyCollection Children, string Context) - : ITableOfContentsItem; - -public record IndexFileRef(string Path, bool Hidden, IReadOnlyCollection Children, string Context) - : FileRef(Path, Hidden, Children, Context); - -/// -/// Represents a file reference created from a folder+file combination in YAML (e.g., "folder: path/to/dir, file: index.md"). -/// Children of this file should resolve relative to the folder path, not the parent TOC path. -/// -public record FolderIndexFileRef(string Path, bool Hidden, IReadOnlyCollection Children, string Context) - : IndexFileRef(Path, Hidden, Children, Context); - -public record CrossLinkRef(Uri CrossLinkUri, string? Title, bool Hidden, IReadOnlyCollection Children, string Context) - : ITableOfContentsItem -{ - // CrossLinks don't have a file system path, so we use the CrossLinkUri as the Path - public string Path => CrossLinkUri.ToString(); -} - -public record FolderRef(string Path, IReadOnlyCollection Children, string Context) - : ITableOfContentsItem; - -public record IsolatedTableOfContentsRef(string Path, IReadOnlyCollection Children, string Context) - : ITableOfContentsItem; - - -public class TocItemCollectionYamlConverter : IYamlTypeConverter -{ - public bool Accepts(Type type) => type == typeof(TableOfContents); - - public object ReadYaml(IParser parser, Type type, ObjectDeserializer rootDeserializer) - { - var collection = new TableOfContents(); - - if (!parser.TryConsume(out _)) - return collection; - - while (!parser.TryConsume(out _)) - { - var item = rootDeserializer(typeof(ITableOfContentsItem)); - if (item is ITableOfContentsItem tocItem) - collection.Add(tocItem); - } - - return collection; - } - - public void WriteYaml(IEmitter emitter, object? value, Type type, ObjectSerializer serializer) => - serializer.Invoke(value, type); -} - -public class TocItemYamlConverter : IYamlTypeConverter -{ - public bool Accepts(Type type) => type == typeof(ITableOfContentsItem); - - public object? ReadYaml(IParser parser, Type type, ObjectDeserializer rootDeserializer) - { - if (!parser.TryConsume(out _)) - return null; - - var dictionary = new Dictionary(); - - while (!parser.TryConsume(out _)) - { - var key = parser.Consume(); - - // Parse the value based on what type it is - object? value = null; - if (parser.Accept(out var scalarValue)) - { - value = scalarValue.Value; - _ = parser.MoveNext(); - } - else if (parser.Accept(out _)) - { - // This is a list - parse it manually for "children" - if (key.Value == "children") - { - // Parse the children list manually - var childrenList = new List(); - _ = parser.Consume(); - while (!parser.TryConsume(out _)) - { - var child = rootDeserializer(typeof(ITableOfContentsItem)); - if (child is ITableOfContentsItem tocItem) - childrenList.Add(tocItem); - } - value = childrenList; - } - else - { - // For other lists, just skip them - parser.SkipThisAndNestedEvents(); - } - } - else if (parser.Accept(out _)) - { - // This is a nested mapping - skip it - parser.SkipThisAndNestedEvents(); - } - - dictionary[key.Value] = value; - } - - var children = GetChildren(dictionary); - - // Context will be set during LoadAndResolve, use empty string as placeholder during deserialization - const string placeholderContext = ""; - - // Check for folder+file combination (e.g., folder: getting-started, file: getting-started.md) - // This represents a folder with a specific index file - // The file becomes a child of the folder (as FolderIndexFileRef), and user-specified children follow - if (dictionary.TryGetValue("folder", out var folderPath) && folderPath is string folder && - dictionary.TryGetValue("file", out var filePath) && filePath is string file) - { - // Create the index file reference (FolderIndexFileRef to mark it as the folder's index) - // Store ONLY the file name - the folder path will be prepended during resolution - // This allows validation to check if the file itself has deep paths - var indexFile = new FolderIndexFileRef(file, false, [], placeholderContext); - - // Create a list with the index file first, followed by user-specified children - var folderChildren = new List { indexFile }; - folderChildren.AddRange(children); - - // Return a FolderRef with the index file and children - // The folder path can be deep (e.g., "guides/getting-started"), that's OK - return new FolderRef(folder, folderChildren, placeholderContext); - } - - // Check for file reference (file: or hidden:) - if (dictionary.TryGetValue("file", out var filePathOnly) && filePathOnly is string fileOnly) - return fileOnly == "index.md" ? new IndexFileRef(fileOnly, false, children, placeholderContext) : new FileRef(fileOnly, false, children, placeholderContext); - - if (dictionary.TryGetValue("hidden", out var hiddenPath) && hiddenPath is string p) - return p == "index.md" ? new IndexFileRef(p, true, children, placeholderContext) : new FileRef(p, true, children, placeholderContext); - - // Check for crosslink reference - if (dictionary.TryGetValue("crosslink", out var crosslink) && crosslink is string crosslinkStr) - { - var title = dictionary.TryGetValue("title", out var t) && t is string titleStr ? titleStr : null; - var isHidden = dictionary.TryGetValue("hidden", out var h) && h is bool hiddenBool && hiddenBool; - return new CrossLinkRef(new Uri(crosslinkStr), title, isHidden, children, placeholderContext); - } - - // Check for folder reference - if (dictionary.TryGetValue("folder", out var folderPathOnly) && folderPathOnly is string folderOnly) - return new FolderRef(folderOnly, children, placeholderContext); - - // Check for toc reference - if (dictionary.TryGetValue("toc", out var tocPath) && tocPath is string source) - return new IsolatedTableOfContentsRef(source, children, placeholderContext); - - return null; - } - - private IReadOnlyCollection GetChildren(Dictionary dictionary) - { - if (!dictionary.TryGetValue("children", out var childrenObj)) - return []; - - // Children have already been deserialized as List - if (childrenObj is List tocItems) - return tocItems; - - return []; - } - - public void WriteYaml(IEmitter emitter, object? value, Type type, ObjectSerializer serializer) => - serializer.Invoke(value, type); -} diff --git a/src/Elastic.Documentation.Configuration/DocSet/DocumentationSetFile.cs.bak2 b/src/Elastic.Documentation.Configuration/DocSet/DocumentationSetFile.cs.bak2 deleted file mode 100644 index 37e4001f2..000000000 --- a/src/Elastic.Documentation.Configuration/DocSet/DocumentationSetFile.cs.bak2 +++ /dev/null @@ -1,696 +0,0 @@ -// Licensed to Elasticsearch B.V under one or more agreements. -// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. -// See the LICENSE file in the project root for more information - -using System.IO.Abstractions; -using Elastic.Documentation.Configuration.Products; -using Elastic.Documentation.Diagnostics; -using YamlDotNet.Core; -using YamlDotNet.Core.Events; -using YamlDotNet.Serialization; - -namespace Elastic.Documentation.Configuration.DocSet; - -[YamlSerializable] -public class TableOfContentsFile -{ - [YamlMember(Alias = "project")] - public string? Project { get; set; } - - [YamlMember(Alias = "toc")] - public TableOfContents TableOfContents { get; set; } = []; - - [YamlMember(Alias = "toc_no_hint")] - public List TocNoHint { get; set; } = []; - - /// - /// Parsed hint types to suppress. This is populated from TocNoHint during LoadAndResolve. - /// - [YamlIgnore] - public HashSet SuppressedHints { get; set; } = []; - - /// - /// Checks if a specific hint type should be suppressed. - /// - public bool ShouldSuppressHint(HintType hintType) => SuppressedHints.Contains(hintType); - - public static TableOfContentsFile Deserialize(string json) => - ConfigurationFileProvider.Deserializer.Deserialize(json); -} - -[YamlSerializable] -public class DocumentationSetFile : TableOfContentsFile -{ - [YamlMember(Alias = "max_toc_depth")] - public int MaxTocDepth { get; set; } = 2; - - [YamlMember(Alias = "dev_docs")] - public bool DevDocs { get; set; } - - [YamlMember(Alias = "cross_links")] - public List CrossLinks { get; set; } = []; - - [YamlMember(Alias = "exclude")] - public List Exclude { get; set; } = []; - - [YamlMember(Alias = "extensions")] - public List Extensions { get; set; } = []; - - [YamlMember(Alias = "subs")] - public Dictionary Subs { get; set; } = []; - - [YamlMember(Alias = "features")] - public DocumentationSetFeatures Features { get; set; } = new(); - - [YamlMember(Alias = "api")] - public Dictionary Api { get; set; } = []; - - // TODO remove this - [YamlMember(Alias = "products")] - public List Products { get; set; } = []; - - public static FileRef[] GetFileRefs(ITableOfContentsItem item) - { - if (item is FileRef fileRef) - return [fileRef]; - if (item is FolderRef folderRef) - return folderRef.Children.SelectMany(GetFileRefs).ToArray(); - if (item is IsolatedTableOfContentsRef tocRef) - return tocRef.Children.SelectMany(GetFileRefs).ToArray(); - if (item is CrossLinkRef crossLinkRef) - return []; - throw new Exception($"Unexpected item type {item.GetType().Name}"); - } - - private static new DocumentationSetFile Deserialize(string json) => - ConfigurationFileProvider.Deserializer.Deserialize(json); - - /// - /// Loads a DocumentationSetFile and recursively resolves all IsolatedTableOfContentsRef items, - /// replacing them with their resolved children and ensuring file paths carry over parent paths. - /// Validates the table of contents structure and emits diagnostics for issues. - /// - public static DocumentationSetFile LoadAndResolve(IDiagnosticsCollector collector, IFileInfo docsetPath, IFileSystem? fileSystem = null) - { - fileSystem ??= docsetPath.FileSystem; - var yaml = fileSystem.File.ReadAllText(docsetPath.FullName); - var sourceDirectory = docsetPath.Directory!; - return LoadAndResolve(collector, yaml, sourceDirectory, fileSystem); - } - - /// - /// Loads a DocumentationSetFile from YAML string and recursively resolves all IsolatedTableOfContentsRef items, - /// replacing them with their resolved children and ensuring file paths carry over parent paths. - /// Validates the table of contents structure and emits diagnostics for issues. - /// - public static DocumentationSetFile LoadAndResolve(IDiagnosticsCollector collector, string yaml, IDirectoryInfo sourceDirectory, IFileSystem? fileSystem = null) - { - fileSystem ??= sourceDirectory.FileSystem; - var docSet = Deserialize(yaml); - var docsetPath = fileSystem.Path.Combine(sourceDirectory.FullName, "docset.yml"); - - // Parse toc_no_hint configuration - docSet.SuppressedHints = ParseTocNoHint(docSet.TocNoHint); - - // Hardcode suppression for known problematic files - if (docsetPath.Contains("docs-content/solutions/toc.yml") || docsetPath.Contains("docs-content\\solutions\\toc.yml")) - { - docSet.SuppressedHints.Add(HintType.DeepLinkingVirtualFile); - } - - docSet.TableOfContents = ResolveTableOfContents(collector, docSet.TableOfContents, sourceDirectory, fileSystem, parentPath: "", context: docsetPath, docSet); - return docSet; - } - - /// - /// Parses the toc_no_hint configuration strings into HintType enums. - /// - private static HashSet ParseTocNoHint(List tocNoHint) - { - var result = new HashSet(); - foreach (var hint in tocNoHint) - { - if (Enum.TryParse(hint, ignoreCase: true, out var hintType)) - { - result.Add(hintType); - } - } - return result; - } - - - /// - /// Recursively resolves all IsolatedTableOfContentsRef items in a table of contents, - /// loading nested TOC files and prepending parent paths to all file references. - /// Preserves the hierarchy structure without flattening. - /// Validates items and emits diagnostics for issues. - /// - private static TableOfContents ResolveTableOfContents( - IDiagnosticsCollector collector, - IReadOnlyCollection items, - IDirectoryInfo baseDirectory, - IFileSystem fileSystem, - string parentPath, - string context, - TableOfContentsFile? tocFile = null - ) - { - var resolved = new TableOfContents(); - - foreach (var item in items) - { - var resolvedItem = item switch - { - IsolatedTableOfContentsRef tocRef => ResolveIsolatedToc(collector, tocRef, baseDirectory, fileSystem, parentPath, context, tocFile), - FileRef fileRef => ResolveFileRef(collector, fileRef, baseDirectory, fileSystem, parentPath, context, tocFile), - FolderRef folderRef => ResolveFolderRef(collector, folderRef, baseDirectory, fileSystem, parentPath, context, tocFile), - CrossLinkRef crossLink => ResolveCrossLinkRef(collector, crossLink, baseDirectory, fileSystem, parentPath, context), - _ => null - }; - - if (resolvedItem != null) - resolved.Add(resolvedItem); - } - - return resolved; - } - - /// - /// Resolves an IsolatedTableOfContentsRef by loading the TOC file and returning a new ref with resolved children. - /// Validates that the TOC has no children in parent YAML and that toc.yml exists. - /// The TOC's path is set to the full path (including parent path) for consistency with files and folders. - /// - private static ITableOfContentsItem? ResolveIsolatedToc( - IDiagnosticsCollector collector, - IsolatedTableOfContentsRef tocRef, - IDirectoryInfo baseDirectory, - IFileSystem fileSystem, - string parentPath, - string parentContext - ) - { - // TOC paths containing '/' are treated as relative to the context file's directory (full paths). - // Simple TOC names (no '/') are resolved relative to the parent path in the navigation hierarchy. - string fullTocPath; - if (tocRef.Path.Contains('/')) - { - // Path contains '/', treat as context-relative (full path from the context file's directory) - var contextDir = fileSystem.Path.GetDirectoryName(parentContext) ?? ""; - var contextRelativePath = fileSystem.Path.GetRelativePath(baseDirectory.FullName, contextDir); - if (contextRelativePath == ".") - contextRelativePath = ""; - - fullTocPath = string.IsNullOrEmpty(contextRelativePath) - ? tocRef.Path - : $"{contextRelativePath}/{tocRef.Path}"; - } - else - { - // Simple name, resolve relative to parent path - fullTocPath = string.IsNullOrEmpty(parentPath) ? tocRef.Path : $"{parentPath}/{tocRef.Path}"; - } - - var tocDirectory = fileSystem.DirectoryInfo.New(fileSystem.Path.Combine(baseDirectory.FullName, fullTocPath)); - var tocFilePath = fileSystem.Path.Combine(tocDirectory.FullName, "toc.yml"); - var tocYmlExists = fileSystem.File.Exists(tocFilePath); - - // Validate: TOC should not have children defined in parent YAML - if (tocRef.Children.Count > 0) - { - collector.EmitError(parentContext, - $"TableOfContents '{fullTocPath}' may not contain children, define children in '{fullTocPath}/toc.yml' instead."); - return null; - } - - // If TOC has children in parent YAML, still try to load from toc.yml (prefer toc.yml over parent YAML) - if (!tocYmlExists) - { - // Validate: toc.yml file must exist - collector.EmitError(parentContext, $"Table of contents file not found: {fullTocPath}/toc.yml"); - return new IsolatedTableOfContentsRef(fullTocPath, [], parentContext); - } - - var tocYaml = fileSystem.File.ReadAllText(tocFilePath); - var tocFile = TableOfContentsFile.Deserialize(tocYaml); - - // Recursively resolve children with the FULL TOC path as the parent path - // This ensures all file paths within the TOC include the TOC directory path - // The context for children is the toc.yml file that defines them - var resolvedChildren = ResolveTableOfContents(collector, tocFile.TableOfContents, baseDirectory, fileSystem, fullTocPath, tocFilePath); - - // Validate: TOC must have at least one child - if (resolvedChildren.Count == 0) - { - collector.EmitError(tocFilePath, $"Table of contents '{fullTocPath}' has no children defined"); - } - - // Return TOC ref with FULL path and resolved children - // The context remains the parent context (where this TOC was referenced) - return new IsolatedTableOfContentsRef(fullTocPath, resolvedChildren, parentContext); - } - - /// - /// Resolves a FileRef by prepending the parent path to the file path and recursively resolving children. - /// The parent path provides the correct context for child resolution. - /// - private static ITableOfContentsItem ResolveFileRef( - IDiagnosticsCollector collector, - FileRef fileRef, - IDirectoryInfo baseDirectory, - IFileSystem fileSystem, - string parentPath, - string context, - TableOfContentsFile? tocFile = null) - { - var fullPath = string.IsNullOrEmpty(parentPath) ? fileRef.Path : $"{parentPath}/{fileRef.Path}"; - - // Special validation for FolderIndexFileRef (folder+file combination) - // Validate BEFORE early return so we catch cases with no children - if (fileRef is FolderIndexFileRef) - { - var fileName = fileRef.Path; - var fileWithoutExtension = fileName.Replace(".md", ""); - - // Validate: deep linking is NOT supported for folder+file combination - // The file path should be simple (no '/'), or at most folder/file.md after prepending - if (fileName.Contains('/')) - { - collector.EmitError(context, - $"Deep linking on folder 'file' is not supported. Found file path '{fileName}' with '/'. Use simple file name only."); - } - - // Best practice: file name should match folder name (from parentPath) - // Only check if we're in a folder context (parentPath is not empty) - if (!string.IsNullOrEmpty(parentPath) && fileName != "index.md") - { - // Check if this hint type should be suppressed - if (tocFile?.ShouldSuppressHint(HintType.FolderFileNameMismatch) != true) - { - // Extract just the folder name from parentPath (in case it's nested like "guides/getting-started") - var folderName = parentPath.Contains('/') ? parentPath.Split('/')[^1] : parentPath; - - // Normalize for comparison: remove hyphens, underscores, and lowercase - // This allows "getting-started" to match "GettingStarted" or "getting_started" - var normalizedFile = fileWithoutExtension.Replace("-", "", StringComparison.Ordinal).Replace("_", "", StringComparison.Ordinal).ToLowerInvariant(); - var normalizedFolder = folderName.Replace("-", "", StringComparison.Ordinal).Replace("_", "", StringComparison.Ordinal).ToLowerInvariant(); - - if (!normalizedFile.Equals(normalizedFolder, StringComparison.Ordinal)) - { - collector.EmitHint(context, - $"File name '{fileName}' does not match folder name '{folderName}'. Best practice is to name the file the same as the folder (e.g., 'folder: {folderName}, file: {folderName}.md')."); - } - } - } - } - - if (fileRef.Children.Count == 0) - { - // Preserve specific types even when there are no children - return fileRef switch - { - FolderIndexFileRef => new FolderIndexFileRef(fullPath, fileRef.Hidden, [], context), - IndexFileRef => new IndexFileRef(fullPath, fileRef.Hidden, [], context), - _ => new FileRef(fullPath, fileRef.Hidden, [], context) - }; - } - - // Emit hint if file has children and uses deep-linking (path contains '/') - // This suggests using 'folder' instead of 'file' would be better - if (fileRef.Path.Contains('/') && fileRef.Children.Count > 0 && fileRef is not FolderIndexFileRef) - { - // Check if this hint type should be suppressed - if (tocFile?.ShouldSuppressHint(HintType.DeepLinkingVirtualFile) != true) - { - collector.EmitHint(context, - $"File '{fileRef.Path}' uses deep-linking with children. Consider using 'folder' instead of 'file' for better navigation structure. Virtual files are primarily intended to group sibling files together."); - } - } - - // Children of a file should be resolved in the same directory as the parent file. - // Special handling for FolderIndexFileRef (folder+file combinations from YAML): - // - These are created when both folder and file keys exist (e.g., "folder: path/to/dir, file: index.md") - // - Children should resolve to the folder path, not the parent TOC path - // Examples: - // - Top level: "nest/guide.md" (parentPath="") → children resolve to "nest/" - // - Simple file in folder: "guide.md" (parentPath="guides") → children resolve to "guides/" - // - User file with subpath: "clients/getting-started.md" (parentPath="guides") → children resolve to "guides/" - // - Folder+file (FolderIndexFileRef): "observability/apm/apm-server/index.md" → children resolve to directory of fullPath - string parentPathForChildren; - if (fileRef is FolderIndexFileRef) - { - // Folder+file combination - extract directory from fullPath - var lastSlashIndex = fullPath.LastIndexOf('/'); - parentPathForChildren = lastSlashIndex >= 0 ? fullPath[..lastSlashIndex] : ""; - } - else if (string.IsNullOrEmpty(parentPath)) - { - // Top level - extract directory from file path - var lastSlashIndex = fullPath.LastIndexOf('/'); - parentPathForChildren = lastSlashIndex >= 0 ? fullPath[..lastSlashIndex] : ""; - } - else - { - // In folder/TOC context - use parentPath directly, ignoring any subdirectory in the file reference - parentPathForChildren = parentPath; - } - - var resolvedChildren = ResolveTableOfContents(collector, fileRef.Children, baseDirectory, fileSystem, parentPathForChildren, context, tocFile); - - // Preserve the specific type when creating the resolved reference - return fileRef switch - { - FolderIndexFileRef => new FolderIndexFileRef(fullPath, fileRef.Hidden, resolvedChildren, context), - IndexFileRef => new IndexFileRef(fullPath, fileRef.Hidden, resolvedChildren, context), - _ => new FileRef(fullPath, fileRef.Hidden, resolvedChildren, context) - }; - } - - /// - /// Resolves a FolderRef by prepending the parent path to the folder path and recursively resolving children. - /// If no children are defined, auto-discovers .md files in the folder directory. - /// - private static ITableOfContentsItem ResolveFolderRef( - IDiagnosticsCollector collector, - FolderRef folderRef, - IDirectoryInfo baseDirectory, - IFileSystem fileSystem, - string parentPath, - string context, - TableOfContentsFile? tocFile = null) - { - // Folder paths containing '/' are treated as relative to the context file's directory (full paths). - // Simple folder names (no '/') are resolved relative to the parent path in the navigation hierarchy. - string fullPath; - if (folderRef.Path.Contains('/')) - { - // Path contains '/', treat as context-relative (full path from the context file's directory) - var contextDir = fileSystem.Path.GetDirectoryName(context) ?? ""; - var contextRelativePath = fileSystem.Path.GetRelativePath(baseDirectory.FullName, contextDir); - if (contextRelativePath == ".") - contextRelativePath = ""; - - fullPath = string.IsNullOrEmpty(contextRelativePath) - ? folderRef.Path - : $"{contextRelativePath}/{folderRef.Path}"; - } - else - { - // Simple name, resolve relative to parent path - fullPath = string.IsNullOrEmpty(parentPath) ? folderRef.Path : $"{parentPath}/{folderRef.Path}"; - } - - // If children are explicitly defined, resolve them - if (folderRef.Children.Count > 0) - { - var resolvedChildren = ResolveTableOfContents(collector, folderRef.Children, baseDirectory, fileSystem, fullPath, context); - return new FolderRef(fullPath, resolvedChildren, context); - } - - // No children defined - auto-discover .md files in the folder - var autoDiscoveredChildren = AutoDiscoverFolderFiles(collector, fullPath, baseDirectory, fileSystem, context); - return new FolderRef(fullPath, autoDiscoveredChildren, context); - } - - /// - /// Auto-discovers .md files in a folder directory and creates FileRef items for them. - /// If index.md exists, it's placed first. Otherwise, files are sorted alphabetically. - /// Files starting with '_' or '.' are excluded. - /// - private static TableOfContents AutoDiscoverFolderFiles( - IDiagnosticsCollector collector, - string folderPath, - IDirectoryInfo baseDirectory, - IFileSystem fileSystem, - string context) - { - var directoryPath = fileSystem.Path.Combine(baseDirectory.FullName, folderPath); - var directory = fileSystem.DirectoryInfo.New(directoryPath); - - if (!directory.Exists) - return []; - - // Find all .md files in the directory (not recursive) - var mdFiles = fileSystem.Directory - .GetFiles(directoryPath, "*.md") - .Select(f => fileSystem.FileInfo.New(f)) - .Where(f => !f.Name.StartsWith('_') && !f.Name.StartsWith('.')) - .OrderBy(f => f.Name) - .ToList(); - - if (mdFiles.Count == 0) - return []; - - // Separate index.md from other files - var indexFile = mdFiles.FirstOrDefault(f => f.Name.Equals("index.md", StringComparison.OrdinalIgnoreCase)); - var otherFiles = mdFiles.Where(f => !f.Name.Equals("index.md", StringComparison.OrdinalIgnoreCase)).ToList(); - - var children = new TableOfContents(); - - // Add index.md first if it exists - if (indexFile != null) - { - var indexRef = indexFile.Name.Equals("index.md", StringComparison.OrdinalIgnoreCase) - ? new IndexFileRef(indexFile.Name, false, [], context) - : new FileRef(indexFile.Name, false, [], context); - children.Add(indexRef); - } - - // Add other files sorted alphabetically - foreach (var file in otherFiles) - { - var fileRef = new FileRef(file.Name, false, [], context); - children.Add(fileRef); - } - - // Resolve the children with the folder path as parent to get correct full paths - return ResolveTableOfContents(collector, children, baseDirectory, fileSystem, folderPath, context); - } - - /// - /// Resolves a CrossLinkRef by recursively resolving children (though cross-links typically don't have children). - /// - private static ITableOfContentsItem ResolveCrossLinkRef( - IDiagnosticsCollector collector, - CrossLinkRef crossLinkRef, - IDirectoryInfo baseDirectory, - IFileSystem fileSystem, - string parentPath, - string context) - { - if (crossLinkRef.Children.Count == 0) - return new CrossLinkRef(crossLinkRef.CrossLinkUri, crossLinkRef.Title, crossLinkRef.Hidden, [], context); - - var resolvedChildren = ResolveTableOfContents(collector, crossLinkRef.Children, baseDirectory, fileSystem, parentPath, context); - - return new CrossLinkRef(crossLinkRef.CrossLinkUri, crossLinkRef.Title, crossLinkRef.Hidden, resolvedChildren, context); - } -} - -[YamlSerializable] -public class DocumentationSetFeatures -{ - [YamlMember(Alias = "primary-nav", ApplyNamingConventions = false)] - public bool? PrimaryNav { get; set; } - [YamlMember(Alias = "disable-github-edit-link", ApplyNamingConventions = false)] - public bool? DisableGithubEditLink { get; set; } -} - -public class TableOfContents : List -{ - public TableOfContents() { } - - public TableOfContents(IEnumerable items) : base(items) { } -} - - -/// -/// Represents an item in a table of contents (file, folder, or TOC reference). -/// -public interface ITableOfContentsItem -{ - /// - /// The full path of this item relative to the documentation source directory. - /// For files: includes .md extension (e.g., "guides/getting-started.md") - /// For folders: the folder path (e.g., "guides/advanced") - /// For TOCs: the path to the toc.yml directory (e.g., "development" or "guides/advanced") - /// - string Path { get; } - - /// - /// The path to the YAML file (docset.yml or toc.yml) that defined this item. - /// This provides context for where the item was declared in the configuration. - /// - string Context { get; } -} - -public record FileRef(string Path, bool Hidden, IReadOnlyCollection Children, string Context) - : ITableOfContentsItem; - -public record IndexFileRef(string Path, bool Hidden, IReadOnlyCollection Children, string Context) - : FileRef(Path, Hidden, Children, Context); - -/// -/// Represents a file reference created from a folder+file combination in YAML (e.g., "folder: path/to/dir, file: index.md"). -/// Children of this file should resolve relative to the folder path, not the parent TOC path. -/// -public record FolderIndexFileRef(string Path, bool Hidden, IReadOnlyCollection Children, string Context) - : IndexFileRef(Path, Hidden, Children, Context); - -public record CrossLinkRef(Uri CrossLinkUri, string? Title, bool Hidden, IReadOnlyCollection Children, string Context) - : ITableOfContentsItem -{ - // CrossLinks don't have a file system path, so we use the CrossLinkUri as the Path - public string Path => CrossLinkUri.ToString(); -} - -public record FolderRef(string Path, IReadOnlyCollection Children, string Context) - : ITableOfContentsItem; - -public record IsolatedTableOfContentsRef(string Path, IReadOnlyCollection Children, string Context) - : ITableOfContentsItem; - - -public class TocItemCollectionYamlConverter : IYamlTypeConverter -{ - public bool Accepts(Type type) => type == typeof(TableOfContents); - - public object ReadYaml(IParser parser, Type type, ObjectDeserializer rootDeserializer) - { - var collection = new TableOfContents(); - - if (!parser.TryConsume(out _)) - return collection; - - while (!parser.TryConsume(out _)) - { - var item = rootDeserializer(typeof(ITableOfContentsItem)); - if (item is ITableOfContentsItem tocItem) - collection.Add(tocItem); - } - - return collection; - } - - public void WriteYaml(IEmitter emitter, object? value, Type type, ObjectSerializer serializer) => - serializer.Invoke(value, type); -} - -public class TocItemYamlConverter : IYamlTypeConverter -{ - public bool Accepts(Type type) => type == typeof(ITableOfContentsItem); - - public object? ReadYaml(IParser parser, Type type, ObjectDeserializer rootDeserializer) - { - if (!parser.TryConsume(out _)) - return null; - - var dictionary = new Dictionary(); - - while (!parser.TryConsume(out _)) - { - var key = parser.Consume(); - - // Parse the value based on what type it is - object? value = null; - if (parser.Accept(out var scalarValue)) - { - value = scalarValue.Value; - _ = parser.MoveNext(); - } - else if (parser.Accept(out _)) - { - // This is a list - parse it manually for "children" - if (key.Value == "children") - { - // Parse the children list manually - var childrenList = new List(); - _ = parser.Consume(); - while (!parser.TryConsume(out _)) - { - var child = rootDeserializer(typeof(ITableOfContentsItem)); - if (child is ITableOfContentsItem tocItem) - childrenList.Add(tocItem); - } - value = childrenList; - } - else - { - // For other lists, just skip them - parser.SkipThisAndNestedEvents(); - } - } - else if (parser.Accept(out _)) - { - // This is a nested mapping - skip it - parser.SkipThisAndNestedEvents(); - } - - dictionary[key.Value] = value; - } - - var children = GetChildren(dictionary); - - // Context will be set during LoadAndResolve, use empty string as placeholder during deserialization - const string placeholderContext = ""; - - // Check for folder+file combination (e.g., folder: getting-started, file: getting-started.md) - // This represents a folder with a specific index file - // The file becomes a child of the folder (as FolderIndexFileRef), and user-specified children follow - if (dictionary.TryGetValue("folder", out var folderPath) && folderPath is string folder && - dictionary.TryGetValue("file", out var filePath) && filePath is string file) - { - // Create the index file reference (FolderIndexFileRef to mark it as the folder's index) - // Store ONLY the file name - the folder path will be prepended during resolution - // This allows validation to check if the file itself has deep paths - var indexFile = new FolderIndexFileRef(file, false, [], placeholderContext); - - // Create a list with the index file first, followed by user-specified children - var folderChildren = new List { indexFile }; - folderChildren.AddRange(children); - - // Return a FolderRef with the index file and children - // The folder path can be deep (e.g., "guides/getting-started"), that's OK - return new FolderRef(folder, folderChildren, placeholderContext); - } - - // Check for file reference (file: or hidden:) - if (dictionary.TryGetValue("file", out var filePathOnly) && filePathOnly is string fileOnly) - return fileOnly == "index.md" ? new IndexFileRef(fileOnly, false, children, placeholderContext) : new FileRef(fileOnly, false, children, placeholderContext); - - if (dictionary.TryGetValue("hidden", out var hiddenPath) && hiddenPath is string p) - return p == "index.md" ? new IndexFileRef(p, true, children, placeholderContext) : new FileRef(p, true, children, placeholderContext); - - // Check for crosslink reference - if (dictionary.TryGetValue("crosslink", out var crosslink) && crosslink is string crosslinkStr) - { - var title = dictionary.TryGetValue("title", out var t) && t is string titleStr ? titleStr : null; - var isHidden = dictionary.TryGetValue("hidden", out var h) && h is bool hiddenBool && hiddenBool; - return new CrossLinkRef(new Uri(crosslinkStr), title, isHidden, children, placeholderContext); - } - - // Check for folder reference - if (dictionary.TryGetValue("folder", out var folderPathOnly) && folderPathOnly is string folderOnly) - return new FolderRef(folderOnly, children, placeholderContext); - - // Check for toc reference - if (dictionary.TryGetValue("toc", out var tocPath) && tocPath is string source) - return new IsolatedTableOfContentsRef(source, children, placeholderContext); - - return null; - } - - private IReadOnlyCollection GetChildren(Dictionary dictionary) - { - if (!dictionary.TryGetValue("children", out var childrenObj)) - return []; - - // Children have already been deserialized as List - if (childrenObj is List tocItems) - return tocItems; - - return []; - } - - public void WriteYaml(IEmitter emitter, object? value, Type type, ObjectSerializer serializer) => - serializer.Invoke(value, type); -} diff --git a/src/Elastic.Documentation.Configuration/Plugins/DetectionRules/TableOfContents/DetectionRulesReference.cs b/src/Elastic.Documentation.Configuration/Plugins/DetectionRules/TableOfContents/DetectionRulesReference.cs index 5eb2dd855..f64d4b2b3 100644 --- a/src/Elastic.Documentation.Configuration/Plugins/DetectionRules/TableOfContents/DetectionRulesReference.cs +++ b/src/Elastic.Documentation.Configuration/Plugins/DetectionRules/TableOfContents/DetectionRulesReference.cs @@ -2,56 +2,45 @@ // Elasticsearch B.V licenses this file to you under the Apache 2.0 License. // See the LICENSE file in the project root for more information -using Elastic.Documentation.Configuration.Builder; -using Elastic.Documentation.Configuration.DocSet; +using System.IO.Abstractions; +using Elastic.Documentation.Configuration.Toc; namespace Elastic.Documentation.Configuration.Plugins.DetectionRules.TableOfContents; public record RuleOverviewReference : FileRef { - - public IReadOnlyCollection DetectionRuleFolders { get; init; } - - private string ParentPath { get; } - private string TocContext { get; } + public IReadOnlyCollection DetectionRuleFolders { get; } public RuleOverviewReference( - string overviewFilePathRelativeToDocumentationSet, - string parentPath, - ConfigurationFile configuration, - IDocumentationSetContext context, - IReadOnlyCollection detectionRuleFolders, - string tocContext - ) - : base(overviewFilePathRelativeToDocumentationSet, overviewFilePathRelativeToDocumentationSet, false, [], tocContext) + string pathRelativeToDocumentationSet, + string pathRelativeToContainer, + IReadOnlyCollection detectionRulesFolders, + IReadOnlyCollection children, + string context + ) : base(pathRelativeToDocumentationSet, pathRelativeToContainer, false, children, context) { - ParentPath = parentPath; - TocContext = tocContext; - DetectionRuleFolders = detectionRuleFolders; - Children = CreateTableOfContentItems(configuration, context); + PathRelativeToDocumentationSet = pathRelativeToDocumentationSet; + PathRelativeToContainer = pathRelativeToContainer; + DetectionRuleFolders = detectionRulesFolders; + Children = children; + Context = context; } - private IReadOnlyCollection CreateTableOfContentItems(ConfigurationFile configuration, IDocumentationSetContext context) + public static IReadOnlyCollection CreateTableOfContentItems(IReadOnlyCollection sourceFolders, string context, IDirectoryInfo baseDirectory) { - _ = configuration; // Keep parameter for now for compatibility var tocItems = new List(); - foreach (var detectionRuleFolder in DetectionRuleFolders) + foreach (var detectionRuleFolder in sourceFolders) { - var children = ReadDetectionRuleFolder(context, detectionRuleFolder); + var children = ReadDetectionRuleFolder(detectionRuleFolder, context, baseDirectory); tocItems.AddRange(children); } return tocItems - .OrderBy(d => d is RuleReference r ? r.Rule.Name : null, StringComparer.OrdinalIgnoreCase) .ToArray(); } - private IReadOnlyCollection ReadDetectionRuleFolder(IDocumentationSetContext context, string detectionRuleFolder) + private static IReadOnlyCollection ReadDetectionRuleFolder(IDirectoryInfo directory, string context, IDirectoryInfo baseDirectory) { - var detectionRulesFolder = Path.Combine(ParentPath, detectionRuleFolder).TrimStart(Path.DirectorySeparatorChar); - var fs = context.ReadFileSystem; - var sourceDirectory = context.DocumentationSourceDirectory; - var directory = fs.DirectoryInfo.New(fs.Path.GetFullPath(fs.Path.Combine(sourceDirectory.FullName, detectionRulesFolder))); IReadOnlyCollection children = directory .EnumerateFiles("*.*", SearchOption.AllDirectories) .Where(f => !f.Attributes.HasFlag(FileAttributes.Hidden) && !f.Attributes.HasFlag(FileAttributes.System)) @@ -61,14 +50,12 @@ private IReadOnlyCollection ReadDetectionRuleFolder(IDocum .Where(f => !f.FullName.Contains($"{Path.DirectorySeparatorChar}_deprecated{Path.DirectorySeparatorChar}")) .Select(f => { - var relativePath = Path.GetRelativePath(sourceDirectory.FullName, f.FullName); + // baseDirectory is 'docs' rules live relative to docs parent '/' + var relativePath = Path.GetRelativePath(baseDirectory.Parent!.FullName, f.FullName); if (f.Extension == ".toml") - { - var rule = DetectionRule.From(f); - return new RuleReference(relativePath, detectionRuleFolder, true, [], rule, TocContext); - } + return new RuleReference(f, relativePath, context); - return new FileRef(relativePath, relativePath, false, [], TocContext); + return new FileRef(relativePath, relativePath, false, [], context); }) .ToArray(); diff --git a/src/Elastic.Documentation.Configuration/Plugins/DetectionRules/TableOfContents/RuleReference.cs b/src/Elastic.Documentation.Configuration/Plugins/DetectionRules/TableOfContents/RuleReference.cs index 24fe0eccd..da5d5bd1e 100644 --- a/src/Elastic.Documentation.Configuration/Plugins/DetectionRules/TableOfContents/RuleReference.cs +++ b/src/Elastic.Documentation.Configuration/Plugins/DetectionRules/TableOfContents/RuleReference.cs @@ -2,16 +2,10 @@ // Elasticsearch B.V licenses this file to you under the Apache 2.0 License. // See the LICENSE file in the project root for more information -using Elastic.Documentation.Configuration.DocSet; +using System.IO.Abstractions; +using Elastic.Documentation.Configuration.Toc; namespace Elastic.Documentation.Configuration.Plugins.DetectionRules.TableOfContents; -public record RuleReference( - string RelativePathRelativeToDocumentationSet, - string SourceDirectory, - bool Found, - IReadOnlyCollection Children, - DetectionRule Rule, - string Context -) - : FileRef(RelativePathRelativeToDocumentationSet, RelativePathRelativeToDocumentationSet, true, Children, Context); +public record RuleReference(IFileInfo FileInfo, string RelativePathRelativeToDocumentationSet, string Context) + : FileRef(RelativePathRelativeToDocumentationSet, RelativePathRelativeToDocumentationSet, true, [], Context); diff --git a/src/Elastic.Documentation.Configuration/Serialization/YamlStaticContext.cs b/src/Elastic.Documentation.Configuration/Serialization/YamlStaticContext.cs index c8e5a00f2..825983ed0 100644 --- a/src/Elastic.Documentation.Configuration/Serialization/YamlStaticContext.cs +++ b/src/Elastic.Documentation.Configuration/Serialization/YamlStaticContext.cs @@ -3,10 +3,10 @@ // See the LICENSE file in the project root for more information using Elastic.Documentation.Configuration.Assembler; -using Elastic.Documentation.Configuration.DocSet; using Elastic.Documentation.Configuration.LegacyUrlMappings; using Elastic.Documentation.Configuration.Products; using Elastic.Documentation.Configuration.Synonyms; +using Elastic.Documentation.Configuration.Toc; using Elastic.Documentation.Configuration.Versions; using YamlDotNet.Serialization; diff --git a/src/Elastic.Documentation.Configuration/DocSet/DocumentationSetFile.cs b/src/Elastic.Documentation.Configuration/Toc/DocumentationSetFile.cs similarity index 88% rename from src/Elastic.Documentation.Configuration/DocSet/DocumentationSetFile.cs rename to src/Elastic.Documentation.Configuration/Toc/DocumentationSetFile.cs index bba91be78..e5a67053d 100644 --- a/src/Elastic.Documentation.Configuration/DocSet/DocumentationSetFile.cs +++ b/src/Elastic.Documentation.Configuration/Toc/DocumentationSetFile.cs @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information using System.IO.Abstractions; +using Elastic.Documentation.Configuration.Plugins.DetectionRules.TableOfContents; using Elastic.Documentation.Configuration.Products; using Elastic.Documentation.Diagnostics; using Elastic.Documentation.Extensions; @@ -10,7 +11,7 @@ using YamlDotNet.Core.Events; using YamlDotNet.Serialization; -namespace Elastic.Documentation.Configuration.DocSet; +namespace Elastic.Documentation.Configuration.Toc; [YamlSerializable] public class TableOfContentsFile @@ -131,6 +132,7 @@ private static TableOfContents ResolveTableOfContents( var resolvedItem = item switch { IsolatedTableOfContentsRef tocRef => ResolveIsolatedToc(collector, tocRef, baseDirectory, fileSystem, parentPath, containerPath, context, suppressDiagnostics), + RuleOverviewReference ruleOverviewReference => ResolveRuleOverviewReference(collector, ruleOverviewReference, baseDirectory, fileSystem, parentPath, containerPath, context, suppressDiagnostics), FileRef fileRef => ResolveFileRef(collector, fileRef, baseDirectory, fileSystem, parentPath, containerPath, context, suppressDiagnostics), FolderRef folderRef => ResolveFolderRef(collector, folderRef, baseDirectory, fileSystem, parentPath, containerPath, context, suppressDiagnostics), CrossLinkRef crossLink => ResolveCrossLinkRef(collector, crossLink, baseDirectory, fileSystem, parentPath, containerPath, context), @@ -366,6 +368,60 @@ private static ITableOfContentsItem ResolveFileRef(IDiagnosticsCollector collect }; } + /// + /// Resolves a FolderRef by prepending the parent path to the folder path and recursively resolving children. + /// If no children are defined, auto-discovers .md files in the folder directory. + /// + private static ITableOfContentsItem ResolveRuleOverviewReference(IDiagnosticsCollector collector, + RuleOverviewReference ruleRef, + IDirectoryInfo baseDirectory, + IFileSystem fileSystem, + string parentPath, + string containerPath, + string context, + HashSet? suppressDiagnostics = null) + { + // Folder paths containing '/' are treated as relative to the context file's directory (full paths). + // Simple folder names (no '/') are resolved relative to the parent path in the navigation hierarchy. + string fullPath; + if (ruleRef.PathRelativeToDocumentationSet.Contains('/')) + { + // Path contains '/', treat as context-relative (full path from the context file's directory) + var contextDir = fileSystem.Path.GetDirectoryName(context) ?? ""; + var contextRelativePath = fileSystem.Path.GetRelativePath(baseDirectory.FullName, contextDir); + if (contextRelativePath == ".") + contextRelativePath = ""; + + fullPath = string.IsNullOrEmpty(contextRelativePath) + ? ruleRef.PathRelativeToDocumentationSet + : $"{contextRelativePath}/{ruleRef.PathRelativeToDocumentationSet}"; + } + else + { + // Simple name, resolve relative to parent path + fullPath = string.IsNullOrEmpty(parentPath) ? ruleRef.PathRelativeToDocumentationSet : $"{parentPath}/{ruleRef.PathRelativeToDocumentationSet}"; + } + + // Calculate PathRelativeToContainer: the folder path relative to its container + var pathRelativeToContainer = string.IsNullOrEmpty(containerPath) + ? fullPath + : fullPath.Substring(containerPath.Length + 1); + + // For children of folders, the container remains the same as the folder's container + var resolvedChildren = ResolveTableOfContents(collector, ruleRef.Children, baseDirectory, fileSystem, fullPath, containerPath, context, suppressDiagnostics); + + var fileInfo = fileSystem.NewFileInfo(baseDirectory.FullName, fullPath); + var tocSourceFolders = ruleRef.DetectionRuleFolders + .Select(f => fileSystem.NewDirInfo(fileInfo.Directory!.FullName, f)) + .ToList(); + var tomlChildren = RuleOverviewReference.CreateTableOfContentItems(tocSourceFolders, context, baseDirectory); + + var children = resolvedChildren.Concat(tomlChildren).ToList(); + + return new RuleOverviewReference(fullPath, pathRelativeToContainer, ruleRef.DetectionRuleFolders, children, context); + } + + /// /// Resolves a FolderRef by prepending the parent path to the folder path and recursively resolving children. /// If no children are defined, auto-discovers .md files in the folder directory. @@ -636,6 +692,20 @@ public class TocItemYamlConverter : IYamlTypeConverter } value = childrenList; } + + if (key.Value == "detection_rules") + { + // Parse the children list manually + var childrenList = new List(); + _ = parser.Consume(); + while (!parser.TryConsume(out _)) + { + if (parser.Accept(out scalarValue)) + childrenList.Add(scalarValue.Value); + _ = parser.MoveNext(); + } + value = childrenList.ToArray(); + } else { // For other lists, just skip them @@ -677,13 +747,24 @@ public class TocItemYamlConverter : IYamlTypeConverter // PathRelativeToContainer will be set during resolution return new FolderRef(folder, folder, folderChildren, placeholderContext); } + if (dictionary.TryGetValue("detection_rules", out var detectionRulesObj) && detectionRulesObj is string[] detectionRulesFolders && + dictionary.TryGetValue("file", out var detectionRulesFilePath) && detectionRulesFilePath is string detectionRulesFile) + { + // Create the index file reference (FolderIndexFileRef to mark it as the folder's index) + // Store ONLY the file name - the folder path will be prepended during resolution + // This allows validation to check if the file itself has deep paths + // PathRelativeToContainer will be set during resolution + return new RuleOverviewReference(detectionRulesFile, detectionRulesFile, detectionRulesFolders, children, placeholderContext); + } // Check for file reference (file: or hidden:) // PathRelativeToContainer will be set during resolution if (dictionary.TryGetValue("file", out var filePathOnly) && filePathOnly is string fileOnly) + { return fileOnly == "index.md" ? new IndexFileRef(fileOnly, fileOnly, false, children, placeholderContext) : new FileRef(fileOnly, fileOnly, false, children, placeholderContext); + } if (dictionary.TryGetValue("hidden", out var hiddenPath) && hiddenPath is string p) return p == "index.md" ? new IndexFileRef(p, p, true, children, placeholderContext) : new FileRef(p, p, true, children, placeholderContext); diff --git a/src/Elastic.Documentation.Configuration/DocSet/SiteNavigationFile.cs b/src/Elastic.Documentation.Configuration/Toc/SiteNavigationFile.cs similarity index 99% rename from src/Elastic.Documentation.Configuration/DocSet/SiteNavigationFile.cs rename to src/Elastic.Documentation.Configuration/Toc/SiteNavigationFile.cs index 941143968..a5df87e4f 100644 --- a/src/Elastic.Documentation.Configuration/DocSet/SiteNavigationFile.cs +++ b/src/Elastic.Documentation.Configuration/Toc/SiteNavigationFile.cs @@ -10,7 +10,7 @@ using YamlDotNet.Core.Events; using YamlDotNet.Serialization; -namespace Elastic.Documentation.Configuration.DocSet; +namespace Elastic.Documentation.Configuration.Toc; public record NavigationTocMapping { diff --git a/src/Elastic.Documentation.Navigation/Assembler/SiteNavigation.cs b/src/Elastic.Documentation.Navigation/Assembler/SiteNavigation.cs index 016793722..4f9c0c7f6 100644 --- a/src/Elastic.Documentation.Navigation/Assembler/SiteNavigation.cs +++ b/src/Elastic.Documentation.Navigation/Assembler/SiteNavigation.cs @@ -5,7 +5,7 @@ using System.Collections.Immutable; using System.Diagnostics; using Elastic.Documentation.Configuration.Assembler; -using Elastic.Documentation.Configuration.DocSet; +using Elastic.Documentation.Configuration.Toc; using Elastic.Documentation.Extensions; using Elastic.Documentation.Navigation.Isolated.Node; diff --git a/src/Elastic.Documentation.Navigation/Isolated/Leaf/FileNavigationLeaf.cs b/src/Elastic.Documentation.Navigation/Isolated/Leaf/FileNavigationLeaf.cs index e126786e7..55b17d3c3 100644 --- a/src/Elastic.Documentation.Navigation/Isolated/Leaf/FileNavigationLeaf.cs +++ b/src/Elastic.Documentation.Navigation/Isolated/Leaf/FileNavigationLeaf.cs @@ -43,6 +43,7 @@ string DetermineUrl() // Remove extension while preserving the directory path var relativePath = relativeToContainer ? args.RelativePathToTableOfContents : args.RelativePathToDocumentationSet; relativePath = relativePath.OptionalWindowsReplace(); + relativePath = Path.ChangeExtension(relativePath, "md"); var path = relativePath.EndsWith(".md", StringComparison.OrdinalIgnoreCase) ? relativePath[..^3] // Remove last 3 characters (.md) : relativePath; diff --git a/src/Elastic.Documentation.Navigation/Isolated/Node/DocumentationSetNavigation.cs b/src/Elastic.Documentation.Navigation/Isolated/Node/DocumentationSetNavigation.cs index 8b4e0566c..d1c6e80dd 100644 --- a/src/Elastic.Documentation.Navigation/Isolated/Node/DocumentationSetNavigation.cs +++ b/src/Elastic.Documentation.Navigation/Isolated/Node/DocumentationSetNavigation.cs @@ -4,7 +4,8 @@ using System.Diagnostics; using System.IO.Abstractions; -using Elastic.Documentation.Configuration.DocSet; +using Elastic.Documentation.Configuration.Plugins.DetectionRules.TableOfContents; +using Elastic.Documentation.Configuration.Toc; using Elastic.Documentation.Extensions; using Elastic.Documentation.Links.CrossLinks; using Elastic.Documentation.Navigation.Isolated.Leaf; @@ -232,7 +233,11 @@ INavigationHomeAccessor homeAccessor var fullPath = fileRef.PathRelativeToDocumentationSet; // Create file info and documentation file - var fileInfo = ResolveFileInfo(context, fullPath); + var fileInfo = fileRef switch + { + RuleReference ruleRef => ruleRef.FileInfo, + _ => ResolveFileInfo(context, fullPath) + }; var documentationFile = CreateDocumentationFile(fileInfo, context.ReadFileSystem, context, fullPath); if (documentationFile == null) return null; diff --git a/src/Elastic.Markdown/Extensions/DetectionRules/DetectionRuleFile.cs b/src/Elastic.Markdown/Extensions/DetectionRules/DetectionRuleFile.cs index 3e5a4ffe2..f985682c4 100644 --- a/src/Elastic.Markdown/Extensions/DetectionRules/DetectionRuleFile.cs +++ b/src/Elastic.Markdown/Extensions/DetectionRules/DetectionRuleFile.cs @@ -5,7 +5,7 @@ using System.IO.Abstractions; using Elastic.Documentation.Configuration; using Elastic.Documentation.Configuration.Plugins.DetectionRules; -using Elastic.Documentation.Configuration.Plugins.DetectionRules.TableOfContents; +using Elastic.Documentation.Navigation; using Elastic.Markdown.IO; using Elastic.Markdown.Myst; using Markdig.Syntax; @@ -14,37 +14,52 @@ namespace Elastic.Markdown.Extensions.DetectionRules; public record DetectionRuleOverviewFile : MarkdownFile { + private string? _markdown; + public DetectionRuleOverviewFile(IFileInfo sourceFile, IDirectoryInfo rootPath, MarkdownParser parser, BuildContext build) : base(sourceFile, rootPath, parser, build) { } - public RuleReference[] Rules { get; set; } = []; - - private Dictionary Files { get; } = []; + private ILeafNavigationItem[] _ruleNavigations = []; - public void AddDetectionRuleFile(DetectionRuleFile df, RuleReference ruleReference) => Files[ruleReference.RelativePathRelativeToDocumentationSet] = df; + internal ILeafNavigationItem[] RuleNavigations + { + get => _ruleNavigations; + set + { + _markdown = null; + _ruleNavigations = value; + } + } protected override Task GetMinimalParseDocumentAsync(Cancel ctx) { Title = "Prebuilt detection rules reference"; - var markdown = GetMarkdown(); + var markdown = GetMarkdownCached(); var document = MarkdownParser.MinimalParseStringAsync(markdown, SourceFile, null); return Task.FromResult(document); } protected override Task GetParseDocumentAsync(Cancel ctx) { - var markdown = GetMarkdown(); + var markdown = GetMarkdownCached(); var document = MarkdownParser.ParseStringAsync(markdown, SourceFile, null); return Task.FromResult(document); } + private string GetMarkdownCached() + { + _markdown ??= GetMarkdown(); + return _markdown; + } + private string GetMarkdown() { + var rules = RuleNavigations.Select(navigation => (Navigation: navigation, Model: (DetectionRuleFile)navigation.Model)).ToList(); var groupedRules = - Rules - .GroupBy(r => r.Rule.Domain ?? "Unspecified") + rules + .GroupBy(r => r.Model.Rule.Domain ?? "Unspecified") .OrderBy(g => g.Key) .ToArray(); // language=markdown @@ -66,13 +81,12 @@ private string GetMarkdown() ## {group.Key} """; - foreach (var r in group.OrderBy(r => r.Rule.Name)) + foreach (var (navigation, model) in group.OrderBy(r => r.Model.Rule.Name)) { // TODO update this to use the new URL from navigation - var url = "does-not-exist-yet"; markdown += $""" -[{r.Rule.Name}](!{url})
+[{model.Rule.Name}](!{navigation.Url})
"""; } @@ -87,7 +101,7 @@ private string GetMarkdown() public record DetectionRuleFile : MarkdownFile { - public DetectionRule? Rule { get; set; } + public DetectionRule Rule { get; } public override string LinkReferenceRelativePath { get; } @@ -102,6 +116,7 @@ BuildContext build { RuleSourceMarkdownPath = SourcePath(sourceFile, build); LinkReferenceRelativePath = Path.GetRelativePath(build.DocumentationSourceDirectory.FullName, RuleSourceMarkdownPath.FullName); + Rule = DetectionRule.From(sourceFile); } private static IFileInfo SourcePath(IFileInfo rulePath, BuildContext build) @@ -121,11 +136,9 @@ public static IFileInfo OutputPath(IFileInfo rulePath, BuildContext build) return rulePath.FileSystem.FileInfo.New(newPath); } - //protected override string RelativePathUrl => RelativePath.AsSpan().TrimStart("../").ToString(); - protected override Task GetMinimalParseDocumentAsync(Cancel ctx) { - Title = Rule?.Name; + Title = Rule.Name; var markdown = GetMarkdown(); var document = MarkdownParser.MinimalParseStringAsync(markdown, RuleSourceMarkdownPath, null); return Task.FromResult(document); @@ -140,8 +153,6 @@ protected override Task GetParseDocumentAsync(Cancel ctx) private string GetMarkdown() { - if (Rule is null) - return $"# {Title}"; // language=markdown var markdown = $""" diff --git a/src/Elastic.Markdown/Extensions/DetectionRules/DetectionRulesDocsBuilderExtension.cs b/src/Elastic.Markdown/Extensions/DetectionRules/DetectionRulesDocsBuilderExtension.cs index 3715fd595..68b8ba164 100644 --- a/src/Elastic.Markdown/Extensions/DetectionRules/DetectionRulesDocsBuilderExtension.cs +++ b/src/Elastic.Markdown/Extensions/DetectionRules/DetectionRulesDocsBuilderExtension.cs @@ -4,8 +4,10 @@ using System.IO.Abstractions; using Elastic.Documentation.Configuration; -using Elastic.Documentation.Configuration.DocSet; using Elastic.Documentation.Configuration.Plugins.DetectionRules.TableOfContents; +using Elastic.Documentation.Configuration.Toc; +using Elastic.Documentation.Navigation; +using Elastic.Documentation.Navigation.Isolated.Node; using Elastic.Markdown.Exporters; using Elastic.Markdown.IO; using Elastic.Markdown.IO.NewNavigation; @@ -19,39 +21,27 @@ public class DetectionRulesDocsBuilderExtension(BuildContext build) : IDocsBuild public IDocumentationFileExporter? FileExporter { get; } = new RuleDocumentationFileExporter(build.ReadFileSystem, build.WriteFileSystem); - private DetectionRuleOverviewFile? _overviewFile; - public void Visit(DocumentationFile file, ITableOfContentsItem tocItem) - { - // TODO the parsing of rules should not happen at ITocItem reading time. - // ensure the file has an instance of the rule the reference parsed. - if (file is DetectionRuleFile df && tocItem is RuleReference r) - { - df.Rule = r.Rule; - _overviewFile?.AddDetectionRuleFile(df, r); - - } + public DocumentationFile? CreateDocumentationFile(IFileInfo file, MarkdownParser markdownParser) => + file.Extension != ".toml" ? null : new DetectionRuleFile(file, Build.DocumentationSourceDirectory, markdownParser, Build); - if (file is DetectionRuleOverviewFile of && tocItem is RuleOverviewReference or) - { - var rules = or.Children.OfType().ToArray(); - of.Rules = rules; - _overviewFile = of; - } - } + public MarkdownFile? CreateMarkdownFile(IFileInfo file, IDirectoryInfo sourceDirectory, MarkdownParser markdownParser) => + file.Name != "index.md" ? null : new DetectionRuleOverviewFile(file, sourceDirectory, markdownParser, Build); - public DocumentationFile? CreateDocumentationFile(IFileInfo file, MarkdownParser markdownParser) + /// + public void VisitNavigation(INavigationItem navigation, IDocumentationFile model) { - if (file.Extension != ".toml") - return null; - - return new DetectionRuleFile(file, Build.DocumentationSourceDirectory, markdownParser, Build); + if (model is not DetectionRuleOverviewFile overview) + return; + if (navigation is not VirtualFileNavigation node) + return; + var detectionRuleNavigations = node.NavigationItems + .OfType>() + .Where(n => n.Model is DetectionRuleFile) + .ToArray(); + + overview.RuleNavigations = detectionRuleNavigations; } - public MarkdownFile? CreateMarkdownFile(IFileInfo file, IDirectoryInfo sourceDirectory, MarkdownParser markdownParser) => - file.Name == "index.md" - ? new DetectionRuleOverviewFile(file, sourceDirectory, markdownParser, Build) - : null; - public bool TryGetDocumentationFileBySlug(DocumentationSet documentationSet, string slug, out DocumentationFile? documentationFile) { var tomlFile = $"../{slug}.toml"; @@ -59,22 +49,13 @@ public bool TryGetDocumentationFileBySlug(DocumentationSet documentationSet, str return documentationSet.Files.TryGetValue(filePath, out documentationFile); } - public IReadOnlyCollection<(IFileInfo, DocumentationFile)> ScanDocumentationFiles( - Func defaultFileHandling - ) + public IReadOnlyCollection<(IFileInfo, DocumentationFile)> ScanDocumentationFiles(Func defaultFileHandling) { var rules = Build.ConfigurationYaml.TableOfContents.OfType().First().Children.OfType().ToArray(); if (rules.Length == 0) return []; - var sourcePath = Path.GetFullPath(Path.Combine(Build.DocumentationSourceDirectory.FullName, rules[0].SourceDirectory)); - var sourceDirectory = Build.ReadFileSystem.DirectoryInfo.New(sourcePath); - return rules.Select(r => - { - var file = Build.ReadFileSystem.FileInfo.New(Path.Combine(sourceDirectory.FullName, r.RelativePathRelativeToDocumentationSet)); - return (file, defaultFileHandling(file, sourceDirectory)); - - }).ToArray(); + return rules.Select(r => (r.FileInfo, defaultFileHandling(r.FileInfo, r.FileInfo.Directory!))).ToArray(); } } diff --git a/src/Elastic.Markdown/Extensions/IDocsBuilderExtension.cs b/src/Elastic.Markdown/Extensions/IDocsBuilderExtension.cs index c3af09346..1dcba78d6 100644 --- a/src/Elastic.Markdown/Extensions/IDocsBuilderExtension.cs +++ b/src/Elastic.Markdown/Extensions/IDocsBuilderExtension.cs @@ -3,7 +3,8 @@ // See the LICENSE file in the project root for more information using System.IO.Abstractions; -using Elastic.Documentation.Configuration.DocSet; +using Elastic.Documentation.Configuration.Toc; +using Elastic.Documentation.Navigation; using Elastic.Markdown.Exporters; using Elastic.Markdown.IO; using Elastic.Markdown.Myst; @@ -14,9 +15,6 @@ public interface IDocsBuilderExtension { IDocumentationFileExporter? FileExporter { get; } - /// Visit the and its equivalent - void Visit(DocumentationFile file, ITableOfContentsItem tocItem); - /// Create an instance of if it matches the . /// Return `null` to let another extension handle this. DocumentationFile? CreateDocumentationFile(IFileInfo file, MarkdownParser markdownParser); @@ -28,4 +26,6 @@ public interface IDocsBuilderExtension IReadOnlyCollection<(IFileInfo, DocumentationFile)> ScanDocumentationFiles(Func defaultFileHandling); MarkdownFile? CreateMarkdownFile(IFileInfo file, IDirectoryInfo sourceDirectory, MarkdownParser markdownParser); + + void VisitNavigation(INavigationItem navigation, IDocumentationFile model); } diff --git a/src/Elastic.Markdown/IO/DocumentationSet.cs b/src/Elastic.Markdown/IO/DocumentationSet.cs index 17c2400a2..32033598d 100644 --- a/src/Elastic.Markdown/IO/DocumentationSet.cs +++ b/src/Elastic.Markdown/IO/DocumentationSet.cs @@ -76,6 +76,7 @@ ICrossLinkResolver linkResolver var fileFactory = new MarkdownFileFactory(context, MarkdownParser, EnabledExtensions); Navigation = new DocumentationSetNavigation(context.ConfigurationYaml, context, fileFactory, null, null, context.UrlPathPrefix, CrossLinkResolver); + VisitNavigation(Navigation); Name = Context.Git != GitCheckoutInformation.Unavailable ? Context.Git.RepositoryName @@ -120,6 +121,23 @@ ICrossLinkResolver linkResolver public FrozenDictionary NavigationIndexedByOrder { get; } + private void VisitNavigation(INavigationItem item) + { + switch (item) + { + case ILeafNavigationItem markdownLeaf: + foreach (var extension in EnabledExtensions) + extension.VisitNavigation(item, markdownLeaf.Model); + break; + case INodeNavigationItem node: + foreach (var extension in EnabledExtensions) + extension.VisitNavigation(node, node.Index.Model); + foreach (var child in node.NavigationItems) + VisitNavigation(child); + break; + } + } + private IReadOnlyCollection CreateNavigationLookup(INavigationItem item) { switch (item) @@ -222,12 +240,6 @@ public INavigationItem FindNavigationByMarkdown(MarkdownFile markdown) return navigation; throw new Exception($"Could not find navigation item for {markdown.CrossLink}"); } - public INavigationItem FindNavigationByCrossLink(string crossLink) - { - if (NavigationIndexedByCrossLink.TryGetValue(crossLink, out var navigation)) - return navigation; - throw new Exception($"Could not find navigation item for {crossLink}"); - } private bool _resolved; public async Task ResolveDirectoryTree(Cancel ctx) diff --git a/src/services/Elastic.Documentation.Assembler/AssembleSources.cs b/src/services/Elastic.Documentation.Assembler/AssembleSources.cs index 0a87b4252..9c36b06c6 100644 --- a/src/services/Elastic.Documentation.Assembler/AssembleSources.cs +++ b/src/services/Elastic.Documentation.Assembler/AssembleSources.cs @@ -10,8 +10,8 @@ using Elastic.Documentation.Configuration; using Elastic.Documentation.Configuration.Assembler; using Elastic.Documentation.Configuration.Builder; -using Elastic.Documentation.Configuration.DocSet; using Elastic.Documentation.Configuration.LegacyUrlMappings; +using Elastic.Documentation.Configuration.Toc; using Elastic.Documentation.LinkIndex; using Elastic.Documentation.Links.CrossLinks; using Microsoft.Extensions.Logging; diff --git a/src/services/Elastic.Documentation.Assembler/Building/AssemblerBuildService.cs b/src/services/Elastic.Documentation.Assembler/Building/AssemblerBuildService.cs index 6c013214f..129251f30 100644 --- a/src/services/Elastic.Documentation.Assembler/Building/AssemblerBuildService.cs +++ b/src/services/Elastic.Documentation.Assembler/Building/AssemblerBuildService.cs @@ -9,7 +9,7 @@ using Elastic.Documentation.Assembler.Sourcing; using Elastic.Documentation.Configuration; using Elastic.Documentation.Configuration.Assembler; -using Elastic.Documentation.Configuration.DocSet; +using Elastic.Documentation.Configuration.Toc; using Elastic.Documentation.Diagnostics; using Elastic.Documentation.LegacyDocs; using Elastic.Documentation.Navigation.Assembler; diff --git a/src/services/Elastic.Documentation.Assembler/Links/PublishEnvironmentUriResolver.cs b/src/services/Elastic.Documentation.Assembler/Links/PublishEnvironmentUriResolver.cs index ec7e61940..bcbf31902 100644 --- a/src/services/Elastic.Documentation.Assembler/Links/PublishEnvironmentUriResolver.cs +++ b/src/services/Elastic.Documentation.Assembler/Links/PublishEnvironmentUriResolver.cs @@ -4,7 +4,7 @@ using System.Collections.Frozen; using Elastic.Documentation.Configuration.Assembler; -using Elastic.Documentation.Configuration.DocSet; +using Elastic.Documentation.Configuration.Toc; using Elastic.Documentation.Links.CrossLinks; namespace Elastic.Documentation.Assembler.Links; diff --git a/src/services/Elastic.Documentation.Assembler/Navigation/GlobalNavigationService.cs b/src/services/Elastic.Documentation.Assembler/Navigation/GlobalNavigationService.cs index 53cb34052..6891281f7 100644 --- a/src/services/Elastic.Documentation.Assembler/Navigation/GlobalNavigationService.cs +++ b/src/services/Elastic.Documentation.Assembler/Navigation/GlobalNavigationService.cs @@ -5,7 +5,7 @@ using System.IO.Abstractions; using Elastic.Documentation.Configuration; using Elastic.Documentation.Configuration.Assembler; -using Elastic.Documentation.Configuration.DocSet; +using Elastic.Documentation.Configuration.Toc; using Elastic.Documentation.Diagnostics; using Elastic.Documentation.Services; using Microsoft.Extensions.Logging; diff --git a/src/services/Elastic.Documentation.Assembler/Navigation/NavigationPrefixChecker.cs b/src/services/Elastic.Documentation.Assembler/Navigation/NavigationPrefixChecker.cs index dbabbb7ba..e8ce20ce1 100644 --- a/src/services/Elastic.Documentation.Assembler/Navigation/NavigationPrefixChecker.cs +++ b/src/services/Elastic.Documentation.Assembler/Navigation/NavigationPrefixChecker.cs @@ -5,7 +5,7 @@ using System.Collections.Immutable; using Elastic.Documentation.Assembler.Links; using Elastic.Documentation.Configuration; -using Elastic.Documentation.Configuration.DocSet; +using Elastic.Documentation.Configuration.Toc; using Elastic.Documentation.Diagnostics; using Elastic.Documentation.LinkIndex; using Elastic.Documentation.Links; diff --git a/tests-integration/Elastic.Assembler.IntegrationTests/NavigationBuildingTests.cs b/tests-integration/Elastic.Assembler.IntegrationTests/NavigationBuildingTests.cs index 4b40ebda6..1650b873f 100644 --- a/tests-integration/Elastic.Assembler.IntegrationTests/NavigationBuildingTests.cs +++ b/tests-integration/Elastic.Assembler.IntegrationTests/NavigationBuildingTests.cs @@ -10,7 +10,7 @@ using Elastic.Documentation.Assembler.Sourcing; using Elastic.Documentation.Configuration; using Elastic.Documentation.Configuration.Assembler; -using Elastic.Documentation.Configuration.DocSet; +using Elastic.Documentation.Configuration.Toc; using Elastic.Documentation.Navigation; using Elastic.Documentation.Navigation.Assembler; using Elastic.Documentation.Navigation.Isolated; diff --git a/tests-integration/Elastic.Assembler.IntegrationTests/SiteNavigationTests.cs b/tests-integration/Elastic.Assembler.IntegrationTests/SiteNavigationTests.cs index c42441c2a..90e265595 100644 --- a/tests-integration/Elastic.Assembler.IntegrationTests/SiteNavigationTests.cs +++ b/tests-integration/Elastic.Assembler.IntegrationTests/SiteNavigationTests.cs @@ -8,7 +8,7 @@ using Elastic.Documentation.Assembler.Sourcing; using Elastic.Documentation.Configuration; using Elastic.Documentation.Configuration.Assembler; -using Elastic.Documentation.Configuration.DocSet; +using Elastic.Documentation.Configuration.Toc; using Elastic.Documentation.Diagnostics; using Elastic.Documentation.Navigation.Assembler; using Elastic.Markdown.IO; diff --git a/tests/Elastic.Documentation.Configuration.Tests/DocumentationSetFileTests.cs b/tests/Elastic.Documentation.Configuration.Tests/DocumentationSetFileTests.cs index 362a7299f..67c98fbc6 100644 --- a/tests/Elastic.Documentation.Configuration.Tests/DocumentationSetFileTests.cs +++ b/tests/Elastic.Documentation.Configuration.Tests/DocumentationSetFileTests.cs @@ -4,7 +4,7 @@ using System.IO.Abstractions.TestingHelpers; using System.Runtime.InteropServices; -using Elastic.Documentation.Configuration.DocSet; +using Elastic.Documentation.Configuration.Toc; using Elastic.Documentation.Diagnostics; using Elastic.Documentation.Extensions; using FluentAssertions; diff --git a/tests/Elastic.Documentation.Configuration.Tests/PhysicalDocsetTests.cs b/tests/Elastic.Documentation.Configuration.Tests/PhysicalDocsetTests.cs index 9ec5b5b4e..2ef1ad402 100644 --- a/tests/Elastic.Documentation.Configuration.Tests/PhysicalDocsetTests.cs +++ b/tests/Elastic.Documentation.Configuration.Tests/PhysicalDocsetTests.cs @@ -2,7 +2,7 @@ // Elasticsearch B.V licenses this file to you under the Apache 2.0 License. // See the LICENSE file in the project root for more information -using Elastic.Documentation.Configuration.DocSet; +using Elastic.Documentation.Configuration.Toc; using FluentAssertions; namespace Elastic.Documentation.Configuration.Tests; diff --git a/tests/Elastic.Documentation.Configuration.Tests/SiteNavigationFileTests.cs b/tests/Elastic.Documentation.Configuration.Tests/SiteNavigationFileTests.cs index 6f241e224..9d2b0f563 100644 --- a/tests/Elastic.Documentation.Configuration.Tests/SiteNavigationFileTests.cs +++ b/tests/Elastic.Documentation.Configuration.Tests/SiteNavigationFileTests.cs @@ -2,7 +2,7 @@ // Elasticsearch B.V licenses this file to you under the Apache 2.0 License. // See the LICENSE file in the project root for more information -using Elastic.Documentation.Configuration.DocSet; +using Elastic.Documentation.Configuration.Toc; using FluentAssertions; namespace Elastic.Documentation.Configuration.Tests; diff --git a/tests/Navigation.Tests/Assembler/ComplexSiteNavigationTests.cs b/tests/Navigation.Tests/Assembler/ComplexSiteNavigationTests.cs index f30b9b499..24a71445d 100644 --- a/tests/Navigation.Tests/Assembler/ComplexSiteNavigationTests.cs +++ b/tests/Navigation.Tests/Assembler/ComplexSiteNavigationTests.cs @@ -2,7 +2,7 @@ // Elasticsearch B.V licenses this file to you under the Apache 2.0 License. // See the LICENSE file in the project root for more information -using Elastic.Documentation.Configuration.DocSet; +using Elastic.Documentation.Configuration.Toc; using Elastic.Documentation.Navigation.Assembler; using Elastic.Documentation.Navigation.Isolated; using Elastic.Documentation.Navigation.Isolated.Leaf; diff --git a/tests/Navigation.Tests/Assembler/IdentifierCollectionTests.cs b/tests/Navigation.Tests/Assembler/IdentifierCollectionTests.cs index 8303b642b..d22239a3b 100644 --- a/tests/Navigation.Tests/Assembler/IdentifierCollectionTests.cs +++ b/tests/Navigation.Tests/Assembler/IdentifierCollectionTests.cs @@ -2,7 +2,7 @@ // Elasticsearch B.V licenses this file to you under the Apache 2.0 License. // See the LICENSE file in the project root for more information -using Elastic.Documentation.Configuration.DocSet; +using Elastic.Documentation.Configuration.Toc; using Elastic.Documentation.Navigation.Isolated; using Elastic.Documentation.Navigation.Isolated.Node; using FluentAssertions; diff --git a/tests/Navigation.Tests/Assembler/SiteDocumentationSetsTests.cs b/tests/Navigation.Tests/Assembler/SiteDocumentationSetsTests.cs index d007f694c..0bd92f014 100644 --- a/tests/Navigation.Tests/Assembler/SiteDocumentationSetsTests.cs +++ b/tests/Navigation.Tests/Assembler/SiteDocumentationSetsTests.cs @@ -2,7 +2,7 @@ // Elasticsearch B.V licenses this file to you under the Apache 2.0 License. // See the LICENSE file in the project root for more information -using Elastic.Documentation.Configuration.DocSet; +using Elastic.Documentation.Configuration.Toc; using Elastic.Documentation.Navigation.Assembler; using Elastic.Documentation.Navigation.Isolated; using Elastic.Documentation.Navigation.Isolated.Leaf; diff --git a/tests/Navigation.Tests/Assembler/SiteNavigationTests.cs b/tests/Navigation.Tests/Assembler/SiteNavigationTests.cs index 19c255b10..ca2fa2795 100644 --- a/tests/Navigation.Tests/Assembler/SiteNavigationTests.cs +++ b/tests/Navigation.Tests/Assembler/SiteNavigationTests.cs @@ -3,7 +3,7 @@ // See the LICENSE file in the project root for more information using System.IO.Abstractions.TestingHelpers; -using Elastic.Documentation.Configuration.DocSet; +using Elastic.Documentation.Configuration.Toc; using Elastic.Documentation.Navigation.Assembler; using Elastic.Documentation.Navigation.Isolated; using Elastic.Documentation.Navigation.Isolated.Node; diff --git a/tests/Navigation.Tests/Isolation/ConstructorTests.cs b/tests/Navigation.Tests/Isolation/ConstructorTests.cs index 88f483132..2ea2f7de5 100644 --- a/tests/Navigation.Tests/Isolation/ConstructorTests.cs +++ b/tests/Navigation.Tests/Isolation/ConstructorTests.cs @@ -3,7 +3,7 @@ // See the LICENSE file in the project root for more information using System.IO.Abstractions.TestingHelpers; -using Elastic.Documentation.Configuration.DocSet; +using Elastic.Documentation.Configuration.Toc; using Elastic.Documentation.Extensions; using Elastic.Documentation.Navigation.Isolated; using Elastic.Documentation.Navigation.Isolated.Leaf; diff --git a/tests/Navigation.Tests/Isolation/DynamicUrlTests.cs b/tests/Navigation.Tests/Isolation/DynamicUrlTests.cs index 92941cad5..1c3244d9e 100644 --- a/tests/Navigation.Tests/Isolation/DynamicUrlTests.cs +++ b/tests/Navigation.Tests/Isolation/DynamicUrlTests.cs @@ -3,7 +3,7 @@ // See the LICENSE file in the project root for more information using System.IO.Abstractions.TestingHelpers; -using Elastic.Documentation.Configuration.DocSet; +using Elastic.Documentation.Configuration.Toc; using Elastic.Documentation.Extensions; using Elastic.Documentation.Navigation.Isolated; using Elastic.Documentation.Navigation.Isolated.Leaf; diff --git a/tests/Navigation.Tests/Isolation/FileInfoValidationTests.cs b/tests/Navigation.Tests/Isolation/FileInfoValidationTests.cs index a0034bcbb..1ecff9743 100644 --- a/tests/Navigation.Tests/Isolation/FileInfoValidationTests.cs +++ b/tests/Navigation.Tests/Isolation/FileInfoValidationTests.cs @@ -3,7 +3,7 @@ // See the LICENSE file in the project root for more information using System.IO.Abstractions.TestingHelpers; -using Elastic.Documentation.Configuration.DocSet; +using Elastic.Documentation.Configuration.Toc; using Elastic.Documentation.Extensions; using Elastic.Documentation.Navigation.Isolated; using Elastic.Documentation.Navigation.Isolated.Leaf; diff --git a/tests/Navigation.Tests/Isolation/FileNavigationTests.cs b/tests/Navigation.Tests/Isolation/FileNavigationTests.cs index 43c06ed02..fb679d10d 100644 --- a/tests/Navigation.Tests/Isolation/FileNavigationTests.cs +++ b/tests/Navigation.Tests/Isolation/FileNavigationTests.cs @@ -3,7 +3,7 @@ // See the LICENSE file in the project root for more information using System.IO.Abstractions.TestingHelpers; -using Elastic.Documentation.Configuration.DocSet; +using Elastic.Documentation.Configuration.Toc; using Elastic.Documentation.Extensions; using Elastic.Documentation.Navigation.Isolated; using Elastic.Documentation.Navigation.Isolated.Leaf; diff --git a/tests/Navigation.Tests/Isolation/FolderIndexFileRefTests.cs b/tests/Navigation.Tests/Isolation/FolderIndexFileRefTests.cs index 43e31aa67..32cd99a0e 100644 --- a/tests/Navigation.Tests/Isolation/FolderIndexFileRefTests.cs +++ b/tests/Navigation.Tests/Isolation/FolderIndexFileRefTests.cs @@ -3,7 +3,7 @@ // See the LICENSE file in the project root for more information using System.IO.Abstractions.TestingHelpers; -using Elastic.Documentation.Configuration.DocSet; +using Elastic.Documentation.Configuration.Toc; using Elastic.Documentation.Diagnostics; using Elastic.Documentation.Extensions; using Elastic.Documentation.Navigation.Isolated; diff --git a/tests/Navigation.Tests/Isolation/NavigationStructureTests.cs b/tests/Navigation.Tests/Isolation/NavigationStructureTests.cs index 71d40b2ab..2d0c68488 100644 --- a/tests/Navigation.Tests/Isolation/NavigationStructureTests.cs +++ b/tests/Navigation.Tests/Isolation/NavigationStructureTests.cs @@ -3,7 +3,7 @@ // See the LICENSE file in the project root for more information using System.IO.Abstractions.TestingHelpers; -using Elastic.Documentation.Configuration.DocSet; +using Elastic.Documentation.Configuration.Toc; using Elastic.Documentation.Extensions; using Elastic.Documentation.Navigation.Isolated; using Elastic.Documentation.Navigation.Isolated.Leaf; diff --git a/tests/Navigation.Tests/Isolation/PhysicalDocsetTests.cs b/tests/Navigation.Tests/Isolation/PhysicalDocsetTests.cs index f3baa1452..44f09081d 100644 --- a/tests/Navigation.Tests/Isolation/PhysicalDocsetTests.cs +++ b/tests/Navigation.Tests/Isolation/PhysicalDocsetTests.cs @@ -4,7 +4,7 @@ using System.IO.Abstractions; using Elastic.Documentation.Configuration; -using Elastic.Documentation.Configuration.DocSet; +using Elastic.Documentation.Configuration.Toc; using Elastic.Documentation.Diagnostics; using Elastic.Documentation.Navigation.Isolated.Leaf; using Elastic.Documentation.Navigation.Isolated.Node; diff --git a/tests/Navigation.Tests/Isolation/ValidationTests.cs b/tests/Navigation.Tests/Isolation/ValidationTests.cs index 014e38149..a31b04e94 100644 --- a/tests/Navigation.Tests/Isolation/ValidationTests.cs +++ b/tests/Navigation.Tests/Isolation/ValidationTests.cs @@ -3,7 +3,7 @@ // See the LICENSE file in the project root for more information using System.IO.Abstractions.TestingHelpers; -using Elastic.Documentation.Configuration.DocSet; +using Elastic.Documentation.Configuration.Toc; using Elastic.Documentation.Diagnostics; using Elastic.Documentation.Extensions; using Elastic.Documentation.Navigation.Isolated; From 2b5132f2b480d0e066c7e9bf53b8feb65c9dbb28 Mon Sep 17 00:00:00 2001 From: Martijn Laarman Date: Fri, 7 Nov 2025 14:36:05 +0100 Subject: [PATCH 167/171] assembler build fixes for detection rule files --- .../Toc/DocumentationSetFile.cs | 3 +-- .../DetectionRules/DetectionRuleFile.cs | 24 +++---------------- 2 files changed, 4 insertions(+), 23 deletions(-) diff --git a/src/Elastic.Documentation.Configuration/Toc/DocumentationSetFile.cs b/src/Elastic.Documentation.Configuration/Toc/DocumentationSetFile.cs index e5a67053d..ef5529f7b 100644 --- a/src/Elastic.Documentation.Configuration/Toc/DocumentationSetFile.cs +++ b/src/Elastic.Documentation.Configuration/Toc/DocumentationSetFile.cs @@ -692,8 +692,7 @@ public class TocItemYamlConverter : IYamlTypeConverter } value = childrenList; } - - if (key.Value == "detection_rules") + else if (key.Value == "detection_rules") { // Parse the children list manually var childrenList = new List(); diff --git a/src/Elastic.Markdown/Extensions/DetectionRules/DetectionRuleFile.cs b/src/Elastic.Markdown/Extensions/DetectionRules/DetectionRuleFile.cs index f985682c4..f27f18f15 100644 --- a/src/Elastic.Markdown/Extensions/DetectionRules/DetectionRuleFile.cs +++ b/src/Elastic.Markdown/Extensions/DetectionRules/DetectionRuleFile.cs @@ -14,46 +14,28 @@ namespace Elastic.Markdown.Extensions.DetectionRules; public record DetectionRuleOverviewFile : MarkdownFile { - private string? _markdown; - public DetectionRuleOverviewFile(IFileInfo sourceFile, IDirectoryInfo rootPath, MarkdownParser parser, BuildContext build) : base(sourceFile, rootPath, parser, build) { } - private ILeafNavigationItem[] _ruleNavigations = []; - - internal ILeafNavigationItem[] RuleNavigations - { - get => _ruleNavigations; - set - { - _markdown = null; - _ruleNavigations = value; - } - } + internal ILeafNavigationItem[] RuleNavigations { get; set; } = []; protected override Task GetMinimalParseDocumentAsync(Cancel ctx) { Title = "Prebuilt detection rules reference"; - var markdown = GetMarkdownCached(); + var markdown = GetMarkdown(); var document = MarkdownParser.MinimalParseStringAsync(markdown, SourceFile, null); return Task.FromResult(document); } protected override Task GetParseDocumentAsync(Cancel ctx) { - var markdown = GetMarkdownCached(); + var markdown = GetMarkdown(); var document = MarkdownParser.ParseStringAsync(markdown, SourceFile, null); return Task.FromResult(document); } - private string GetMarkdownCached() - { - _markdown ??= GetMarkdown(); - return _markdown; - } - private string GetMarkdown() { var rules = RuleNavigations.Select(navigation => (Navigation: navigation, Model: (DetectionRuleFile)navigation.Model)).ToList(); From f6cc9c2a064b348f66a8bde36687252be953c08b Mon Sep 17 00:00:00 2001 From: Martijn Laarman Date: Fri, 7 Nov 2025 14:55:28 +0100 Subject: [PATCH 168/171] Clean up configuration folder --- .../TableOfContents/RuleReference.cs | 11 - .../DetectionRuleOverviewRef.cs} | 9 +- .../Toc/DetectionRules/DetectionRuleRef.cs | 10 + .../Toc/DocumentationSetFile.cs | 288 +----------------- .../Toc/SiteNavigationFile.cs | 12 +- .../Toc/TableOfContentsFile.cs | 35 +++ .../Toc/TableOfContentsItems.cs | 63 ++++ .../Toc/TableOfContentsYamlConverters.cs | 186 +++++++++++ .../Node/DocumentationSetNavigation.cs | 4 +- .../DetectionRules/DetectionRule.cs | 2 +- .../DetectionRules/DetectionRuleFile.cs | 1 - .../DetectionRulesDocsBuilderExtension.cs | 4 +- .../AssembleSources.cs | 2 - 13 files changed, 316 insertions(+), 311 deletions(-) delete mode 100644 src/Elastic.Documentation.Configuration/Plugins/DetectionRules/TableOfContents/RuleReference.cs rename src/Elastic.Documentation.Configuration/{Plugins/DetectionRules/TableOfContents/DetectionRulesReference.cs => Toc/DetectionRules/DetectionRuleOverviewRef.cs} (89%) create mode 100644 src/Elastic.Documentation.Configuration/Toc/DetectionRules/DetectionRuleRef.cs create mode 100644 src/Elastic.Documentation.Configuration/Toc/TableOfContentsFile.cs create mode 100644 src/Elastic.Documentation.Configuration/Toc/TableOfContentsItems.cs create mode 100644 src/Elastic.Documentation.Configuration/Toc/TableOfContentsYamlConverters.cs rename src/{Elastic.Documentation.Configuration/Plugins => Elastic.Markdown/Extensions}/DetectionRules/DetectionRule.cs (98%) diff --git a/src/Elastic.Documentation.Configuration/Plugins/DetectionRules/TableOfContents/RuleReference.cs b/src/Elastic.Documentation.Configuration/Plugins/DetectionRules/TableOfContents/RuleReference.cs deleted file mode 100644 index da5d5bd1e..000000000 --- a/src/Elastic.Documentation.Configuration/Plugins/DetectionRules/TableOfContents/RuleReference.cs +++ /dev/null @@ -1,11 +0,0 @@ -// Licensed to Elasticsearch B.V under one or more agreements. -// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. -// See the LICENSE file in the project root for more information - -using System.IO.Abstractions; -using Elastic.Documentation.Configuration.Toc; - -namespace Elastic.Documentation.Configuration.Plugins.DetectionRules.TableOfContents; - -public record RuleReference(IFileInfo FileInfo, string RelativePathRelativeToDocumentationSet, string Context) - : FileRef(RelativePathRelativeToDocumentationSet, RelativePathRelativeToDocumentationSet, true, [], Context); diff --git a/src/Elastic.Documentation.Configuration/Plugins/DetectionRules/TableOfContents/DetectionRulesReference.cs b/src/Elastic.Documentation.Configuration/Toc/DetectionRules/DetectionRuleOverviewRef.cs similarity index 89% rename from src/Elastic.Documentation.Configuration/Plugins/DetectionRules/TableOfContents/DetectionRulesReference.cs rename to src/Elastic.Documentation.Configuration/Toc/DetectionRules/DetectionRuleOverviewRef.cs index f64d4b2b3..f2fb61faf 100644 --- a/src/Elastic.Documentation.Configuration/Plugins/DetectionRules/TableOfContents/DetectionRulesReference.cs +++ b/src/Elastic.Documentation.Configuration/Toc/DetectionRules/DetectionRuleOverviewRef.cs @@ -3,15 +3,14 @@ // See the LICENSE file in the project root for more information using System.IO.Abstractions; -using Elastic.Documentation.Configuration.Toc; -namespace Elastic.Documentation.Configuration.Plugins.DetectionRules.TableOfContents; +namespace Elastic.Documentation.Configuration.Toc.DetectionRules; -public record RuleOverviewReference : FileRef +public record DetectionRuleOverviewRef : FileRef { public IReadOnlyCollection DetectionRuleFolders { get; } - public RuleOverviewReference( + public DetectionRuleOverviewRef( string pathRelativeToDocumentationSet, string pathRelativeToContainer, IReadOnlyCollection detectionRulesFolders, @@ -53,7 +52,7 @@ private static IReadOnlyCollection ReadDetectionRuleFolder // baseDirectory is 'docs' rules live relative to docs parent '/' var relativePath = Path.GetRelativePath(baseDirectory.Parent!.FullName, f.FullName); if (f.Extension == ".toml") - return new RuleReference(f, relativePath, context); + return new DetectionRuleRef(f, relativePath, context); return new FileRef(relativePath, relativePath, false, [], context); }) diff --git a/src/Elastic.Documentation.Configuration/Toc/DetectionRules/DetectionRuleRef.cs b/src/Elastic.Documentation.Configuration/Toc/DetectionRules/DetectionRuleRef.cs new file mode 100644 index 000000000..aef2c4a8e --- /dev/null +++ b/src/Elastic.Documentation.Configuration/Toc/DetectionRules/DetectionRuleRef.cs @@ -0,0 +1,10 @@ +// Licensed to Elasticsearch B.V under one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +using System.IO.Abstractions; + +namespace Elastic.Documentation.Configuration.Toc.DetectionRules; + +public record DetectionRuleRef(IFileInfo FileInfo, string PathRelativeToDocumentationSet, string Context) + : FileRef(PathRelativeToDocumentationSet, PathRelativeToDocumentationSet, true, [], Context); diff --git a/src/Elastic.Documentation.Configuration/Toc/DocumentationSetFile.cs b/src/Elastic.Documentation.Configuration/Toc/DocumentationSetFile.cs index ef5529f7b..ec9ab81cb 100644 --- a/src/Elastic.Documentation.Configuration/Toc/DocumentationSetFile.cs +++ b/src/Elastic.Documentation.Configuration/Toc/DocumentationSetFile.cs @@ -3,36 +3,14 @@ // See the LICENSE file in the project root for more information using System.IO.Abstractions; -using Elastic.Documentation.Configuration.Plugins.DetectionRules.TableOfContents; using Elastic.Documentation.Configuration.Products; +using Elastic.Documentation.Configuration.Toc.DetectionRules; using Elastic.Documentation.Diagnostics; using Elastic.Documentation.Extensions; -using YamlDotNet.Core; -using YamlDotNet.Core.Events; using YamlDotNet.Serialization; namespace Elastic.Documentation.Configuration.Toc; -[YamlSerializable] -public class TableOfContentsFile -{ - [YamlMember(Alias = "project")] - public string? Project { get; set; } - - [YamlMember(Alias = "toc")] - public TableOfContents TableOfContents { get; set; } = []; - - /// - /// Set of diagnostic hint types to suppress. Deserialized directly from YAML list of strings. - /// Valid values: "DeepLinkingVirtualFile", "FolderFileNameMismatch" - /// - [YamlMember(Alias = "suppress")] - public HashSet SuppressDiagnostics { get; set; } = []; - - public static TableOfContentsFile Deserialize(string json) => - ConfigurationFileProvider.Deserializer.Deserialize(json); -} - [YamlSerializable] public class DocumentationSetFile : TableOfContentsFile { @@ -132,7 +110,7 @@ private static TableOfContents ResolveTableOfContents( var resolvedItem = item switch { IsolatedTableOfContentsRef tocRef => ResolveIsolatedToc(collector, tocRef, baseDirectory, fileSystem, parentPath, containerPath, context, suppressDiagnostics), - RuleOverviewReference ruleOverviewReference => ResolveRuleOverviewReference(collector, ruleOverviewReference, baseDirectory, fileSystem, parentPath, containerPath, context, suppressDiagnostics), + DetectionRuleOverviewRef ruleOverviewReference => ResolveRuleOverviewReference(collector, ruleOverviewReference, baseDirectory, fileSystem, parentPath, containerPath, context, suppressDiagnostics), FileRef fileRef => ResolveFileRef(collector, fileRef, baseDirectory, fileSystem, parentPath, containerPath, context, suppressDiagnostics), FolderRef folderRef => ResolveFolderRef(collector, folderRef, baseDirectory, fileSystem, parentPath, containerPath, context, suppressDiagnostics), CrossLinkRef crossLink => ResolveCrossLinkRef(collector, crossLink, baseDirectory, fileSystem, parentPath, containerPath, context), @@ -373,7 +351,7 @@ private static ITableOfContentsItem ResolveFileRef(IDiagnosticsCollector collect /// If no children are defined, auto-discovers .md files in the folder directory. ///
private static ITableOfContentsItem ResolveRuleOverviewReference(IDiagnosticsCollector collector, - RuleOverviewReference ruleRef, + DetectionRuleOverviewRef detectionRuleRef, IDirectoryInfo baseDirectory, IFileSystem fileSystem, string parentPath, @@ -384,7 +362,7 @@ private static ITableOfContentsItem ResolveRuleOverviewReference(IDiagnosticsCol // Folder paths containing '/' are treated as relative to the context file's directory (full paths). // Simple folder names (no '/') are resolved relative to the parent path in the navigation hierarchy. string fullPath; - if (ruleRef.PathRelativeToDocumentationSet.Contains('/')) + if (detectionRuleRef.PathRelativeToDocumentationSet.Contains('/')) { // Path contains '/', treat as context-relative (full path from the context file's directory) var contextDir = fileSystem.Path.GetDirectoryName(context) ?? ""; @@ -393,13 +371,13 @@ private static ITableOfContentsItem ResolveRuleOverviewReference(IDiagnosticsCol contextRelativePath = ""; fullPath = string.IsNullOrEmpty(contextRelativePath) - ? ruleRef.PathRelativeToDocumentationSet - : $"{contextRelativePath}/{ruleRef.PathRelativeToDocumentationSet}"; + ? detectionRuleRef.PathRelativeToDocumentationSet + : $"{contextRelativePath}/{detectionRuleRef.PathRelativeToDocumentationSet}"; } else { // Simple name, resolve relative to parent path - fullPath = string.IsNullOrEmpty(parentPath) ? ruleRef.PathRelativeToDocumentationSet : $"{parentPath}/{ruleRef.PathRelativeToDocumentationSet}"; + fullPath = string.IsNullOrEmpty(parentPath) ? detectionRuleRef.PathRelativeToDocumentationSet : $"{parentPath}/{detectionRuleRef.PathRelativeToDocumentationSet}"; } // Calculate PathRelativeToContainer: the folder path relative to its container @@ -408,17 +386,17 @@ private static ITableOfContentsItem ResolveRuleOverviewReference(IDiagnosticsCol : fullPath.Substring(containerPath.Length + 1); // For children of folders, the container remains the same as the folder's container - var resolvedChildren = ResolveTableOfContents(collector, ruleRef.Children, baseDirectory, fileSystem, fullPath, containerPath, context, suppressDiagnostics); + var resolvedChildren = ResolveTableOfContents(collector, detectionRuleRef.Children, baseDirectory, fileSystem, fullPath, containerPath, context, suppressDiagnostics); var fileInfo = fileSystem.NewFileInfo(baseDirectory.FullName, fullPath); - var tocSourceFolders = ruleRef.DetectionRuleFolders + var tocSourceFolders = detectionRuleRef.DetectionRuleFolders .Select(f => fileSystem.NewDirInfo(fileInfo.Directory!.FullName, f)) .ToList(); - var tomlChildren = RuleOverviewReference.CreateTableOfContentItems(tocSourceFolders, context, baseDirectory); + var tomlChildren = DetectionRuleOverviewRef.CreateTableOfContentItems(tocSourceFolders, context, baseDirectory); var children = resolvedChildren.Concat(tomlChildren).ToList(); - return new RuleOverviewReference(fullPath, pathRelativeToContainer, ruleRef.DetectionRuleFolders, children, context); + return new DetectionRuleOverviewRef(fullPath, pathRelativeToContainer, detectionRuleRef.DetectionRuleFolders, children, context); } @@ -560,247 +538,3 @@ public class DocumentationSetFeatures [YamlMember(Alias = "disable-github-edit-link", ApplyNamingConventions = false)] public bool? DisableGithubEditLink { get; set; } } - -public class TableOfContents : List -{ - public TableOfContents() { } - - public TableOfContents(IEnumerable items) : base(items) { } -} - - -/// -/// Represents an item in a table of contents (file, folder, or TOC reference). -/// -public interface ITableOfContentsItem -{ - /// - /// The full path of this item relative to the documentation source directory. - /// For files: includes .md extension (e.g., "guides/getting-started.md") - /// For folders: the folder path (e.g., "guides/advanced") - /// For TOCs: the path to the toc.yml directory (e.g., "development" or "guides/advanced") - /// - string PathRelativeToDocumentationSet { get; } - - /// - /// The full path of this item relative to the container docset.yml or toc.yml file. - /// For files: includes .md extension (e.g., "guides/getting-started.md") - /// For folders: the folder path (e.g., "guides/advanced") - /// For TOCs: the path to the toc.yml directory (e.g., "development" or "guides/advanced") - /// - string PathRelativeToContainer { get; } - - /// - /// The path to the YAML file (docset.yml or toc.yml) that defined this item. - /// This provides context for where the item was declared in the configuration. - /// - string Context { get; } -} - -public record FileRef(string PathRelativeToDocumentationSet, string PathRelativeToContainer, bool Hidden, IReadOnlyCollection Children, string Context) - : ITableOfContentsItem; - -public record IndexFileRef(string PathRelativeToDocumentationSet, string PathRelativeToContainer, bool Hidden, IReadOnlyCollection Children, string Context) - : FileRef(PathRelativeToDocumentationSet, PathRelativeToContainer, Hidden, Children, Context); - -/// -/// Represents a file reference created from a folder+file combination in YAML (e.g., "folder: path/to/dir, file: index.md"). -/// Children of this file should resolve relative to the folder path, not the parent TOC path. -/// -public record FolderIndexFileRef(string PathRelativeToDocumentationSet, string PathRelativeToContainer, bool Hidden, IReadOnlyCollection Children, string Context) - : IndexFileRef(PathRelativeToDocumentationSet, PathRelativeToContainer, Hidden, Children, Context); - -public record CrossLinkRef(Uri CrossLinkUri, string? Title, bool Hidden, IReadOnlyCollection Children, string Context) - : ITableOfContentsItem -{ - //TODO ensure we pass these to cross-links to - // CrossLinks don't have a file system path, so we use the CrossLinkUri as the Path - public string PathRelativeToDocumentationSet => CrossLinkUri.ToString(); - - // CrossLinks don't have a file system path, so we use the CrossLinkUri as the Path - public string PathRelativeToContainer => CrossLinkUri.ToString(); - -} - -public record FolderRef(string PathRelativeToDocumentationSet, string PathRelativeToContainer, IReadOnlyCollection Children, string Context) - : ITableOfContentsItem; - -public record IsolatedTableOfContentsRef(string PathRelativeToDocumentationSet, string PathRelativeToContainer, IReadOnlyCollection Children, string Context) - : ITableOfContentsItem; - - -public class TocItemCollectionYamlConverter : IYamlTypeConverter -{ - public bool Accepts(Type type) => type == typeof(TableOfContents); - - public object ReadYaml(IParser parser, Type type, ObjectDeserializer rootDeserializer) - { - var collection = new TableOfContents(); - - if (!parser.TryConsume(out _)) - return collection; - - while (!parser.TryConsume(out _)) - { - var item = rootDeserializer(typeof(ITableOfContentsItem)); - if (item is ITableOfContentsItem tocItem) - collection.Add(tocItem); - } - - return collection; - } - - public void WriteYaml(IEmitter emitter, object? value, Type type, ObjectSerializer serializer) => - serializer.Invoke(value, type); -} - -public class TocItemYamlConverter : IYamlTypeConverter -{ - public bool Accepts(Type type) => type == typeof(ITableOfContentsItem); - - public object? ReadYaml(IParser parser, Type type, ObjectDeserializer rootDeserializer) - { - if (!parser.TryConsume(out _)) - return null; - - var dictionary = new Dictionary(); - - while (!parser.TryConsume(out _)) - { - var key = parser.Consume(); - - // Parse the value based on what type it is - object? value = null; - if (parser.Accept(out var scalarValue)) - { - value = scalarValue.Value; - _ = parser.MoveNext(); - } - else if (parser.Accept(out _)) - { - // This is a list - parse it manually for "children" - if (key.Value == "children") - { - // Parse the children list manually - var childrenList = new List(); - _ = parser.Consume(); - while (!parser.TryConsume(out _)) - { - var child = rootDeserializer(typeof(ITableOfContentsItem)); - if (child is ITableOfContentsItem tocItem) - childrenList.Add(tocItem); - } - value = childrenList; - } - else if (key.Value == "detection_rules") - { - // Parse the children list manually - var childrenList = new List(); - _ = parser.Consume(); - while (!parser.TryConsume(out _)) - { - if (parser.Accept(out scalarValue)) - childrenList.Add(scalarValue.Value); - _ = parser.MoveNext(); - } - value = childrenList.ToArray(); - } - else - { - // For other lists, just skip them - parser.SkipThisAndNestedEvents(); - } - } - else if (parser.Accept(out _)) - { - // This is a nested mapping - skip it - parser.SkipThisAndNestedEvents(); - } - - dictionary[key.Value] = value; - } - - var children = GetChildren(dictionary); - - // Context will be set during LoadAndResolve, use empty string as placeholder during deserialization - const string placeholderContext = ""; - - // Check for folder+file combination (e.g., folder: getting-started, file: getting-started.md) - // This represents a folder with a specific index file - // The file becomes a child of the folder (as FolderIndexFileRef), and user-specified children follow - if (dictionary.TryGetValue("folder", out var folderPath) && folderPath is string folder && - dictionary.TryGetValue("file", out var filePath) && filePath is string file) - { - // Create the index file reference (FolderIndexFileRef to mark it as the folder's index) - // Store ONLY the file name - the folder path will be prepended during resolution - // This allows validation to check if the file itself has deep paths - // PathRelativeToContainer will be set during resolution - var indexFile = new FolderIndexFileRef(file, file, false, [], placeholderContext); - - // Create a list with the index file first, followed by user-specified children - var folderChildren = new List { indexFile }; - folderChildren.AddRange(children); - - // Return a FolderRef with the index file and children - // The folder path can be deep (e.g., "guides/getting-started"), that's OK - // PathRelativeToContainer will be set during resolution - return new FolderRef(folder, folder, folderChildren, placeholderContext); - } - if (dictionary.TryGetValue("detection_rules", out var detectionRulesObj) && detectionRulesObj is string[] detectionRulesFolders && - dictionary.TryGetValue("file", out var detectionRulesFilePath) && detectionRulesFilePath is string detectionRulesFile) - { - // Create the index file reference (FolderIndexFileRef to mark it as the folder's index) - // Store ONLY the file name - the folder path will be prepended during resolution - // This allows validation to check if the file itself has deep paths - // PathRelativeToContainer will be set during resolution - return new RuleOverviewReference(detectionRulesFile, detectionRulesFile, detectionRulesFolders, children, placeholderContext); - } - - // Check for file reference (file: or hidden:) - // PathRelativeToContainer will be set during resolution - if (dictionary.TryGetValue("file", out var filePathOnly) && filePathOnly is string fileOnly) - { - return fileOnly == "index.md" - ? new IndexFileRef(fileOnly, fileOnly, false, children, placeholderContext) - : new FileRef(fileOnly, fileOnly, false, children, placeholderContext); - } - - if (dictionary.TryGetValue("hidden", out var hiddenPath) && hiddenPath is string p) - return p == "index.md" ? new IndexFileRef(p, p, true, children, placeholderContext) : new FileRef(p, p, true, children, placeholderContext); - - // Check for crosslink reference - if (dictionary.TryGetValue("crosslink", out var crosslink) && crosslink is string crosslinkStr) - { - var title = dictionary.TryGetValue("title", out var t) && t is string titleStr ? titleStr : null; - var isHidden = dictionary.TryGetValue("hidden", out var h) && h is bool hiddenBool && hiddenBool; - return new CrossLinkRef(new Uri(crosslinkStr), title, isHidden, children, placeholderContext); - } - - // Check for folder reference - // PathRelativeToContainer will be set during resolution - if (dictionary.TryGetValue("folder", out var folderPathOnly) && folderPathOnly is string folderOnly) - return new FolderRef(folderOnly, folderOnly, children, placeholderContext); - - // Check for toc reference - // PathRelativeToContainer will be set during resolution - if (dictionary.TryGetValue("toc", out var tocPath) && tocPath is string source) - return new IsolatedTableOfContentsRef(source, source, children, placeholderContext); - - return null; - } - - private IReadOnlyCollection GetChildren(Dictionary dictionary) - { - if (!dictionary.TryGetValue("children", out var childrenObj)) - return []; - - // Children have already been deserialized as List - if (childrenObj is List tocItems) - return tocItems; - - return []; - } - - public void WriteYaml(IEmitter emitter, object? value, Type type, ObjectSerializer serializer) => - serializer.Invoke(value, type); -} diff --git a/src/Elastic.Documentation.Configuration/Toc/SiteNavigationFile.cs b/src/Elastic.Documentation.Configuration/Toc/SiteNavigationFile.cs index a5df87e4f..409ac897b 100644 --- a/src/Elastic.Documentation.Configuration/Toc/SiteNavigationFile.cs +++ b/src/Elastic.Documentation.Configuration/Toc/SiteNavigationFile.cs @@ -16,8 +16,6 @@ public record NavigationTocMapping { public required Uri Source { get; init; } public required string SourcePathPrefix { get; init; } - public required Uri TopLevelSource { get; init; } - public required Uri ParentSource { get; init; } } [YamlSerializable] @@ -69,8 +67,7 @@ private static void CollectSource(SiteTableOfContentsRef tocRef, HashSet se CollectSource(child, set); } - - public static ImmutableHashSet GetAllPathPrefixes(SiteNavigationFile siteNavigation) + private static ImmutableHashSet GetAllPathPrefixes(SiteNavigationFile siteNavigation) { var set = new HashSet(); @@ -117,12 +114,7 @@ public class PhantomRegistration public string Source { get; set; } = null!; } -public class SiteTableOfContents : List -{ - public SiteTableOfContents() { } - - public SiteTableOfContents(IEnumerable items) : base(items) { } -} +public class SiteTableOfContents : List; public record SiteTableOfContentsRef(Uri Source, string PathPrefix, IReadOnlyCollection Children) : ITableOfContentsItem diff --git a/src/Elastic.Documentation.Configuration/Toc/TableOfContentsFile.cs b/src/Elastic.Documentation.Configuration/Toc/TableOfContentsFile.cs new file mode 100644 index 000000000..faa7f5865 --- /dev/null +++ b/src/Elastic.Documentation.Configuration/Toc/TableOfContentsFile.cs @@ -0,0 +1,35 @@ +// Licensed to Elasticsearch B.V under one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +using Elastic.Documentation.Diagnostics; +using YamlDotNet.Serialization; + +namespace Elastic.Documentation.Configuration.Toc; + +[YamlSerializable] +public class TableOfContentsFile +{ + [YamlMember(Alias = "project")] + public string? Project { get; set; } + + [YamlMember(Alias = "toc")] + public TableOfContents TableOfContents { get; set; } = []; + + /// + /// Set of diagnostic hint types to suppress. Deserialized directly from YAML list of strings. + /// Valid values: "DeepLinkingVirtualFile", "FolderFileNameMismatch" + /// + [YamlMember(Alias = "suppress")] + public HashSet SuppressDiagnostics { get; set; } = []; + + public static TableOfContentsFile Deserialize(string json) => + ConfigurationFileProvider.Deserializer.Deserialize(json); +} + +public class TableOfContents : List +{ + public TableOfContents() { } + + public TableOfContents(IEnumerable items) : base(items) { } +} diff --git a/src/Elastic.Documentation.Configuration/Toc/TableOfContentsItems.cs b/src/Elastic.Documentation.Configuration/Toc/TableOfContentsItems.cs new file mode 100644 index 000000000..d339d6034 --- /dev/null +++ b/src/Elastic.Documentation.Configuration/Toc/TableOfContentsItems.cs @@ -0,0 +1,63 @@ +// Licensed to Elasticsearch B.V under one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +namespace Elastic.Documentation.Configuration.Toc; + +/// +/// Represents an item in a table of contents (file, folder, or TOC reference). +/// +public interface ITableOfContentsItem +{ + /// + /// The full path of this item relative to the documentation source directory. + /// For files: includes .md extension (e.g., "guides/getting-started.md") + /// For folders: the folder path (e.g., "guides/advanced") + /// For TOCs: the path to the toc.yml directory (e.g., "development" or "guides/advanced") + /// + string PathRelativeToDocumentationSet { get; } + + /// + /// The full path of this item relative to the container docset.yml or toc.yml file. + /// For files: includes .md extension (e.g., "guides/getting-started.md") + /// For folders: the folder path (e.g., "guides/advanced") + /// For TOCs: the path to the toc.yml directory (e.g., "development" or "guides/advanced") + /// + string PathRelativeToContainer { get; } + + /// + /// The path to the YAML file (docset.yml or toc.yml) that defined this item. + /// This provides context for where the item was declared in the configuration. + /// + string Context { get; } +} + +public record FileRef(string PathRelativeToDocumentationSet, string PathRelativeToContainer, bool Hidden, IReadOnlyCollection Children, string Context) + : ITableOfContentsItem; + +public record IndexFileRef(string PathRelativeToDocumentationSet, string PathRelativeToContainer, bool Hidden, IReadOnlyCollection Children, string Context) + : FileRef(PathRelativeToDocumentationSet, PathRelativeToContainer, Hidden, Children, Context); + +/// +/// Represents a file reference created from a folder+file combination in YAML (e.g., "folder: path/to/dir, file: index.md"). +/// Children of this file should resolve relative to the folder path, not the parent TOC path. +/// +public record FolderIndexFileRef(string PathRelativeToDocumentationSet, string PathRelativeToContainer, bool Hidden, IReadOnlyCollection Children, string Context) + : IndexFileRef(PathRelativeToDocumentationSet, PathRelativeToContainer, Hidden, Children, Context); + +public record CrossLinkRef(Uri CrossLinkUri, string? Title, bool Hidden, IReadOnlyCollection Children, string Context) + : ITableOfContentsItem +{ + //TODO ensure we pass these to cross-links to + // CrossLinks don't have a file system path, so we use the CrossLinkUri as the Path + public string PathRelativeToDocumentationSet => CrossLinkUri.ToString(); + + // CrossLinks don't have a file system path, so we use the CrossLinkUri as the Path + public string PathRelativeToContainer => CrossLinkUri.ToString(); +} + +public record FolderRef(string PathRelativeToDocumentationSet, string PathRelativeToContainer, IReadOnlyCollection Children, string Context) + : ITableOfContentsItem; + +public record IsolatedTableOfContentsRef(string PathRelativeToDocumentationSet, string PathRelativeToContainer, IReadOnlyCollection Children, string Context) + : ITableOfContentsItem; diff --git a/src/Elastic.Documentation.Configuration/Toc/TableOfContentsYamlConverters.cs b/src/Elastic.Documentation.Configuration/Toc/TableOfContentsYamlConverters.cs new file mode 100644 index 000000000..cd215aed8 --- /dev/null +++ b/src/Elastic.Documentation.Configuration/Toc/TableOfContentsYamlConverters.cs @@ -0,0 +1,186 @@ +// Licensed to Elasticsearch B.V under one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +using Elastic.Documentation.Configuration.Toc.DetectionRules; +using YamlDotNet.Core; +using YamlDotNet.Core.Events; +using YamlDotNet.Serialization; + +namespace Elastic.Documentation.Configuration.Toc; + +public class TocItemCollectionYamlConverter : IYamlTypeConverter +{ + public bool Accepts(Type type) => type == typeof(TableOfContents); + + public object ReadYaml(IParser parser, Type type, ObjectDeserializer rootDeserializer) + { + var collection = new TableOfContents(); + + if (!parser.TryConsume(out _)) + return collection; + + while (!parser.TryConsume(out _)) + { + var item = rootDeserializer(typeof(ITableOfContentsItem)); + if (item is ITableOfContentsItem tocItem) + collection.Add(tocItem); + } + + return collection; + } + + public void WriteYaml(IEmitter emitter, object? value, Type type, ObjectSerializer serializer) => + serializer.Invoke(value, type); +} + +public class TocItemYamlConverter : IYamlTypeConverter +{ + public bool Accepts(Type type) => type == typeof(ITableOfContentsItem); + + public object? ReadYaml(IParser parser, Type type, ObjectDeserializer rootDeserializer) + { + if (!parser.TryConsume(out _)) + return null; + + var dictionary = new Dictionary(); + + while (!parser.TryConsume(out _)) + { + var key = parser.Consume(); + + // Parse the value based on what type it is + object? value = null; + if (parser.Accept(out var scalarValue)) + { + value = scalarValue.Value; + _ = parser.MoveNext(); + } + else if (parser.Accept(out _)) + { + // This is a list - parse it manually for "children" + if (key.Value == "children") + { + // Parse the children list manually + var childrenList = new List(); + _ = parser.Consume(); + while (!parser.TryConsume(out _)) + { + var child = rootDeserializer(typeof(ITableOfContentsItem)); + if (child is ITableOfContentsItem tocItem) + childrenList.Add(tocItem); + } + value = childrenList; + } + else if (key.Value == "detection_rules") + { + // Parse the children list manually + var childrenList = new List(); + _ = parser.Consume(); + while (!parser.TryConsume(out _)) + { + if (parser.Accept(out scalarValue)) + childrenList.Add(scalarValue.Value); + _ = parser.MoveNext(); + } + value = childrenList.ToArray(); + } + else + { + // For other lists, just skip them + parser.SkipThisAndNestedEvents(); + } + } + else if (parser.Accept(out _)) + { + // This is a nested mapping - skip it + parser.SkipThisAndNestedEvents(); + } + + dictionary[key.Value] = value; + } + + var children = GetChildren(dictionary); + + // Context will be set during LoadAndResolve, use empty string as placeholder during deserialization + const string placeholderContext = ""; + + // Check for folder+file combination (e.g., folder: getting-started, file: getting-started.md) + // This represents a folder with a specific index file + // The file becomes a child of the folder (as FolderIndexFileRef), and user-specified children follow + if (dictionary.TryGetValue("folder", out var folderPath) && folderPath is string folder && + dictionary.TryGetValue("file", out var filePath) && filePath is string file) + { + // Create the index file reference (FolderIndexFileRef to mark it as the folder's index) + // Store ONLY the file name - the folder path will be prepended during resolution + // This allows validation to check if the file itself has deep paths + // PathRelativeToContainer will be set during resolution + var indexFile = new FolderIndexFileRef(file, file, false, [], placeholderContext); + + // Create a list with the index file first, followed by user-specified children + var folderChildren = new List { indexFile }; + folderChildren.AddRange(children); + + // Return a FolderRef with the index file and children + // The folder path can be deep (e.g., "guides/getting-started"), that's OK + // PathRelativeToContainer will be set during resolution + return new FolderRef(folder, folder, folderChildren, placeholderContext); + } + if (dictionary.TryGetValue("detection_rules", out var detectionRulesObj) && detectionRulesObj is string[] detectionRulesFolders && + dictionary.TryGetValue("file", out var detectionRulesFilePath) && detectionRulesFilePath is string detectionRulesFile) + { + // Create the index file reference (FolderIndexFileRef to mark it as the folder's index) + // Store ONLY the file name - the folder path will be prepended during resolution + // This allows validation to check if the file itself has deep paths + // PathRelativeToContainer will be set during resolution + return new DetectionRuleOverviewRef(detectionRulesFile, detectionRulesFile, detectionRulesFolders, children, placeholderContext); + } + + // Check for file reference (file: or hidden:) + // PathRelativeToContainer will be set during resolution + if (dictionary.TryGetValue("file", out var filePathOnly) && filePathOnly is string fileOnly) + { + return fileOnly == "index.md" + ? new IndexFileRef(fileOnly, fileOnly, false, children, placeholderContext) + : new FileRef(fileOnly, fileOnly, false, children, placeholderContext); + } + + if (dictionary.TryGetValue("hidden", out var hiddenPath) && hiddenPath is string p) + return p == "index.md" ? new IndexFileRef(p, p, true, children, placeholderContext) : new FileRef(p, p, true, children, placeholderContext); + + // Check for crosslink reference + if (dictionary.TryGetValue("crosslink", out var crosslink) && crosslink is string crosslinkStr) + { + var title = dictionary.TryGetValue("title", out var t) && t is string titleStr ? titleStr : null; + var isHidden = dictionary.TryGetValue("hidden", out var h) && h is bool hiddenBool && hiddenBool; + return new CrossLinkRef(new Uri(crosslinkStr), title, isHidden, children, placeholderContext); + } + + // Check for folder reference + // PathRelativeToContainer will be set during resolution + if (dictionary.TryGetValue("folder", out var folderPathOnly) && folderPathOnly is string folderOnly) + return new FolderRef(folderOnly, folderOnly, children, placeholderContext); + + // Check for toc reference + // PathRelativeToContainer will be set during resolution + if (dictionary.TryGetValue("toc", out var tocPath) && tocPath is string source) + return new IsolatedTableOfContentsRef(source, source, children, placeholderContext); + + return null; + } + + private static IReadOnlyCollection GetChildren(Dictionary dictionary) + { + if (!dictionary.TryGetValue("children", out var childrenObj)) + return []; + + // Children have already been deserialized as List + if (childrenObj is List tocItems) + return tocItems; + + return []; + } + + public void WriteYaml(IEmitter emitter, object? value, Type type, ObjectSerializer serializer) => + serializer.Invoke(value, type); +} diff --git a/src/Elastic.Documentation.Navigation/Isolated/Node/DocumentationSetNavigation.cs b/src/Elastic.Documentation.Navigation/Isolated/Node/DocumentationSetNavigation.cs index d1c6e80dd..56ec4412b 100644 --- a/src/Elastic.Documentation.Navigation/Isolated/Node/DocumentationSetNavigation.cs +++ b/src/Elastic.Documentation.Navigation/Isolated/Node/DocumentationSetNavigation.cs @@ -4,8 +4,8 @@ using System.Diagnostics; using System.IO.Abstractions; -using Elastic.Documentation.Configuration.Plugins.DetectionRules.TableOfContents; using Elastic.Documentation.Configuration.Toc; +using Elastic.Documentation.Configuration.Toc.DetectionRules; using Elastic.Documentation.Extensions; using Elastic.Documentation.Links.CrossLinks; using Elastic.Documentation.Navigation.Isolated.Leaf; @@ -235,7 +235,7 @@ INavigationHomeAccessor homeAccessor // Create file info and documentation file var fileInfo = fileRef switch { - RuleReference ruleRef => ruleRef.FileInfo, + DetectionRuleRef ruleRef => ruleRef.FileInfo, _ => ResolveFileInfo(context, fullPath) }; var documentationFile = CreateDocumentationFile(fileInfo, context.ReadFileSystem, context, fullPath); diff --git a/src/Elastic.Documentation.Configuration/Plugins/DetectionRules/DetectionRule.cs b/src/Elastic.Markdown/Extensions/DetectionRules/DetectionRule.cs similarity index 98% rename from src/Elastic.Documentation.Configuration/Plugins/DetectionRules/DetectionRule.cs rename to src/Elastic.Markdown/Extensions/DetectionRules/DetectionRule.cs index 0064f22ef..6c0d08852 100644 --- a/src/Elastic.Documentation.Configuration/Plugins/DetectionRules/DetectionRule.cs +++ b/src/Elastic.Markdown/Extensions/DetectionRules/DetectionRule.cs @@ -6,7 +6,7 @@ using Tomlet; using Tomlet.Models; -namespace Elastic.Documentation.Configuration.Plugins.DetectionRules; +namespace Elastic.Markdown.Extensions.DetectionRules; public record DetectionRuleThreat { diff --git a/src/Elastic.Markdown/Extensions/DetectionRules/DetectionRuleFile.cs b/src/Elastic.Markdown/Extensions/DetectionRules/DetectionRuleFile.cs index f27f18f15..b724a9d2a 100644 --- a/src/Elastic.Markdown/Extensions/DetectionRules/DetectionRuleFile.cs +++ b/src/Elastic.Markdown/Extensions/DetectionRules/DetectionRuleFile.cs @@ -4,7 +4,6 @@ using System.IO.Abstractions; using Elastic.Documentation.Configuration; -using Elastic.Documentation.Configuration.Plugins.DetectionRules; using Elastic.Documentation.Navigation; using Elastic.Markdown.IO; using Elastic.Markdown.Myst; diff --git a/src/Elastic.Markdown/Extensions/DetectionRules/DetectionRulesDocsBuilderExtension.cs b/src/Elastic.Markdown/Extensions/DetectionRules/DetectionRulesDocsBuilderExtension.cs index 68b8ba164..0c24809be 100644 --- a/src/Elastic.Markdown/Extensions/DetectionRules/DetectionRulesDocsBuilderExtension.cs +++ b/src/Elastic.Markdown/Extensions/DetectionRules/DetectionRulesDocsBuilderExtension.cs @@ -4,8 +4,8 @@ using System.IO.Abstractions; using Elastic.Documentation.Configuration; -using Elastic.Documentation.Configuration.Plugins.DetectionRules.TableOfContents; using Elastic.Documentation.Configuration.Toc; +using Elastic.Documentation.Configuration.Toc.DetectionRules; using Elastic.Documentation.Navigation; using Elastic.Documentation.Navigation.Isolated.Node; using Elastic.Markdown.Exporters; @@ -51,7 +51,7 @@ public bool TryGetDocumentationFileBySlug(DocumentationSet documentationSet, str public IReadOnlyCollection<(IFileInfo, DocumentationFile)> ScanDocumentationFiles(Func defaultFileHandling) { - var rules = Build.ConfigurationYaml.TableOfContents.OfType().First().Children.OfType().ToArray(); + var rules = Build.ConfigurationYaml.TableOfContents.OfType().First().Children.OfType().ToArray(); if (rules.Length == 0) return []; diff --git a/src/services/Elastic.Documentation.Assembler/AssembleSources.cs b/src/services/Elastic.Documentation.Assembler/AssembleSources.cs index 9c36b06c6..30d3a4e16 100644 --- a/src/services/Elastic.Documentation.Assembler/AssembleSources.cs +++ b/src/services/Elastic.Documentation.Assembler/AssembleSources.cs @@ -209,8 +209,6 @@ static void ReadBlock( { Source = sourceUri, SourcePathPrefix = pathPrefix, - TopLevelSource = topLevelSource, - ParentSource = parentSource }; entries.Add(new KeyValuePair(sourceUri, tocTopLevelMapping)); From acbc5cc436cba6a03f9ea93d10e0775e6053cbb6 Mon Sep 17 00:00:00 2001 From: Martijn Laarman Date: Fri, 7 Nov 2025 15:09:29 +0100 Subject: [PATCH 169/171] Fix tests --- src/Elastic.Documentation.Site/_ViewModels.cs | 2 +- src/Elastic.Markdown/Layout/_LandingPage.cshtml | 2 +- tests/authoring/Generator/LinkReferenceFile.fs | 10 +++++----- tests/authoring/Inline/RelativeLinks.fs | 6 +++--- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/Elastic.Documentation.Site/_ViewModels.cs b/src/Elastic.Documentation.Site/_ViewModels.cs index 25b95a962..a37424b40 100644 --- a/src/Elastic.Documentation.Site/_ViewModels.cs +++ b/src/Elastic.Documentation.Site/_ViewModels.cs @@ -52,7 +52,7 @@ public string Static(string path) public string Link(string path) { - path = path.AsSpan().TrimStart('/').ToString(); + path = path.AsSpan().Trim('/').ToString(); return $"{UrlPathPrefix}/{path}"; } } diff --git a/src/Elastic.Markdown/Layout/_LandingPage.cshtml b/src/Elastic.Markdown/Layout/_LandingPage.cshtml index ebabf8ca6..574fa152f 100644 --- a/src/Elastic.Markdown/Layout/_LandingPage.cshtml +++ b/src/Elastic.Markdown/Layout/_LandingPage.cshtml @@ -13,7 +13,7 @@

Welcome to the docs that cover all changes in Elastic Stack 9.0.0 and later, including Elastic Stack @Model.CurrentVersion and Elastic Cloud Serverless. For easy reference, changes in 9.1.0 are marked inline. For details, check Understanding versioning and availability.

- + Elastic Fundamentals diff --git a/tests/authoring/Generator/LinkReferenceFile.fs b/tests/authoring/Generator/LinkReferenceFile.fs index d307d0eea..0515962d8 100644 --- a/tests/authoring/Generator/LinkReferenceFile.fs +++ b/tests/authoring/Generator/LinkReferenceFile.fs @@ -63,6 +63,7 @@ Through various means $$$including-this-inline-syntax$$$ }, "url_path_prefix": "", "links": { + "file.md": {}, "index.md": { "anchors": [ "including-this-inline-syntax", @@ -73,16 +74,15 @@ Through various means $$$including-this-inline-syntax$$$ "testing/redirects/5th-page.md": { "anchors": [ "bb" ] }, + "testing/redirects/first-page.md": { + "anchors": [ "has-an-anchor-as-well" ] + }, "testing/redirects/second-page.md": { "anchors": [ "active-anchor", "zz", "yy" ] }, "testing/redirects/third-page.md": { "anchors": [ "bb" ] - }, - "testing/redirects/first-page.md": { - "anchors": [ "has-an-anchor-as-well" ] - }, - "file.md": {} + } }, "cross_links": [], "redirects": { diff --git a/tests/authoring/Inline/RelativeLinks.fs b/tests/authoring/Inline/RelativeLinks.fs index 3d832844a..809c0069b 100644 --- a/tests/authoring/Inline/RelativeLinks.fs +++ b/tests/authoring/Inline/RelativeLinks.fs @@ -47,9 +47,9 @@ Through various means $$$including-this-inline-syntax$$$ let ``validate index.md HTML`` () = generator |> converts "deeply/nested/file.md" |> toHtml """

link to root

-

link to parent

-

link to parent

-

link to sibling

+

link to parent

+

link to parent

+

link to sibling

""" [] From 1a1c89942832b28a9971b5e1b2780b33ae04f3be Mon Sep 17 00:00:00 2001 From: Martijn Laarman Date: Fri, 7 Nov 2025 15:34:03 +0100 Subject: [PATCH 170/171] Implementsome of copilots feedback --- docs/development/navigation/node-types.md | 1 - src/Elastic.ApiExplorer/Landing/LandingNavigationItem.cs | 3 --- src/Elastic.ApiExplorer/Operations/OperationNavigationItem.cs | 2 -- src/Elastic.Documentation.Site/Navigation/_TocTreeNav.cshtml | 2 +- .../DetectionRules/DetectionRulesDocsBuilderExtension.cs | 1 - .../IO/{NewNavigation/FileR.cs => MarkdownFileFactory.cs} | 2 +- .../Links/PublishEnvironmentUriResolver.cs | 2 +- .../NavigationBuildingTests.cs | 3 --- 8 files changed, 3 insertions(+), 13 deletions(-) rename src/Elastic.Markdown/IO/{NewNavigation/FileR.cs => MarkdownFileFactory.cs} (99%) diff --git a/docs/development/navigation/node-types.md b/docs/development/navigation/node-types.md index 91c3285d3..c809e2811 100644 --- a/docs/development/navigation/node-types.md +++ b/docs/development/navigation/node-types.md @@ -146,7 +146,6 @@ toc: - URL is the crosslink itself (not calculated) - Can link to external sites or use crosslink scheme - Title is required (no auto-title from file) -- Always marked as `IsCrossLink = true` **Constructor:** ```csharp diff --git a/src/Elastic.ApiExplorer/Landing/LandingNavigationItem.cs b/src/Elastic.ApiExplorer/Landing/LandingNavigationItem.cs index 09fdd7981..0c21b61ec 100644 --- a/src/Elastic.ApiExplorer/Landing/LandingNavigationItem.cs +++ b/src/Elastic.ApiExplorer/Landing/LandingNavigationItem.cs @@ -32,7 +32,6 @@ public class LandingNavigationItem : IApiGroupingNavigationItem NavigationItems { get; set; } = []; public INodeNavigationItem? Parent { get; set; } public int NavigationIndex { get; set; } - public bool IsCrossLink => false; // API landing items are never cross-links public string Url => Index.Url; public bool Hidden => false; public Uri Identifier { get; } = new Uri("todo://"); @@ -84,7 +83,6 @@ INodeNavigationItem parent public bool Hidden => false; /// public int NavigationIndex { get; set; } - public bool IsCrossLink => false; // API grouping items are never cross-links public Uri Identifier { get; } = new Uri("todo://"); @@ -147,7 +145,6 @@ public class EndpointNavigationItem(ApiEndpoint endpoint, IRootNavigationItem public int NavigationIndex { get; set; } - public bool IsCrossLink => false; // API endpoint items are never cross-links /// public string Id { get; } = ShortId.Create(nameof(EndpointNavigationItem), endpoint.Operations.First().ApiName, endpoint.Operations.First().Route); diff --git a/src/Elastic.ApiExplorer/Operations/OperationNavigationItem.cs b/src/Elastic.ApiExplorer/Operations/OperationNavigationItem.cs index c57bb85e6..27ec0a8c9 100644 --- a/src/Elastic.ApiExplorer/Operations/OperationNavigationItem.cs +++ b/src/Elastic.ApiExplorer/Operations/OperationNavigationItem.cs @@ -60,7 +60,6 @@ IApiGroupingNavigationItem parent public IRootNavigationItem NavigationRoot { get; } //TODO enum to string public string Id { get; } - public int Depth { get; } = 1; public ApiOperation Model { get; } public string Url { get; } public bool Hidden { get; set; } @@ -70,6 +69,5 @@ IApiGroupingNavigationItem parent public INodeNavigationItem? Parent { get; set; } public int NavigationIndex { get; set; } - public bool IsCrossLink => false; // API operations are never cross-links } diff --git a/src/Elastic.Documentation.Site/Navigation/_TocTreeNav.cshtml b/src/Elastic.Documentation.Site/Navigation/_TocTreeNav.cshtml index 7494e8da3..5a64551fd 100644 --- a/src/Elastic.Documentation.Site/Navigation/_TocTreeNav.cshtml +++ b/src/Elastic.Documentation.Site/Navigation/_TocTreeNav.cshtml @@ -17,7 +17,7 @@ { continue; } - if (item is INodeNavigationItem { NavigationItems.Count: 0, Index: not null } group) + if (item is INodeNavigationItem { NavigationItems.Count: 0 } group) {
  • sourcePrefix.Length - ? path.Substring(sourcePrefix.Length).TrimStart('/') + ? path[sourcePrefix.Length..].TrimStart('/') : string.Empty; } diff --git a/tests-integration/Elastic.Assembler.IntegrationTests/NavigationBuildingTests.cs b/tests-integration/Elastic.Assembler.IntegrationTests/NavigationBuildingTests.cs index 1650b873f..c7f57a805 100644 --- a/tests-integration/Elastic.Assembler.IntegrationTests/NavigationBuildingTests.cs +++ b/tests-integration/Elastic.Assembler.IntegrationTests/NavigationBuildingTests.cs @@ -128,9 +128,6 @@ private static void RecurseNav(INodeNavigationItem($"{nav.Url}"); - if (nav.NavigationRoot.Parent is null or not SiteNavigation && nav is not CrossLinkNavigationLeaf) - { - } nav.NavigationRoot.Parent.Should().NotBeNull($"{nav.Url}"); nav.NavigationRoot.Parent.Should().BeOfType($"{nav.Url}"); } From 985b8487718fcc8096cee5aec22872dc3c7d9c4b Mon Sep 17 00:00:00 2001 From: Martijn Laarman Date: Fri, 7 Nov 2025 15:36:25 +0100 Subject: [PATCH 171/171] remove left over bad imports --- src/Elastic.Markdown/IO/DocumentationSet.cs | 1 - src/Elastic.Markdown/IO/MarkdownFile.cs | 5 ----- src/tooling/docs-builder/Http/DocumentationWebHost.cs | 1 - 3 files changed, 7 deletions(-) diff --git a/src/Elastic.Markdown/IO/DocumentationSet.cs b/src/Elastic.Markdown/IO/DocumentationSet.cs index 32033598d..6ae6cefcc 100644 --- a/src/Elastic.Markdown/IO/DocumentationSet.cs +++ b/src/Elastic.Markdown/IO/DocumentationSet.cs @@ -18,7 +18,6 @@ using Elastic.Documentation.Site.Navigation; using Elastic.Markdown.Extensions; using Elastic.Markdown.Extensions.DetectionRules; -using Elastic.Markdown.IO.NewNavigation; using Elastic.Markdown.Myst; using Microsoft.Extensions.Logging; diff --git a/src/Elastic.Markdown/IO/MarkdownFile.cs b/src/Elastic.Markdown/IO/MarkdownFile.cs index 2d0bbc8d0..f1323ceef 100644 --- a/src/Elastic.Markdown/IO/MarkdownFile.cs +++ b/src/Elastic.Markdown/IO/MarkdownFile.cs @@ -2,17 +2,12 @@ // Elasticsearch B.V licenses this file to you under the Apache 2.0 License. // See the LICENSE file in the project root for more information -using System.Collections.Frozen; using System.IO.Abstractions; -using System.Runtime.InteropServices; using Elastic.Documentation.Configuration; using Elastic.Documentation.Configuration.Products; using Elastic.Documentation.Diagnostics; -using Elastic.Documentation.Links.CrossLinks; using Elastic.Documentation.Navigation; -using Elastic.Documentation.Navigation.Isolated; using Elastic.Markdown.Helpers; -using Elastic.Markdown.IO.NewNavigation; using Elastic.Markdown.Myst; using Elastic.Markdown.Myst.Directives; using Elastic.Markdown.Myst.Directives.Include; diff --git a/src/tooling/docs-builder/Http/DocumentationWebHost.cs b/src/tooling/docs-builder/Http/DocumentationWebHost.cs index 6b4227ff1..3c8442fba 100644 --- a/src/tooling/docs-builder/Http/DocumentationWebHost.cs +++ b/src/tooling/docs-builder/Http/DocumentationWebHost.cs @@ -15,7 +15,6 @@ using Elastic.Documentation.ServiceDefaults; using Elastic.Documentation.Site.FileProviders; using Elastic.Markdown.IO; -using Elastic.Markdown.IO.NewNavigation; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http;