diff --git a/Directory.Packages.props b/Directory.Packages.props
index a33c0da98..beaa4fe95 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -23,6 +23,7 @@
+
diff --git a/README.md b/README.md
index 4abb8c8ef..7423ec4dd 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,4 @@
-# docs-builder
+#ocs-builder
[](https://github.com/elastic/docs-builder/actions/workflows/ci.yml)
diff --git a/docs-builder.sln b/docs-builder.sln
index 51348c58e..bbb9cf6a5 100644
--- a/docs-builder.sln
+++ b/docs-builder.sln
@@ -1,4 +1,4 @@
-
+
Microsoft Visual Studio Solution File, Format Version 12.00
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elastic.Markdown", "src\Elastic.Markdown\Elastic.Markdown.csproj", "{4D198E25-C211-41DC-9E84-B15E89BD7048}"
EndProject
@@ -158,153 +158,442 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elastic.Documentation.Links
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elastic.Documentation.Isolated", "src\services\Elastic.Documentation.Isolated\Elastic.Documentation.Isolated.csproj", "{AABD3EF7-8C86-4981-B1D2-B1F786F33069}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elastic.Documentation.Navigation", "src\Elastic.Documentation.Navigation\Elastic.Documentation.Navigation.csproj", "{2906C5A4-6EF2-47A4-8CC8-18D3A53AC300}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elastic.Documentation.Configuration.Tests", "tests\Elastic.Documentation.Configuration.Tests\Elastic.Documentation.Configuration.Tests.csproj", "{A8952020-F843-41B6-B456-BE95AFEBBBCA}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Navigation.Tests", "tests\Navigation.Tests\Navigation.Tests.csproj", "{E9514A33-3DC1-48B5-9131-FDBDD492A833}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
+ Debug|x64 = Debug|x64
+ Debug|x86 = Debug|x86
Release|Any CPU = Release|Any CPU
+ Release|x64 = Release|x64
+ Release|x86 = Release|x86
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{4D198E25-C211-41DC-9E84-B15E89BD7048}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{4D198E25-C211-41DC-9E84-B15E89BD7048}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {4D198E25-C211-41DC-9E84-B15E89BD7048}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {4D198E25-C211-41DC-9E84-B15E89BD7048}.Debug|x64.Build.0 = Debug|Any CPU
+ {4D198E25-C211-41DC-9E84-B15E89BD7048}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {4D198E25-C211-41DC-9E84-B15E89BD7048}.Debug|x86.Build.0 = Debug|Any CPU
{4D198E25-C211-41DC-9E84-B15E89BD7048}.Release|Any CPU.ActiveCfg = Release|Any CPU
{4D198E25-C211-41DC-9E84-B15E89BD7048}.Release|Any CPU.Build.0 = Release|Any CPU
+ {4D198E25-C211-41DC-9E84-B15E89BD7048}.Release|x64.ActiveCfg = Release|Any CPU
+ {4D198E25-C211-41DC-9E84-B15E89BD7048}.Release|x64.Build.0 = Release|Any CPU
+ {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
+ {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
+ {01F05AD0-E0E0-401F-A7EC-905928E1E9F0}.Debug|x64.Build.0 = Debug|Any CPU
+ {01F05AD0-E0E0-401F-A7EC-905928E1E9F0}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {01F05AD0-E0E0-401F-A7EC-905928E1E9F0}.Debug|x86.Build.0 = Debug|Any CPU
{01F05AD0-E0E0-401F-A7EC-905928E1E9F0}.Release|Any CPU.ActiveCfg = Release|Any CPU
{01F05AD0-E0E0-401F-A7EC-905928E1E9F0}.Release|Any CPU.Build.0 = Release|Any CPU
+ {01F05AD0-E0E0-401F-A7EC-905928E1E9F0}.Release|x64.ActiveCfg = Release|Any CPU
+ {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
+ {B27C5107-128B-465A-B8F8-8985399E4CFB}.Debug|x64.Build.0 = Debug|Any CPU
+ {B27C5107-128B-465A-B8F8-8985399E4CFB}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {B27C5107-128B-465A-B8F8-8985399E4CFB}.Debug|x86.Build.0 = Debug|Any CPU
{B27C5107-128B-465A-B8F8-8985399E4CFB}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B27C5107-128B-465A-B8F8-8985399E4CFB}.Release|Any CPU.Build.0 = Release|Any CPU
+ {B27C5107-128B-465A-B8F8-8985399E4CFB}.Release|x64.ActiveCfg = Release|Any CPU
+ {B27C5107-128B-465A-B8F8-8985399E4CFB}.Release|x64.Build.0 = Release|Any CPU
+ {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
+ {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
{28350800-B44B-479B-86E2-1D39E321C0B4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{28350800-B44B-479B-86E2-1D39E321C0B4}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {28350800-B44B-479B-86E2-1D39E321C0B4}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {28350800-B44B-479B-86E2-1D39E321C0B4}.Debug|x64.Build.0 = Debug|Any CPU
+ {28350800-B44B-479B-86E2-1D39E321C0B4}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {28350800-B44B-479B-86E2-1D39E321C0B4}.Debug|x86.Build.0 = Debug|Any CPU
{28350800-B44B-479B-86E2-1D39E321C0B4}.Release|Any CPU.ActiveCfg = Release|Any CPU
{28350800-B44B-479B-86E2-1D39E321C0B4}.Release|Any CPU.Build.0 = Release|Any CPU
+ {28350800-B44B-479B-86E2-1D39E321C0B4}.Release|x64.ActiveCfg = Release|Any CPU
+ {28350800-B44B-479B-86E2-1D39E321C0B4}.Release|x64.Build.0 = Release|Any CPU
+ {28350800-B44B-479B-86E2-1D39E321C0B4}.Release|x86.ActiveCfg = Release|Any CPU
+ {28350800-B44B-479B-86E2-1D39E321C0B4}.Release|x86.Build.0 = Release|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
+ {018F959E-824B-4664-B345-066784478D24}.Debug|x64.Build.0 = Debug|Any CPU
+ {018F959E-824B-4664-B345-066784478D24}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {018F959E-824B-4664-B345-066784478D24}.Debug|x86.Build.0 = Debug|Any CPU
{018F959E-824B-4664-B345-066784478D24}.Release|Any CPU.ActiveCfg = Release|Any CPU
{018F959E-824B-4664-B345-066784478D24}.Release|Any CPU.Build.0 = Release|Any CPU
+ {018F959E-824B-4664-B345-066784478D24}.Release|x64.ActiveCfg = Release|Any CPU
+ {018F959E-824B-4664-B345-066784478D24}.Release|x64.Build.0 = Release|Any CPU
+ {018F959E-824B-4664-B345-066784478D24}.Release|x86.ActiveCfg = Release|Any CPU
+ {018F959E-824B-4664-B345-066784478D24}.Release|x86.Build.0 = Release|Any CPU
{7D36DDDA-9E0B-4D2C-8033-5D62FF8B6166}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{7D36DDDA-9E0B-4D2C-8033-5D62FF8B6166}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {7D36DDDA-9E0B-4D2C-8033-5D62FF8B6166}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {7D36DDDA-9E0B-4D2C-8033-5D62FF8B6166}.Debug|x64.Build.0 = Debug|Any CPU
+ {7D36DDDA-9E0B-4D2C-8033-5D62FF8B6166}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {7D36DDDA-9E0B-4D2C-8033-5D62FF8B6166}.Debug|x86.Build.0 = Debug|Any CPU
{7D36DDDA-9E0B-4D2C-8033-5D62FF8B6166}.Release|Any CPU.ActiveCfg = Release|Any CPU
{7D36DDDA-9E0B-4D2C-8033-5D62FF8B6166}.Release|Any CPU.Build.0 = Release|Any CPU
+ {7D36DDDA-9E0B-4D2C-8033-5D62FF8B6166}.Release|x64.ActiveCfg = Release|Any CPU
+ {7D36DDDA-9E0B-4D2C-8033-5D62FF8B6166}.Release|x64.Build.0 = Release|Any CPU
+ {7D36DDDA-9E0B-4D2C-8033-5D62FF8B6166}.Release|x86.ActiveCfg = Release|Any CPU
+ {7D36DDDA-9E0B-4D2C-8033-5D62FF8B6166}.Release|x86.Build.0 = Release|Any CPU
{4CCE599A-B9FE-4DF2-8763-34CF0A99D4AA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{4CCE599A-B9FE-4DF2-8763-34CF0A99D4AA}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {4CCE599A-B9FE-4DF2-8763-34CF0A99D4AA}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {4CCE599A-B9FE-4DF2-8763-34CF0A99D4AA}.Debug|x64.Build.0 = Debug|Any CPU
+ {4CCE599A-B9FE-4DF2-8763-34CF0A99D4AA}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {4CCE599A-B9FE-4DF2-8763-34CF0A99D4AA}.Debug|x86.Build.0 = Debug|Any CPU
{4CCE599A-B9FE-4DF2-8763-34CF0A99D4AA}.Release|Any CPU.ActiveCfg = Release|Any CPU
{4CCE599A-B9FE-4DF2-8763-34CF0A99D4AA}.Release|Any CPU.Build.0 = Release|Any CPU
+ {4CCE599A-B9FE-4DF2-8763-34CF0A99D4AA}.Release|x64.ActiveCfg = Release|Any CPU
+ {4CCE599A-B9FE-4DF2-8763-34CF0A99D4AA}.Release|x64.Build.0 = Release|Any CPU
+ {4CCE599A-B9FE-4DF2-8763-34CF0A99D4AA}.Release|x86.ActiveCfg = Release|Any CPU
+ {4CCE599A-B9FE-4DF2-8763-34CF0A99D4AA}.Release|x86.Build.0 = Release|Any CPU
{C559D52D-100B-4B2B-BE87-2344D835761D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C559D52D-100B-4B2B-BE87-2344D835761D}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {C559D52D-100B-4B2B-BE87-2344D835761D}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {C559D52D-100B-4B2B-BE87-2344D835761D}.Debug|x64.Build.0 = Debug|Any CPU
+ {C559D52D-100B-4B2B-BE87-2344D835761D}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {C559D52D-100B-4B2B-BE87-2344D835761D}.Debug|x86.Build.0 = Debug|Any CPU
{C559D52D-100B-4B2B-BE87-2344D835761D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C559D52D-100B-4B2B-BE87-2344D835761D}.Release|Any CPU.Build.0 = Release|Any CPU
+ {C559D52D-100B-4B2B-BE87-2344D835761D}.Release|x64.ActiveCfg = Release|Any CPU
+ {C559D52D-100B-4B2B-BE87-2344D835761D}.Release|x64.Build.0 = Release|Any CPU
+ {C559D52D-100B-4B2B-BE87-2344D835761D}.Release|x86.ActiveCfg = Release|Any CPU
+ {C559D52D-100B-4B2B-BE87-2344D835761D}.Release|x86.Build.0 = Release|Any CPU
{CDC0ECF4-6597-4FBA-8D25-5C244F0877E3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{CDC0ECF4-6597-4FBA-8D25-5C244F0877E3}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {CDC0ECF4-6597-4FBA-8D25-5C244F0877E3}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {CDC0ECF4-6597-4FBA-8D25-5C244F0877E3}.Debug|x64.Build.0 = Debug|Any CPU
+ {CDC0ECF4-6597-4FBA-8D25-5C244F0877E3}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {CDC0ECF4-6597-4FBA-8D25-5C244F0877E3}.Debug|x86.Build.0 = Debug|Any CPU
{CDC0ECF4-6597-4FBA-8D25-5C244F0877E3}.Release|Any CPU.ActiveCfg = Release|Any CPU
{CDC0ECF4-6597-4FBA-8D25-5C244F0877E3}.Release|Any CPU.Build.0 = Release|Any CPU
+ {CDC0ECF4-6597-4FBA-8D25-5C244F0877E3}.Release|x64.ActiveCfg = Release|Any CPU
+ {CDC0ECF4-6597-4FBA-8D25-5C244F0877E3}.Release|x64.Build.0 = Release|Any CPU
+ {CDC0ECF4-6597-4FBA-8D25-5C244F0877E3}.Release|x86.ActiveCfg = Release|Any CPU
+ {CDC0ECF4-6597-4FBA-8D25-5C244F0877E3}.Release|x86.Build.0 = Release|Any CPU
{09CE30F6-013A-49ED-B3D6-60AFA84682AC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{09CE30F6-013A-49ED-B3D6-60AFA84682AC}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {09CE30F6-013A-49ED-B3D6-60AFA84682AC}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {09CE30F6-013A-49ED-B3D6-60AFA84682AC}.Debug|x64.Build.0 = Debug|Any CPU
+ {09CE30F6-013A-49ED-B3D6-60AFA84682AC}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {09CE30F6-013A-49ED-B3D6-60AFA84682AC}.Debug|x86.Build.0 = Debug|Any CPU
{09CE30F6-013A-49ED-B3D6-60AFA84682AC}.Release|Any CPU.ActiveCfg = Release|Any CPU
{09CE30F6-013A-49ED-B3D6-60AFA84682AC}.Release|Any CPU.Build.0 = Release|Any CPU
+ {09CE30F6-013A-49ED-B3D6-60AFA84682AC}.Release|x64.ActiveCfg = Release|Any CPU
+ {09CE30F6-013A-49ED-B3D6-60AFA84682AC}.Release|x64.Build.0 = Release|Any CPU
+ {09CE30F6-013A-49ED-B3D6-60AFA84682AC}.Release|x86.ActiveCfg = Release|Any CPU
+ {09CE30F6-013A-49ED-B3D6-60AFA84682AC}.Release|x86.Build.0 = Release|Any CPU
{CD94F9E4-7FCD-4152-81F1-4288C6B75367}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{CD94F9E4-7FCD-4152-81F1-4288C6B75367}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {CD94F9E4-7FCD-4152-81F1-4288C6B75367}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {CD94F9E4-7FCD-4152-81F1-4288C6B75367}.Debug|x64.Build.0 = Debug|Any CPU
+ {CD94F9E4-7FCD-4152-81F1-4288C6B75367}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {CD94F9E4-7FCD-4152-81F1-4288C6B75367}.Debug|x86.Build.0 = Debug|Any CPU
{CD94F9E4-7FCD-4152-81F1-4288C6B75367}.Release|Any CPU.ActiveCfg = Release|Any CPU
{CD94F9E4-7FCD-4152-81F1-4288C6B75367}.Release|Any CPU.Build.0 = Release|Any CPU
+ {CD94F9E4-7FCD-4152-81F1-4288C6B75367}.Release|x64.ActiveCfg = Release|Any CPU
+ {CD94F9E4-7FCD-4152-81F1-4288C6B75367}.Release|x64.Build.0 = Release|Any CPU
+ {CD94F9E4-7FCD-4152-81F1-4288C6B75367}.Release|x86.ActiveCfg = Release|Any CPU
+ {CD94F9E4-7FCD-4152-81F1-4288C6B75367}.Release|x86.Build.0 = Release|Any CPU
{FD1AC230-798B-4AB9-8CE6-A06264885DBC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{FD1AC230-798B-4AB9-8CE6-A06264885DBC}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {FD1AC230-798B-4AB9-8CE6-A06264885DBC}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {FD1AC230-798B-4AB9-8CE6-A06264885DBC}.Debug|x64.Build.0 = Debug|Any CPU
+ {FD1AC230-798B-4AB9-8CE6-A06264885DBC}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {FD1AC230-798B-4AB9-8CE6-A06264885DBC}.Debug|x86.Build.0 = Debug|Any CPU
{FD1AC230-798B-4AB9-8CE6-A06264885DBC}.Release|Any CPU.ActiveCfg = Release|Any CPU
{FD1AC230-798B-4AB9-8CE6-A06264885DBC}.Release|Any CPU.Build.0 = Release|Any CPU
+ {FD1AC230-798B-4AB9-8CE6-A06264885DBC}.Release|x64.ActiveCfg = Release|Any CPU
+ {FD1AC230-798B-4AB9-8CE6-A06264885DBC}.Release|x64.Build.0 = Release|Any CPU
+ {FD1AC230-798B-4AB9-8CE6-A06264885DBC}.Release|x86.ActiveCfg = Release|Any CPU
+ {FD1AC230-798B-4AB9-8CE6-A06264885DBC}.Release|x86.Build.0 = Release|Any CPU
{C883AC18-7C6A-482E-A9D7-C44DF8633425}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C883AC18-7C6A-482E-A9D7-C44DF8633425}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {C883AC18-7C6A-482E-A9D7-C44DF8633425}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {C883AC18-7C6A-482E-A9D7-C44DF8633425}.Debug|x64.Build.0 = Debug|Any CPU
+ {C883AC18-7C6A-482E-A9D7-C44DF8633425}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {C883AC18-7C6A-482E-A9D7-C44DF8633425}.Debug|x86.Build.0 = Debug|Any CPU
{C883AC18-7C6A-482E-A9D7-C44DF8633425}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C883AC18-7C6A-482E-A9D7-C44DF8633425}.Release|Any CPU.Build.0 = Release|Any CPU
+ {C883AC18-7C6A-482E-A9D7-C44DF8633425}.Release|x64.ActiveCfg = Release|Any CPU
+ {C883AC18-7C6A-482E-A9D7-C44DF8633425}.Release|x64.Build.0 = Release|Any CPU
+ {C883AC18-7C6A-482E-A9D7-C44DF8633425}.Release|x86.ActiveCfg = Release|Any CPU
+ {C883AC18-7C6A-482E-A9D7-C44DF8633425}.Release|x86.Build.0 = Release|Any CPU
{0331559E-4ED1-4A56-9C35-3EAD4D7E696D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{0331559E-4ED1-4A56-9C35-3EAD4D7E696D}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {0331559E-4ED1-4A56-9C35-3EAD4D7E696D}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {0331559E-4ED1-4A56-9C35-3EAD4D7E696D}.Debug|x64.Build.0 = Debug|Any CPU
+ {0331559E-4ED1-4A56-9C35-3EAD4D7E696D}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {0331559E-4ED1-4A56-9C35-3EAD4D7E696D}.Debug|x86.Build.0 = Debug|Any CPU
{0331559E-4ED1-4A56-9C35-3EAD4D7E696D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{0331559E-4ED1-4A56-9C35-3EAD4D7E696D}.Release|Any CPU.Build.0 = Release|Any CPU
+ {0331559E-4ED1-4A56-9C35-3EAD4D7E696D}.Release|x64.ActiveCfg = Release|Any CPU
+ {0331559E-4ED1-4A56-9C35-3EAD4D7E696D}.Release|x64.Build.0 = Release|Any CPU
+ {0331559E-4ED1-4A56-9C35-3EAD4D7E696D}.Release|x86.ActiveCfg = Release|Any CPU
+ {0331559E-4ED1-4A56-9C35-3EAD4D7E696D}.Release|x86.Build.0 = Release|Any CPU
{89B83007-71E6-4B57-BA78-2544BFA476DB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{89B83007-71E6-4B57-BA78-2544BFA476DB}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {89B83007-71E6-4B57-BA78-2544BFA476DB}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {89B83007-71E6-4B57-BA78-2544BFA476DB}.Debug|x64.Build.0 = Debug|Any CPU
+ {89B83007-71E6-4B57-BA78-2544BFA476DB}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {89B83007-71E6-4B57-BA78-2544BFA476DB}.Debug|x86.Build.0 = Debug|Any CPU
{89B83007-71E6-4B57-BA78-2544BFA476DB}.Release|Any CPU.ActiveCfg = Release|Any CPU
{89B83007-71E6-4B57-BA78-2544BFA476DB}.Release|Any CPU.Build.0 = Release|Any CPU
+ {89B83007-71E6-4B57-BA78-2544BFA476DB}.Release|x64.ActiveCfg = Release|Any CPU
+ {89B83007-71E6-4B57-BA78-2544BFA476DB}.Release|x64.Build.0 = Release|Any CPU
+ {89B83007-71E6-4B57-BA78-2544BFA476DB}.Release|x86.ActiveCfg = Release|Any CPU
+ {89B83007-71E6-4B57-BA78-2544BFA476DB}.Release|x86.Build.0 = Release|Any CPU
{111E7029-BB29-4039-9B45-04776798A8DD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{111E7029-BB29-4039-9B45-04776798A8DD}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {111E7029-BB29-4039-9B45-04776798A8DD}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {111E7029-BB29-4039-9B45-04776798A8DD}.Debug|x64.Build.0 = Debug|Any CPU
+ {111E7029-BB29-4039-9B45-04776798A8DD}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {111E7029-BB29-4039-9B45-04776798A8DD}.Debug|x86.Build.0 = Debug|Any CPU
{111E7029-BB29-4039-9B45-04776798A8DD}.Release|Any CPU.ActiveCfg = Release|Any CPU
{111E7029-BB29-4039-9B45-04776798A8DD}.Release|Any CPU.Build.0 = Release|Any CPU
+ {111E7029-BB29-4039-9B45-04776798A8DD}.Release|x64.ActiveCfg = Release|Any CPU
+ {111E7029-BB29-4039-9B45-04776798A8DD}.Release|x64.Build.0 = Release|Any CPU
+ {111E7029-BB29-4039-9B45-04776798A8DD}.Release|x86.ActiveCfg = Release|Any CPU
+ {111E7029-BB29-4039-9B45-04776798A8DD}.Release|x86.Build.0 = Release|Any CPU
{164F55EC-9412-4CD4-81AD-3598B57632A6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{164F55EC-9412-4CD4-81AD-3598B57632A6}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {164F55EC-9412-4CD4-81AD-3598B57632A6}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {164F55EC-9412-4CD4-81AD-3598B57632A6}.Debug|x64.Build.0 = Debug|Any CPU
+ {164F55EC-9412-4CD4-81AD-3598B57632A6}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {164F55EC-9412-4CD4-81AD-3598B57632A6}.Debug|x86.Build.0 = Debug|Any CPU
{164F55EC-9412-4CD4-81AD-3598B57632A6}.Release|Any CPU.ActiveCfg = Release|Any CPU
{164F55EC-9412-4CD4-81AD-3598B57632A6}.Release|Any CPU.Build.0 = Release|Any CPU
+ {164F55EC-9412-4CD4-81AD-3598B57632A6}.Release|x64.ActiveCfg = Release|Any CPU
+ {164F55EC-9412-4CD4-81AD-3598B57632A6}.Release|x64.Build.0 = Release|Any CPU
+ {164F55EC-9412-4CD4-81AD-3598B57632A6}.Release|x86.ActiveCfg = Release|Any CPU
+ {164F55EC-9412-4CD4-81AD-3598B57632A6}.Release|x86.Build.0 = Release|Any CPU
{A272D3EC-FAAF-4795-A796-302725382AFF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A272D3EC-FAAF-4795-A796-302725382AFF}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {A272D3EC-FAAF-4795-A796-302725382AFF}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {A272D3EC-FAAF-4795-A796-302725382AFF}.Debug|x64.Build.0 = Debug|Any CPU
+ {A272D3EC-FAAF-4795-A796-302725382AFF}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {A272D3EC-FAAF-4795-A796-302725382AFF}.Debug|x86.Build.0 = Debug|Any CPU
{A272D3EC-FAAF-4795-A796-302725382AFF}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A272D3EC-FAAF-4795-A796-302725382AFF}.Release|Any CPU.Build.0 = Release|Any CPU
+ {A272D3EC-FAAF-4795-A796-302725382AFF}.Release|x64.ActiveCfg = Release|Any CPU
+ {A272D3EC-FAAF-4795-A796-302725382AFF}.Release|x64.Build.0 = Release|Any CPU
+ {A272D3EC-FAAF-4795-A796-302725382AFF}.Release|x86.ActiveCfg = Release|Any CPU
+ {A272D3EC-FAAF-4795-A796-302725382AFF}.Release|x86.Build.0 = Release|Any CPU
{4DFECE72-4A1F-4B58-918E-DCD07B585231}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{4DFECE72-4A1F-4B58-918E-DCD07B585231}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {4DFECE72-4A1F-4B58-918E-DCD07B585231}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {4DFECE72-4A1F-4B58-918E-DCD07B585231}.Debug|x64.Build.0 = Debug|Any CPU
+ {4DFECE72-4A1F-4B58-918E-DCD07B585231}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {4DFECE72-4A1F-4B58-918E-DCD07B585231}.Debug|x86.Build.0 = Debug|Any CPU
{4DFECE72-4A1F-4B58-918E-DCD07B585231}.Release|Any CPU.ActiveCfg = Release|Any CPU
{4DFECE72-4A1F-4B58-918E-DCD07B585231}.Release|Any CPU.Build.0 = Release|Any CPU
+ {4DFECE72-4A1F-4B58-918E-DCD07B585231}.Release|x64.ActiveCfg = Release|Any CPU
+ {4DFECE72-4A1F-4B58-918E-DCD07B585231}.Release|x64.Build.0 = Release|Any CPU
+ {4DFECE72-4A1F-4B58-918E-DCD07B585231}.Release|x86.ActiveCfg = Release|Any CPU
+ {4DFECE72-4A1F-4B58-918E-DCD07B585231}.Release|x86.Build.0 = Release|Any CPU
{2A83ED35-B631-4F02-8D4C-15611D0DB72C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{2A83ED35-B631-4F02-8D4C-15611D0DB72C}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {2A83ED35-B631-4F02-8D4C-15611D0DB72C}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {2A83ED35-B631-4F02-8D4C-15611D0DB72C}.Debug|x64.Build.0 = Debug|Any CPU
+ {2A83ED35-B631-4F02-8D4C-15611D0DB72C}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {2A83ED35-B631-4F02-8D4C-15611D0DB72C}.Debug|x86.Build.0 = Debug|Any CPU
{2A83ED35-B631-4F02-8D4C-15611D0DB72C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{2A83ED35-B631-4F02-8D4C-15611D0DB72C}.Release|Any CPU.Build.0 = Release|Any CPU
+ {2A83ED35-B631-4F02-8D4C-15611D0DB72C}.Release|x64.ActiveCfg = Release|Any CPU
+ {2A83ED35-B631-4F02-8D4C-15611D0DB72C}.Release|x64.Build.0 = Release|Any CPU
+ {2A83ED35-B631-4F02-8D4C-15611D0DB72C}.Release|x86.ActiveCfg = Release|Any CPU
+ {2A83ED35-B631-4F02-8D4C-15611D0DB72C}.Release|x86.Build.0 = Release|Any CPU
{F30B90AD-1A01-4A6F-9699-809FA6875B22}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F30B90AD-1A01-4A6F-9699-809FA6875B22}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {F30B90AD-1A01-4A6F-9699-809FA6875B22}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {F30B90AD-1A01-4A6F-9699-809FA6875B22}.Debug|x64.Build.0 = Debug|Any CPU
+ {F30B90AD-1A01-4A6F-9699-809FA6875B22}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {F30B90AD-1A01-4A6F-9699-809FA6875B22}.Debug|x86.Build.0 = Debug|Any CPU
{F30B90AD-1A01-4A6F-9699-809FA6875B22}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F30B90AD-1A01-4A6F-9699-809FA6875B22}.Release|Any CPU.Build.0 = Release|Any CPU
+ {F30B90AD-1A01-4A6F-9699-809FA6875B22}.Release|x64.ActiveCfg = Release|Any CPU
+ {F30B90AD-1A01-4A6F-9699-809FA6875B22}.Release|x64.Build.0 = Release|Any CPU
+ {F30B90AD-1A01-4A6F-9699-809FA6875B22}.Release|x86.ActiveCfg = Release|Any CPU
+ {F30B90AD-1A01-4A6F-9699-809FA6875B22}.Release|x86.Build.0 = Release|Any CPU
{AE3FC78E-167F-4B6E-88EC-84743EB748B7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{AE3FC78E-167F-4B6E-88EC-84743EB748B7}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {AE3FC78E-167F-4B6E-88EC-84743EB748B7}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {AE3FC78E-167F-4B6E-88EC-84743EB748B7}.Debug|x64.Build.0 = Debug|Any CPU
+ {AE3FC78E-167F-4B6E-88EC-84743EB748B7}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {AE3FC78E-167F-4B6E-88EC-84743EB748B7}.Debug|x86.Build.0 = Debug|Any CPU
{AE3FC78E-167F-4B6E-88EC-84743EB748B7}.Release|Any CPU.ActiveCfg = Release|Any CPU
{AE3FC78E-167F-4B6E-88EC-84743EB748B7}.Release|Any CPU.Build.0 = Release|Any CPU
+ {AE3FC78E-167F-4B6E-88EC-84743EB748B7}.Release|x64.ActiveCfg = Release|Any CPU
+ {AE3FC78E-167F-4B6E-88EC-84743EB748B7}.Release|x64.Build.0 = Release|Any CPU
+ {AE3FC78E-167F-4B6E-88EC-84743EB748B7}.Release|x86.ActiveCfg = Release|Any CPU
+ {AE3FC78E-167F-4B6E-88EC-84743EB748B7}.Release|x86.Build.0 = Release|Any CPU
{C6A121C5-DEB1-4FCE-9140-AF144EA98EEE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C6A121C5-DEB1-4FCE-9140-AF144EA98EEE}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {C6A121C5-DEB1-4FCE-9140-AF144EA98EEE}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {C6A121C5-DEB1-4FCE-9140-AF144EA98EEE}.Debug|x64.Build.0 = Debug|Any CPU
+ {C6A121C5-DEB1-4FCE-9140-AF144EA98EEE}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {C6A121C5-DEB1-4FCE-9140-AF144EA98EEE}.Debug|x86.Build.0 = Debug|Any CPU
{C6A121C5-DEB1-4FCE-9140-AF144EA98EEE}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C6A121C5-DEB1-4FCE-9140-AF144EA98EEE}.Release|Any CPU.Build.0 = Release|Any CPU
+ {C6A121C5-DEB1-4FCE-9140-AF144EA98EEE}.Release|x64.ActiveCfg = Release|Any CPU
+ {C6A121C5-DEB1-4FCE-9140-AF144EA98EEE}.Release|x64.Build.0 = Release|Any CPU
+ {C6A121C5-DEB1-4FCE-9140-AF144EA98EEE}.Release|x86.ActiveCfg = Release|Any CPU
+ {C6A121C5-DEB1-4FCE-9140-AF144EA98EEE}.Release|x86.Build.0 = Release|Any CPU
{094433A4-504F-4E12-959F-CCB1965C1C9A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{094433A4-504F-4E12-959F-CCB1965C1C9A}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {094433A4-504F-4E12-959F-CCB1965C1C9A}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {094433A4-504F-4E12-959F-CCB1965C1C9A}.Debug|x64.Build.0 = Debug|Any CPU
+ {094433A4-504F-4E12-959F-CCB1965C1C9A}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {094433A4-504F-4E12-959F-CCB1965C1C9A}.Debug|x86.Build.0 = Debug|Any CPU
{094433A4-504F-4E12-959F-CCB1965C1C9A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{094433A4-504F-4E12-959F-CCB1965C1C9A}.Release|Any CPU.Build.0 = Release|Any CPU
+ {094433A4-504F-4E12-959F-CCB1965C1C9A}.Release|x64.ActiveCfg = Release|Any CPU
+ {094433A4-504F-4E12-959F-CCB1965C1C9A}.Release|x64.Build.0 = Release|Any CPU
+ {094433A4-504F-4E12-959F-CCB1965C1C9A}.Release|x86.ActiveCfg = Release|Any CPU
+ {094433A4-504F-4E12-959F-CCB1965C1C9A}.Release|x86.Build.0 = Release|Any CPU
{E6EA955D-D0A7-4749-9586-0F7256EF5C5E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{E6EA955D-D0A7-4749-9586-0F7256EF5C5E}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {E6EA955D-D0A7-4749-9586-0F7256EF5C5E}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {E6EA955D-D0A7-4749-9586-0F7256EF5C5E}.Debug|x64.Build.0 = Debug|Any CPU
+ {E6EA955D-D0A7-4749-9586-0F7256EF5C5E}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {E6EA955D-D0A7-4749-9586-0F7256EF5C5E}.Debug|x86.Build.0 = Debug|Any CPU
{E6EA955D-D0A7-4749-9586-0F7256EF5C5E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E6EA955D-D0A7-4749-9586-0F7256EF5C5E}.Release|Any CPU.Build.0 = Release|Any CPU
+ {E6EA955D-D0A7-4749-9586-0F7256EF5C5E}.Release|x64.ActiveCfg = Release|Any CPU
+ {E6EA955D-D0A7-4749-9586-0F7256EF5C5E}.Release|x64.Build.0 = Release|Any CPU
+ {E6EA955D-D0A7-4749-9586-0F7256EF5C5E}.Release|x86.ActiveCfg = Release|Any CPU
+ {E6EA955D-D0A7-4749-9586-0F7256EF5C5E}.Release|x86.Build.0 = Release|Any CPU
{153FC4AD-F5B0-4100-990E-0987C86DBF01}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{153FC4AD-F5B0-4100-990E-0987C86DBF01}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {153FC4AD-F5B0-4100-990E-0987C86DBF01}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {153FC4AD-F5B0-4100-990E-0987C86DBF01}.Debug|x64.Build.0 = Debug|Any CPU
+ {153FC4AD-F5B0-4100-990E-0987C86DBF01}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {153FC4AD-F5B0-4100-990E-0987C86DBF01}.Debug|x86.Build.0 = Debug|Any CPU
{153FC4AD-F5B0-4100-990E-0987C86DBF01}.Release|Any CPU.ActiveCfg = Release|Any CPU
{153FC4AD-F5B0-4100-990E-0987C86DBF01}.Release|Any CPU.Build.0 = Release|Any CPU
+ {153FC4AD-F5B0-4100-990E-0987C86DBF01}.Release|x64.ActiveCfg = Release|Any CPU
+ {153FC4AD-F5B0-4100-990E-0987C86DBF01}.Release|x64.Build.0 = Release|Any CPU
+ {153FC4AD-F5B0-4100-990E-0987C86DBF01}.Release|x86.ActiveCfg = Release|Any CPU
+ {153FC4AD-F5B0-4100-990E-0987C86DBF01}.Release|x86.Build.0 = Release|Any CPU
{AABD3EF7-8C86-4981-B1D2-B1F786F33069}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{AABD3EF7-8C86-4981-B1D2-B1F786F33069}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {AABD3EF7-8C86-4981-B1D2-B1F786F33069}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {AABD3EF7-8C86-4981-B1D2-B1F786F33069}.Debug|x64.Build.0 = Debug|Any CPU
+ {AABD3EF7-8C86-4981-B1D2-B1F786F33069}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {AABD3EF7-8C86-4981-B1D2-B1F786F33069}.Debug|x86.Build.0 = Debug|Any CPU
{AABD3EF7-8C86-4981-B1D2-B1F786F33069}.Release|Any CPU.ActiveCfg = Release|Any CPU
{AABD3EF7-8C86-4981-B1D2-B1F786F33069}.Release|Any CPU.Build.0 = Release|Any CPU
+ {AABD3EF7-8C86-4981-B1D2-B1F786F33069}.Release|x64.ActiveCfg = Release|Any CPU
+ {AABD3EF7-8C86-4981-B1D2-B1F786F33069}.Release|x64.Build.0 = Release|Any CPU
+ {AABD3EF7-8C86-4981-B1D2-B1F786F33069}.Release|x86.ActiveCfg = Release|Any CPU
+ {AABD3EF7-8C86-4981-B1D2-B1F786F33069}.Release|x86.Build.0 = Release|Any CPU
+ {2906C5A4-6EF2-47A4-8CC8-18D3A53AC300}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {2906C5A4-6EF2-47A4-8CC8-18D3A53AC300}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {2906C5A4-6EF2-47A4-8CC8-18D3A53AC300}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {2906C5A4-6EF2-47A4-8CC8-18D3A53AC300}.Debug|x64.Build.0 = Debug|Any CPU
+ {2906C5A4-6EF2-47A4-8CC8-18D3A53AC300}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {2906C5A4-6EF2-47A4-8CC8-18D3A53AC300}.Debug|x86.Build.0 = Debug|Any CPU
+ {2906C5A4-6EF2-47A4-8CC8-18D3A53AC300}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {2906C5A4-6EF2-47A4-8CC8-18D3A53AC300}.Release|Any CPU.Build.0 = Release|Any CPU
+ {2906C5A4-6EF2-47A4-8CC8-18D3A53AC300}.Release|x64.ActiveCfg = Release|Any CPU
+ {2906C5A4-6EF2-47A4-8CC8-18D3A53AC300}.Release|x64.Build.0 = Release|Any CPU
+ {2906C5A4-6EF2-47A4-8CC8-18D3A53AC300}.Release|x86.ActiveCfg = Release|Any CPU
+ {2906C5A4-6EF2-47A4-8CC8-18D3A53AC300}.Release|x86.Build.0 = Release|Any CPU
+ {A8952020-F843-41B6-B456-BE95AFEBBBCA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {A8952020-F843-41B6-B456-BE95AFEBBBCA}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {A8952020-F843-41B6-B456-BE95AFEBBBCA}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {A8952020-F843-41B6-B456-BE95AFEBBBCA}.Debug|x64.Build.0 = Debug|Any CPU
+ {A8952020-F843-41B6-B456-BE95AFEBBBCA}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {A8952020-F843-41B6-B456-BE95AFEBBBCA}.Debug|x86.Build.0 = Debug|Any CPU
+ {A8952020-F843-41B6-B456-BE95AFEBBBCA}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {A8952020-F843-41B6-B456-BE95AFEBBBCA}.Release|Any CPU.Build.0 = Release|Any CPU
+ {A8952020-F843-41B6-B456-BE95AFEBBBCA}.Release|x64.ActiveCfg = Release|Any CPU
+ {A8952020-F843-41B6-B456-BE95AFEBBBCA}.Release|x64.Build.0 = Release|Any CPU
+ {A8952020-F843-41B6-B456-BE95AFEBBBCA}.Release|x86.ActiveCfg = Release|Any CPU
+ {A8952020-F843-41B6-B456-BE95AFEBBBCA}.Release|x86.Build.0 = Release|Any CPU
+ {E9514A33-3DC1-48B5-9131-FDBDD492A833}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {E9514A33-3DC1-48B5-9131-FDBDD492A833}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {E9514A33-3DC1-48B5-9131-FDBDD492A833}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {E9514A33-3DC1-48B5-9131-FDBDD492A833}.Debug|x64.Build.0 = Debug|Any CPU
+ {E9514A33-3DC1-48B5-9131-FDBDD492A833}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {E9514A33-3DC1-48B5-9131-FDBDD492A833}.Debug|x86.Build.0 = Debug|Any CPU
+ {E9514A33-3DC1-48B5-9131-FDBDD492A833}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {E9514A33-3DC1-48B5-9131-FDBDD492A833}.Release|Any CPU.Build.0 = Release|Any CPU
+ {E9514A33-3DC1-48B5-9131-FDBDD492A833}.Release|x64.ActiveCfg = Release|Any CPU
+ {E9514A33-3DC1-48B5-9131-FDBDD492A833}.Release|x64.Build.0 = Release|Any CPU
+ {E9514A33-3DC1-48B5-9131-FDBDD492A833}.Release|x86.ActiveCfg = Release|Any CPU
+ {E9514A33-3DC1-48B5-9131-FDBDD492A833}.Release|x86.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{4D198E25-C211-41DC-9E84-B15E89BD7048} = {BE6011CC-1200-4957-B01F-FCCA10C5CF5A}
+ {01F05AD0-E0E0-401F-A7EC-905928E1E9F0} = {73ABAE37-118F-4A53-BC2C-F19333555C90}
{B27C5107-128B-465A-B8F8-8985399E4CFB} = {67B576EE-02FA-4F9B-94BC-3630BC09ECE5}
{CD2887E3-BDA9-434B-A5BF-9ED38DE20332} = {245023D2-D3CA-47B9-831D-DAB91A2FFDC7}
{A2A34BBC-CB5E-4100-9529-A12B6ECB769C} = {245023D2-D3CA-47B9-831D-DAB91A2FFDC7}
+ {28350800-B44B-479B-86E2-1D39E321C0B4} = {73ABAE37-118F-4A53-BC2C-F19333555C90}
{018F959E-824B-4664-B345-066784478D24} = {67B576EE-02FA-4F9B-94BC-3630BC09ECE5}
+ {7D36DDDA-9E0B-4D2C-8033-5D62FF8B6166} = {059E787F-85C1-43BE-9DD6-CE319E106383}
{CFEE9FAD-9E0C-4C0E-A0C2-B97D594C14B5} = {245023D2-D3CA-47B9-831D-DAB91A2FFDC7}
+ {4CCE599A-B9FE-4DF2-8763-34CF0A99D4AA} = {73ABAE37-118F-4A53-BC2C-F19333555C90}
{6E2ED6CC-AFC1-4E58-965D-6AEC500EBB46} = {245023D2-D3CA-47B9-831D-DAB91A2FFDC7}
{6554F917-73CE-4B3D-9101-F28EAA762C6B} = {245023D2-D3CA-47B9-831D-DAB91A2FFDC7}
+ {C559D52D-100B-4B2B-BE87-2344D835761D} = {4894063D-0DEF-4B7E-97D0-0D0A5B85C608}
{CDC0ECF4-6597-4FBA-8D25-5C244F0877E3} = {67B576EE-02FA-4F9B-94BC-3630BC09ECE5}
{4894063D-0DEF-4B7E-97D0-0D0A5B85C608} = {BE6011CC-1200-4957-B01F-FCCA10C5CF5A}
- {C559D52D-100B-4B2B-BE87-2344D835761D} = {4894063D-0DEF-4B7E-97D0-0D0A5B85C608}
{BB789671-B262-43DD-91DB-39F9186B8257} = {245023D2-D3CA-47B9-831D-DAB91A2FFDC7}
{09CE30F6-013A-49ED-B3D6-60AFA84682AC} = {BE6011CC-1200-4957-B01F-FCCA10C5CF5A}
{CD94F9E4-7FCD-4152-81F1-4288C6B75367} = {BE6011CC-1200-4957-B01F-FCCA10C5CF5A}
{73ABAE37-118F-4A53-BC2C-F19333555C90} = {BE6011CC-1200-4957-B01F-FCCA10C5CF5A}
- {28350800-B44B-479B-86E2-1D39E321C0B4} = {73ABAE37-118F-4A53-BC2C-F19333555C90}
- {01F05AD0-E0E0-401F-A7EC-905928E1E9F0} = {73ABAE37-118F-4A53-BC2C-F19333555C90}
- {4CCE599A-B9FE-4DF2-8763-34CF0A99D4AA} = {73ABAE37-118F-4A53-BC2C-F19333555C90}
{059E787F-85C1-43BE-9DD6-CE319E106383} = {BE6011CC-1200-4957-B01F-FCCA10C5CF5A}
- {7D36DDDA-9E0B-4D2C-8033-5D62FF8B6166} = {059E787F-85C1-43BE-9DD6-CE319E106383}
{FB1C1954-D8E2-4745-BA62-04DD82FB4792} = {245023D2-D3CA-47B9-831D-DAB91A2FFDC7}
{E20FEEF9-1D1A-4CDA-A546-7FDC573BE399} = {245023D2-D3CA-47B9-831D-DAB91A2FFDC7}
{FD1AC230-798B-4AB9-8CE6-A06264885DBC} = {BE6011CC-1200-4957-B01F-FCCA10C5CF5A}
@@ -325,5 +614,8 @@ Global
{E6EA955D-D0A7-4749-9586-0F7256EF5C5E} = {7AACA67B-3C56-4C7C-9891-558589FC52DB}
{153FC4AD-F5B0-4100-990E-0987C86DBF01} = {BE6011CC-1200-4957-B01F-FCCA10C5CF5A}
{AABD3EF7-8C86-4981-B1D2-B1F786F33069} = {7AACA67B-3C56-4C7C-9891-558589FC52DB}
+ {2906C5A4-6EF2-47A4-8CC8-18D3A53AC300} = {BE6011CC-1200-4957-B01F-FCCA10C5CF5A}
+ {A8952020-F843-41B6-B456-BE95AFEBBBCA} = {67B576EE-02FA-4F9B-94BC-3630BC09ECE5}
+ {E9514A33-3DC1-48B5-9131-FDBDD492A833} = {67B576EE-02FA-4F9B-94BC-3630BC09ECE5}
EndGlobalSection
EndGlobal
diff --git a/docs/_docset.yml b/docs/_docset.yml
index bad2fe7e4..5f94bdd03 100644
--- a/docs/_docset.yml
+++ b/docs/_docset.yml
@@ -174,6 +174,8 @@ toc:
- file: index.md
- file: req.md
- folder: nested
+ children:
+ - file: index.md
- file: cross-links.md
children:
- title: "Getting Started Guide"
diff --git a/src/Elastic.ApiExplorer/ApiIndexLeafNavigation.cs b/src/Elastic.ApiExplorer/ApiIndexLeafNavigation.cs
new file mode 100644
index 000000000..dadab3f12
--- /dev/null
+++ b/src/Elastic.ApiExplorer/ApiIndexLeafNavigation.cs
@@ -0,0 +1,39 @@
+// 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.Navigation;
+
+namespace Elastic.ApiExplorer;
+
+public class ApiIndexLeafNavigation(
+ TModel model, string url, string navigationTitle,
+ IRootNavigationItem rootNavigation,
+ INodeNavigationItem? parent = null
+) : ILeafNavigationItem
+ where TModel : IApiModel
+{
+ ///
+ public string Url { get; } = url;
+
+ ///
+ public string NavigationTitle { get; } = navigationTitle;
+
+ ///
+ public IRootNavigationItem NavigationRoot { get; } = rootNavigation;
+
+ ///
+ public INodeNavigationItem? Parent { get; set; } = parent;
+
+ ///
+ public bool Hidden { get; }
+
+ ///
+ public int NavigationIndex { get; set; }
+
+ ///
+ public bool IsCrossLink { get; }
+
+ ///
+ public TModel Model { get; } = model;
+}
diff --git a/src/Elastic.ApiExplorer/ApiRenderContext.cs b/src/Elastic.ApiExplorer/ApiRenderContext.cs
index 0a60880e6..749ebc42a 100644
--- a/src/Elastic.ApiExplorer/ApiRenderContext.cs
+++ b/src/Elastic.ApiExplorer/ApiRenderContext.cs
@@ -4,6 +4,7 @@
using Elastic.Documentation;
using Elastic.Documentation.Configuration;
+using Elastic.Documentation.Navigation;
using Elastic.Documentation.Site.FileProviders;
using Elastic.Documentation.Site.Navigation;
using Microsoft.OpenApi.Models;
diff --git a/src/Elastic.ApiExplorer/ApiViewModel.cs b/src/Elastic.ApiExplorer/ApiViewModel.cs
index 0cc7d464e..048190738 100644
--- a/src/Elastic.ApiExplorer/ApiViewModel.cs
+++ b/src/Elastic.ApiExplorer/ApiViewModel.cs
@@ -7,9 +7,9 @@
using Elastic.Documentation.Configuration.Assembler;
using Elastic.Documentation.Configuration.Builder;
using Elastic.Documentation.Extensions;
+using Elastic.Documentation.Navigation;
using Elastic.Documentation.Site;
using Elastic.Documentation.Site.FileProviders;
-using Elastic.Documentation.Site.Navigation;
using Microsoft.AspNetCore.Html;
namespace Elastic.ApiExplorer;
diff --git a/src/Elastic.ApiExplorer/Endpoints/ApiEndpoint.cs b/src/Elastic.ApiExplorer/Endpoints/ApiEndpoint.cs
index 4e918aacc..b79b83b14 100644
--- a/src/Elastic.ApiExplorer/Endpoints/ApiEndpoint.cs
+++ b/src/Elastic.ApiExplorer/Endpoints/ApiEndpoint.cs
@@ -3,6 +3,7 @@
// See the LICENSE file in the project root for more information
using System.IO.Abstractions;
+using Elastic.Documentation.Navigation;
using Elastic.Documentation.Site.Navigation;
using Microsoft.OpenApi.Models.Interfaces;
using RazorSlices;
diff --git a/src/Elastic.ApiExplorer/Landing/LandingNavigationItem.cs b/src/Elastic.ApiExplorer/Landing/LandingNavigationItem.cs
index 9c636420e..d0326fc84 100644
--- a/src/Elastic.ApiExplorer/Landing/LandingNavigationItem.cs
+++ b/src/Elastic.ApiExplorer/Landing/LandingNavigationItem.cs
@@ -5,7 +5,7 @@
using System.IO.Abstractions;
using Elastic.ApiExplorer.Operations;
using Elastic.Documentation.Extensions;
-using Elastic.Documentation.Site.Navigation;
+using Elastic.Documentation.Navigation;
using RazorSlices;
namespace Elastic.ApiExplorer.Landing;
@@ -29,27 +29,23 @@ public class LandingNavigationItem : IApiGroupingNavigationItem NavigationRoot { get; }
public string Id { get; }
public int Depth { get; }
- public ApiLanding Index { get; }
+ public ILeafNavigationItem Index { get; }
public IReadOnlyCollection 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 { get; }
+ public string Url => Index.Url;
public bool Hidden => false;
- //TODO
- public string NavigationTitle { get; } = "API Overview";
+ public string NavigationTitle => Index.NavigationTitle;
public LandingNavigationItem(string url)
{
Depth = 0;
NavigationRoot = this;
Id = ShortId.Create("root");
-
var landing = new ApiLanding();
- Url = url;
-
- Index = landing;
+ Index = new ApiIndexLeafNavigation(landing, url, "Api Overview", this);
}
///
@@ -63,7 +59,8 @@ public interface IApiGroupingNavigationItem(
TGroupingModel groupingModel,
IRootNavigationItem rootNavigation,
- INodeNavigationItem parent)
+ INodeNavigationItem parent
+)
: IApiGroupingNavigationItem
where TGroupingModel : IApiGroupingModel
where TNavigationItem : INavigationItem
@@ -91,8 +88,10 @@ public abstract class ApiGroupingNavigationItem
///
public abstract string Id { get; }
+
+ //TODO ensure Index is not newed everytime
///
- public TGroupingModel Index { get; } = groupingModel;
+ public ILeafNavigationItem Index => new ApiIndexLeafNavigation(groupingModel, Url, NavigationTitle, rootNavigation, Parent);
///
public IReadOnlyCollection NavigationItems { get; set; } = [];
@@ -151,8 +150,9 @@ public class EndpointNavigationItem(ApiEndpoint endpoint, IRootNavigationItem
public string Id { get; } = ShortId.Create(nameof(EndpointNavigationItem), endpoint.Operations.First().ApiName, endpoint.Operations.First().Route);
+ //TODO ensure Index is not newed everytime
///
- public ApiEndpoint Index { get; } = endpoint;
+ public ILeafNavigationItem Index => new ApiIndexLeafNavigation(endpoint, Url, NavigationTitle, rootNavigation, Parent);
///
public IReadOnlyCollection NavigationItems { get; set; } = [];
diff --git a/src/Elastic.ApiExplorer/Landing/LandingView.cshtml b/src/Elastic.ApiExplorer/Landing/LandingView.cshtml
index 8d81b5502..f3fd31b30 100644
--- a/src/Elastic.ApiExplorer/Landing/LandingView.cshtml
+++ b/src/Elastic.ApiExplorer/Landing/LandingView.cshtml
@@ -1,6 +1,7 @@
@inherits RazorSliceHttpResult
@using Elastic.ApiExplorer.Landing
@using Elastic.ApiExplorer.Operations
+@using Elastic.Documentation.Navigation
@using Elastic.Documentation.Site.Navigation
@implements IUsesLayout
@functions {
diff --git a/src/Elastic.ApiExplorer/OpenApiGenerator.cs b/src/Elastic.ApiExplorer/OpenApiGenerator.cs
index 50860c849..e31f6f298 100644
--- a/src/Elastic.ApiExplorer/OpenApiGenerator.cs
+++ b/src/Elastic.ApiExplorer/OpenApiGenerator.cs
@@ -8,6 +8,7 @@
using Elastic.ApiExplorer.Operations;
using Elastic.Documentation;
using Elastic.Documentation.Configuration;
+using Elastic.Documentation.Navigation;
using Elastic.Documentation.Site.FileProviders;
using Elastic.Documentation.Site.Navigation;
using Microsoft.Extensions.Logging;
@@ -265,7 +266,7 @@ public async Task Generate(Cancel ctx = default)
CurrentNavigation = navigation,
MarkdownRenderer = markdownStringRenderer
};
- _ = await Render(prefix, navigation, navigation.Index, renderContext, navigationRenderer, ctx);
+ _ = await Render(prefix, navigation, navigation.Index.Model, renderContext, navigationRenderer, ctx);
await RenderNavigationItems(prefix, renderContext, navigationRenderer, navigation, ctx);
}
@@ -275,7 +276,7 @@ private async Task RenderNavigationItems(string prefix, ApiRenderContext renderC
{
if (currentNavigation is INodeNavigationItem node)
{
- _ = await Render(prefix, node, node.Index, renderContext, navigationRenderer, ctx);
+ _ = await Render(prefix, node, node.Index.Model, renderContext, navigationRenderer, ctx);
foreach (var child in node.NavigationItems)
await RenderNavigationItems(prefix, renderContext, navigationRenderer, child, ctx);
}
diff --git a/src/Elastic.ApiExplorer/Operations/OperationNavigationItem.cs b/src/Elastic.ApiExplorer/Operations/OperationNavigationItem.cs
index 1c0f51fdb..c57bb85e6 100644
--- a/src/Elastic.ApiExplorer/Operations/OperationNavigationItem.cs
+++ b/src/Elastic.ApiExplorer/Operations/OperationNavigationItem.cs
@@ -5,7 +5,7 @@
using System.IO.Abstractions;
using Elastic.ApiExplorer.Landing;
using Elastic.Documentation.Extensions;
-using Elastic.Documentation.Site.Navigation;
+using Elastic.Documentation.Navigation;
using Microsoft.OpenApi.Models;
using Microsoft.OpenApi.Models.Interfaces;
using RazorSlices;
diff --git a/src/Elastic.Documentation.Configuration/Assembler/AssemblyConfiguration.cs b/src/Elastic.Documentation.Configuration/Assembler/AssemblyConfiguration.cs
index 3cccb9cf2..52d5cb1c5 100644
--- a/src/Elastic.Documentation.Configuration/Assembler/AssemblyConfiguration.cs
+++ b/src/Elastic.Documentation.Configuration/Assembler/AssemblyConfiguration.cs
@@ -5,7 +5,6 @@
using System.Text.RegularExpressions;
using Elastic.Documentation.Extensions;
using YamlDotNet.Serialization;
-using YamlStaticContext = Elastic.Documentation.Configuration.Serialization.YamlStaticContext;
namespace Elastic.Documentation.Configuration.Assembler;
diff --git a/src/Elastic.Documentation.Configuration/BuildContext.cs b/src/Elastic.Documentation.Configuration/BuildContext.cs
index 6b3b383c3..37e80ea39 100644
--- a/src/Elastic.Documentation.Configuration/BuildContext.cs
+++ b/src/Elastic.Documentation.Configuration/BuildContext.cs
@@ -6,6 +6,7 @@
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.Versions;
@@ -28,6 +29,8 @@ public record BuildContext : IDocumentationSetContext, IDocumentationConfigurati
public ConfigurationFile Configuration { get; }
+ public DocumentationSetFile ConfigurationYaml { get; set; }
+
public VersionsConfiguration VersionsConfiguration { get; }
public ConfigurationFileProvider ConfigurationFileProvider { get; }
public DocumentationEndpoints Endpoints { get; }
@@ -108,9 +111,13 @@ public BuildContext(
Git = gitCheckoutInformation ?? GitCheckoutInformation.Create(DocumentationCheckoutDirectory, ReadFileSystem);
Configuration = new ConfigurationFile(this, VersionsConfiguration, ProductsConfiguration);
+
+ var yaml = readFileSystem.File.ReadAllText(Configuration.SourceFile.FullName);
+ ConfigurationYaml = DocumentationSetFile.Deserialize(yaml);
GoogleTagManager = new GoogleTagManagerConfiguration
{
Enabled = false
};
}
+
}
diff --git a/src/Elastic.Documentation.Configuration/Builder/ConfigurationFile.cs b/src/Elastic.Documentation.Configuration/Builder/ConfigurationFile.cs
index a5b1c94de..3fe004f4f 100644
--- a/src/Elastic.Documentation.Configuration/Builder/ConfigurationFile.cs
+++ b/src/Elastic.Documentation.Configuration/Builder/ConfigurationFile.cs
@@ -39,10 +39,6 @@ public record ConfigurationFile : ITableOfContentsScope
public HashSet Products { get; } = [];
- public HashSet ImplicitFolders { get; } = new(StringComparer.OrdinalIgnoreCase);
-
- public Glob[] Globs { get; } = [];
-
private readonly Dictionary _substitutions = new(StringComparer.OrdinalIgnoreCase);
public IReadOnlyDictionary Substitutions => _substitutions;
@@ -195,8 +191,6 @@ public ConfigurationFile(IDocumentationSetContext context, VersionsConfiguration
reader.EmitError("Could not load docset.yml", e);
throw;
}
-
- Globs = [.. ImplicitFolders.Select(f => Glob.Parse($"{f}{Path.DirectorySeparatorChar}*.md"))];
}
}
diff --git a/src/Elastic.Documentation.Configuration/Builder/TableOfContentsConfiguration.cs b/src/Elastic.Documentation.Configuration/Builder/TableOfContentsConfiguration.cs
index 346734fbe..dd9a041df 100644
--- a/src/Elastic.Documentation.Configuration/Builder/TableOfContentsConfiguration.cs
+++ b/src/Elastic.Documentation.Configuration/Builder/TableOfContentsConfiguration.cs
@@ -240,12 +240,7 @@ file is null && crossLink is null && folder is null && toc is null &&
}
if (folder is not null)
- {
- if (children is null)
- _ = _configuration.ImplicitFolders.Add(parentPath.TrimStart(Path.DirectorySeparatorChar));
-
return [new FolderReference(this, $"{parentPath}".TrimStart(Path.DirectorySeparatorChar), children ?? [])];
- }
return null;
}
diff --git a/src/Elastic.Documentation.Configuration/ConfigurationFileProvider.cs b/src/Elastic.Documentation.Configuration/ConfigurationFileProvider.cs
index 7b6f5c03b..42e1952af 100644
--- a/src/Elastic.Documentation.Configuration/ConfigurationFileProvider.cs
+++ b/src/Elastic.Documentation.Configuration/ConfigurationFileProvider.cs
@@ -5,6 +5,7 @@
using System.IO.Abstractions;
using System.Text.RegularExpressions;
using Elastic.Documentation.Configuration.Assembler;
+using Elastic.Documentation.Configuration.DocSet;
using Elastic.Documentation.Configuration.Serialization;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
@@ -21,6 +22,10 @@ public partial class ConfigurationFileProvider
internal static IDeserializer Deserializer { get; } = new StaticDeserializerBuilder(new YamlStaticContext())
.WithNamingConvention(UnderscoredNamingConvention.Instance)
+ .WithTypeConverter(new TocItemCollectionYamlConverter())
+ .WithTypeConverter(new TocItemYamlConverter())
+ .WithTypeConverter(new SiteTableOfContentsCollectionYamlConverter())
+ .WithTypeConverter(new SiteTableOfContentsRefYamlConverter())
.Build();
public ConfigurationSource ConfigurationSource { get; }
diff --git a/src/Elastic.Documentation.Configuration/DocSet/DocumentationSetFile.cs b/src/Elastic.Documentation.Configuration/DocSet/DocumentationSetFile.cs
new file mode 100644
index 000000000..870ff94a3
--- /dev/null
+++ b/src/Elastic.Documentation.Configuration/DocSet/DocumentationSetFile.cs
@@ -0,0 +1,208 @@
+// 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 YamlDotNet.Core;
+using YamlDotNet.Core.Events;
+using YamlDotNet.Serialization;
+
+namespace Elastic.Documentation.Configuration.DocSet;
+
+[YamlSerializable]
+public class TableOfContentsFile
+{
+ [YamlMember(Alias = "toc")]
+ public TableOfContents Toc { get; set; } = [];
+
+ public static TableOfContentsFile Deserialize(string json) =>
+ ConfigurationFileProvider.Deserializer.Deserialize(json);
+}
+
+[YamlSerializable]
+public class DocumentationSetFile : TableOfContentsFile
+{
+ [YamlMember(Alias = "project")]
+ public string? Project { get; set; }
+
+ [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 = "subs")]
+ public Dictionary Subs { get; set; } = [];
+
+ [YamlMember(Alias = "features")]
+ public DocumentationSetFeatures Features { get; set; } = new();
+
+ [YamlMember(Alias = "api")]
+ public Dictionary Api { get; set; } = [];
+
+ [YamlMember(Alias = "toc")]
+ public new TableOfContents Toc { get; set; } = [];
+
+ public static new DocumentationSetFile Deserialize(string json) =>
+ ConfigurationFileProvider.Deserializer.Deserialize(json);
+}
+
+[YamlSerializable]
+public class DocumentationSetFeatures
+{
+ [YamlMember(Alias = "primary-nav", ApplyNamingConventions = false)]
+ public bool? PrimaryNav { get; set; }
+}
+
+public class TableOfContents : List
+{
+ public TableOfContents() { }
+
+ public TableOfContents(IEnumerable items) : base(items) { }
+}
+
+
+public interface ITableOfContentsItem;
+
+public record FileRef(string Path, bool Hidden, IReadOnlyCollection Children)
+ : ITableOfContentsItem;
+
+public record IndexFileRef(string Path, bool Hidden, IReadOnlyCollection Children)
+ : FileRef(Path, Hidden, Children);
+
+public record CrossLinkRef(Uri CrossLinkUri, string? Title, bool Hidden, IReadOnlyCollection Children)
+ : ITableOfContentsItem;
+
+public record FolderRef(string Path, IReadOnlyCollection Children)
+ : ITableOfContentsItem;
+
+public record IsolatedTableOfContentsRef(string Source, IReadOnlyCollection Children)
+ : 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);
+
+ // Check for file reference (file: or hidden:)
+ if (dictionary.TryGetValue("file", out var filePath) && filePath is string file)
+ return file == "index.md" ? new IndexFileRef(file, false, children) : new FileRef(file, false, children);
+
+ if (dictionary.TryGetValue("hidden", out var hiddenPath) && hiddenPath is string p)
+ return p == "index.md" ? new IndexFileRef(p, true, children) : new FileRef(p, true, children);
+
+ // 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);
+ }
+
+ // Check for folder reference
+ if (dictionary.TryGetValue("folder", out var folderPath) && folderPath is string folder)
+ return new FolderRef(folder, children);
+
+ // Check for toc reference
+ if (dictionary.TryGetValue("toc", out var tocPath) && tocPath is string source)
+ return new IsolatedTableOfContentsRef(source, children);
+
+ 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/SiteNavigationFile.cs b/src/Elastic.Documentation.Configuration/DocSet/SiteNavigationFile.cs
new file mode 100644
index 000000000..17f83d0b9
--- /dev/null
+++ b/src/Elastic.Documentation.Configuration/DocSet/SiteNavigationFile.cs
@@ -0,0 +1,155 @@
+// 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.Serialization;
+using YamlDotNet.Core;
+using YamlDotNet.Core.Events;
+using YamlDotNet.Serialization;
+
+namespace Elastic.Documentation.Configuration.DocSet;
+
+[YamlSerializable]
+public class SiteNavigationFile
+{
+ [YamlMember(Alias = "phantoms")]
+ public IReadOnlyCollection Phantoms { get; set; } = [];
+
+ [YamlMember(Alias = "toc")]
+ public SiteTableOfContents TableOfContents { get; set; } = [];
+
+ public static SiteNavigationFile Deserialize(string yaml) =>
+ ConfigurationFileProvider.Deserializer.Deserialize(yaml);
+}
+
+public class PhantomRegistration
+{
+ [YamlMember(Alias = "toc")]
+ public string Source { get; set; } = null!;
+}
+
+public class SiteTableOfContents : List
+{
+ public SiteTableOfContents() { }
+
+ public SiteTableOfContents(IEnumerable items) : base(items) { }
+}
+
+public record SiteTableOfContentsRef(Uri Source, string PathPrefix, IReadOnlyCollection Children)
+ : ITableOfContentsItem;
+
+public class SiteTableOfContentsCollectionYamlConverter : IYamlTypeConverter
+{
+ public bool Accepts(Type type) => type == typeof(SiteTableOfContents);
+
+ public object ReadYaml(IParser parser, Type type, ObjectDeserializer rootDeserializer)
+ {
+ var collection = new SiteTableOfContents();
+
+ if (!parser.TryConsume(out _))
+ return collection;
+
+ while (!parser.TryConsume(out _))
+ {
+ var item = rootDeserializer(typeof(SiteTableOfContentsRef));
+ if (item is SiteTableOfContentsRef tocRef)
+ collection.Add(tocRef);
+ }
+
+ return collection;
+ }
+
+ public void WriteYaml(IEmitter emitter, object? value, Type type, ObjectSerializer serializer) =>
+ serializer.Invoke(value, type);
+}
+
+public class SiteTableOfContentsRefYamlConverter : IYamlTypeConverter
+{
+ public bool Accepts(Type type) => type == typeof(SiteTableOfContentsRef);
+
+ 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(SiteTableOfContentsRef));
+ if (child is SiteTableOfContentsRef childRef)
+ childrenList.Add(childRef);
+ }
+ 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);
+
+ // Check for toc reference - required
+ if (dictionary.TryGetValue("toc", out var tocPath) && tocPath is string sourceString)
+ {
+ // Convert string to Uri - if no scheme, prepend "docs-content://"
+ var uriString = sourceString.Contains("://") ? sourceString : $"docs-content://{sourceString}";
+
+ if (!Uri.TryCreate(uriString, UriKind.Absolute, out var source))
+ throw new InvalidOperationException($"Invalid TOC source: '{sourceString}' could not be parsed as a URI");
+
+ var pathPrefix = dictionary.TryGetValue("path_prefix", out var pathValue) && pathValue is string path
+ ? path
+ : string.Empty;
+
+ return new SiteTableOfContentsRef(source, pathPrefix, children);
+ }
+
+ 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 tocRefs)
+ return tocRefs;
+
+ return [];
+ }
+
+ public void WriteYaml(IEmitter emitter, object? value, Type type, ObjectSerializer serializer) =>
+ serializer.Invoke(value, type);
+}
+
diff --git a/src/Elastic.Documentation.Configuration/Products/ProductExtensions.cs b/src/Elastic.Documentation.Configuration/Products/ProductExtensions.cs
index eab68d0bb..f46b187dd 100644
--- a/src/Elastic.Documentation.Configuration/Products/ProductExtensions.cs
+++ b/src/Elastic.Documentation.Configuration/Products/ProductExtensions.cs
@@ -4,6 +4,7 @@
using System.Collections.Frozen;
using Elastic.Documentation.Configuration.Versions;
+using YamlDotNet.Serialization;
namespace Elastic.Documentation.Configuration.Products;
@@ -35,10 +36,14 @@ public static ProductsConfiguration CreateProducts(this ConfigurationFileProvide
internal sealed record ProductConfigDto
{
+ [YamlMember(Alias = "products")]
public Dictionary Products { get; set; } = [];
}
internal sealed record ProductDto
{
+ [YamlMember(Alias = "display")]
public string Display { get; set; } = string.Empty;
+
+ [YamlMember(Alias = "versioning")]
public string? Versioning { get; set; }
}
diff --git a/src/Elastic.Documentation.Configuration/Serialization/YamlStaticContext.cs b/src/Elastic.Documentation.Configuration/Serialization/YamlStaticContext.cs
index 5a9c6ef8a..fc39310b7 100644
--- a/src/Elastic.Documentation.Configuration/Serialization/YamlStaticContext.cs
+++ b/src/Elastic.Documentation.Configuration/Serialization/YamlStaticContext.cs
@@ -3,8 +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.TableOfContents;
using Elastic.Documentation.Configuration.Versions;
using YamlDotNet.Serialization;
@@ -23,4 +25,8 @@ namespace Elastic.Documentation.Configuration.Serialization;
[YamlSerializable(typeof(ProductDto))]
[YamlSerializable(typeof(LegacyUrlMappingDto))]
[YamlSerializable(typeof(LegacyUrlMappingConfigDto))]
+[YamlSerializable(typeof(DocumentationSetFile))]
+[YamlSerializable(typeof(TableOfContentsFile))]
+[YamlSerializable(typeof(SiteNavigationFile))]
+[YamlSerializable(typeof(PhantomRegistration))]
public partial class YamlStaticContext;
diff --git a/src/Elastic.Documentation.Configuration/Versions/VersionsConfigurationExtensions.cs b/src/Elastic.Documentation.Configuration/Versions/VersionsConfigurationExtensions.cs
index 00ae91b88..290b88c75 100644
--- a/src/Elastic.Documentation.Configuration/Versions/VersionsConfigurationExtensions.cs
+++ b/src/Elastic.Documentation.Configuration/Versions/VersionsConfigurationExtensions.cs
@@ -2,6 +2,8 @@
// 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 YamlDotNet.Serialization;
+
namespace Elastic.Documentation.Configuration.Versions;
public static class VersionsConfigurationExtensions
@@ -56,7 +58,9 @@ internal sealed record VersionsConfigDto
internal sealed record VersioningSystemDto
{
+ [YamlMember(Alias = "base")]
public string Base { get; set; } = string.Empty;
+ [YamlMember(Alias = "current")]
public string Current { get; set; } = string.Empty;
}
diff --git a/src/Elastic.Documentation.Navigation/Assembler/AssembledNavigation.cs b/src/Elastic.Documentation.Navigation/Assembler/AssembledNavigation.cs
new file mode 100644
index 000000000..e7ed68ec8
--- /dev/null
+++ b/src/Elastic.Documentation.Navigation/Assembler/AssembledNavigation.cs
@@ -0,0 +1,224 @@
+// 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.DocSet;
+using Elastic.Documentation.Extensions;
+using Elastic.Documentation.Navigation.Isolated;
+
+namespace Elastic.Documentation.Navigation.Assembler;
+
+public record SiteNavigationNoIndexFile(string NavigationTitle) : IDocumentationFile;
+
+
+public class SiteNavigation : IRootNavigationItem
+{
+ public SiteNavigation(
+ SiteNavigationFile siteNavigationFile,
+ IDocumentationContext context,
+ IReadOnlyCollection documentationSetNavigations
+ )
+ {
+ // Initialize root properties
+ NavigationRoot = this;
+ Parent = null;
+ Depth = 0;
+ Hidden = false;
+ IsCrossLink = false;
+ Id = ShortId.Create("site");
+ IsUsingNavigationDropdown = false;
+ _nodes = [];
+ foreach (var setNavigation in documentationSetNavigations)
+ {
+ foreach (var (identifier, node) in setNavigation.TableOfContentNodes)
+ {
+ if (!_nodes.TryAdd(identifier, node))
+ {
+ //TODO configurationFileProvider navigation path
+ context.EmitError(context.ConfigurationPath, $"Duplicate navigation identifier: {identifier} in navigation.yml");
+ }
+ }
+ }
+
+ // Build NavigationItems from SiteTableOfContentsRef items
+ var items = new List();
+ var index = 0;
+ foreach (var tocRef in siteNavigationFile.TableOfContents)
+ {
+ var navItem = CreateSiteTableOfContentsNavigation(
+ tocRef,
+ index++,
+ context
+ );
+
+ if (navItem != null)
+ items.Add(navItem);
+ }
+
+ NavigationItems = items;
+ _ = this.UpdateNavigationIndex(context);
+ Index = this.FindIndex(new NotFoundModel("/index.md"));
+ }
+
+ private readonly Dictionary> _nodes;
+ public IReadOnlyDictionary> Nodes => _nodes;
+
+
+ //TODO Obsolete?
+ public IReadOnlyCollection> TopLevelItems =>
+ NavigationItems.OfType>().ToList();
+
+ ///
+ public string Url { get; set; } = "/";
+
+ ///
+ public string NavigationTitle => Index.NavigationTitle;
+
+ ///
+ public IRootNavigationItem NavigationRoot { get; }
+
+ ///
+ public INodeNavigationItem? Parent { get; set; }
+
+ ///
+ public bool Hidden { get; }
+
+ ///
+ public int NavigationIndex { get; set; }
+
+ ///
+ public bool IsCrossLink { get; }
+
+ ///
+ public int Depth { get; }
+
+ ///
+ public string Id { get; }
+
+ ///
+ public ILeafNavigationItem Index { get; }
+
+ ///
+ public bool IsUsingNavigationDropdown { get; }
+
+ ///
+ public IReadOnlyCollection NavigationItems { get; }
+
+ private INavigationItem? CreateSiteTableOfContentsNavigation(
+ SiteTableOfContentsRef tocRef,
+ int index,
+ IDocumentationContext context
+ )
+ {
+ var pathPrefix = tocRef.PathPrefix;
+ // Validate that path_prefix is set
+ if (string.IsNullOrWhiteSpace(pathPrefix))
+ {
+ context.EmitError(context.ConfigurationPath, $"path_prefix is required for TOC reference: {tocRef.Source}");
+ pathPrefix = tocRef.Source.Scheme;
+ if (!string.IsNullOrEmpty(tocRef.Source.Host))
+ pathPrefix += $"/{tocRef.Source.Host}";
+ if (!string.IsNullOrEmpty(tocRef.Source.AbsolutePath) && tocRef.Source.AbsolutePath != "/")
+ pathPrefix += $"/{tocRef.Source.AbsolutePath}";
+ }
+
+ // Look up the node in the collected nodes
+ if (!_nodes.TryGetValue(tocRef.Source, out var node))
+ {
+ context.EmitError(context.ConfigurationPath, $"Could not find navigation node for identifier: {tocRef.Source} (from source: {tocRef.Source})");
+ return null;
+ }
+ if (node is not INavigationPathPrefixProvider prefixProvider)
+ {
+ context.EmitError(context.ConfigurationPath, $"Navigation contains an node navigation that does not implement: {nameof(IPathPrefixProvider)} (from source: {tocRef.Source})");
+ return null;
+ }
+
+ // Set the navigation index
+ node.NavigationIndex = index;
+ prefixProvider.PathPrefixProvider = new PathPrefixProvider(pathPrefix);
+
+ // Recursively create child navigation items if children are specified
+ var children = new List();
+ if (tocRef.Children.Count > 0)
+ {
+ var childIndex = 0;
+ foreach (var child in tocRef.Children)
+ {
+ var childItem = CreateSiteTableOfContentsNavigation(
+ child,
+ childIndex++,
+ context
+ );
+ if (childItem != null)
+ children.Add(childItem);
+ }
+ }
+ else
+ {
+ // If no children specified, use the node's original children
+ children = node.NavigationItems.ToList();
+ }
+
+ // Always return a wrapper to ensure path_prefix is the URL (not path_prefix + node's URL)
+ return new SiteTableOfContentsNavigation(node, prefixProvider.PathPrefixProvider, children);
+ }
+}
+
+///
+/// Wrapper for a navigation node that applies a path prefix to URLs and optionally
+/// overrides the children to show only the children specified in the site navigation configuration.
+///
+///
+/// Wrapper for a navigation node that applies a path prefix to URLs and optionally
+/// overrides the children to show only the children specified in the site navigation configuration.
+///
+public sealed class SiteTableOfContentsNavigation(
+ INodeNavigationItem wrappedNode,
+ IPathPrefixProvider pathPrefixProvider,
+ IReadOnlyCollection children
+ ) : INodeNavigationItem, INavigationPathPrefixProvider
+ where TModel : IDocumentationFile
+{
+ // For site navigation TOC references, the path_prefix IS the URL
+ // We don't append the wrapped node's URL
+ public string Url
+ {
+ get
+ {
+ var url = PathPrefixProvider.PathPrefix.TrimEnd('/');
+ return string.IsNullOrEmpty(url) ? "/" : url;
+ }
+ }
+
+ public string NavigationTitle => wrappedNode.NavigationTitle;
+ public IRootNavigationItem NavigationRoot => wrappedNode.NavigationRoot;
+
+ public INodeNavigationItem? Parent
+ {
+ get => wrappedNode.Parent;
+ set => wrappedNode.Parent = value;
+ }
+
+ public bool Hidden => wrappedNode.Hidden;
+
+ public int NavigationIndex
+ {
+ get => wrappedNode.NavigationIndex;
+ set => wrappedNode.NavigationIndex = value;
+ }
+
+ public bool IsCrossLink => wrappedNode.IsCrossLink;
+ public int Depth => wrappedNode.Depth;
+ public string Id => wrappedNode.Id;
+ public ILeafNavigationItem Index => wrappedNode.Index;
+
+ // Override to return the specified children from site navigation
+ // Wrap children to apply path prefix recursively - but don't wrap children that are
+ // already SiteTableOfContentsNavigation (they have their own path prefix)
+ public IReadOnlyCollection NavigationItems { get; } = children;
+
+ ///
+ public IPathPrefixProvider PathPrefixProvider { get; set; } = pathPrefixProvider;
+}
+
diff --git a/src/Elastic.Documentation.Navigation/Elastic.Documentation.Navigation.csproj b/src/Elastic.Documentation.Navigation/Elastic.Documentation.Navigation.csproj
new file mode 100644
index 000000000..5448edb21
--- /dev/null
+++ b/src/Elastic.Documentation.Navigation/Elastic.Documentation.Navigation.csproj
@@ -0,0 +1,13 @@
+
+
+
+ net9.0
+ enable
+ enable
+
+
+
+
+
+
+
diff --git a/src/Elastic.Documentation.Site/Navigation/INavigationItem.cs b/src/Elastic.Documentation.Navigation/INavigationItem.cs
similarity index 96%
rename from src/Elastic.Documentation.Site/Navigation/INavigationItem.cs
rename to src/Elastic.Documentation.Navigation/INavigationItem.cs
index 4ec006872..430726252 100644
--- a/src/Elastic.Documentation.Site/Navigation/INavigationItem.cs
+++ b/src/Elastic.Documentation.Navigation/INavigationItem.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
-namespace Elastic.Documentation.Site.Navigation;
+namespace Elastic.Documentation.Navigation;
/// Represents navigation model data for documentation elements.
public interface INavigationModel
@@ -63,7 +63,7 @@ public interface INodeNavigationItem : INaviga
string Id { get; }
/// Gets the index model associated with this node.
- TIndex Index { get; }
+ ILeafNavigationItem Index { get; }
///
/// Gets the collection of child navigation items.
diff --git a/src/Elastic.Documentation.Navigation/Isolated/CrossLinkNavigationLeaf.cs b/src/Elastic.Documentation.Navigation/Isolated/CrossLinkNavigationLeaf.cs
new file mode 100644
index 000000000..04779e6a9
--- /dev/null
+++ b/src/Elastic.Documentation.Navigation/Isolated/CrossLinkNavigationLeaf.cs
@@ -0,0 +1,41 @@
+// 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.Navigation.Isolated;
+
+public record CrossLinkModel(Uri CrossLinkUri, string NavigationTitle) : IDocumentationFile;
+
+public class CrossLinkNavigationLeaf(
+ CrossLinkModel model,
+ string url,
+ bool hidden,
+ INodeNavigationItem? parent,
+ IRootNavigationItem navigationRoot
+) : ILeafNavigationItem
+{
+ ///
+ public CrossLinkModel Model { get; init; } = model;
+
+ ///
+ public string Url { get; init; } = url;
+
+ ///
+ public bool Hidden { get; init; } = hidden;
+
+ ///
+ public IRootNavigationItem NavigationRoot { get; init; } = navigationRoot;
+
+ ///
+ public INodeNavigationItem? Parent { get; set; } = parent;
+
+ ///
+ public string NavigationTitle => Model.NavigationTitle;
+
+ ///
+ public int NavigationIndex { get; set; }
+
+ ///
+ public bool IsCrossLink => true;
+
+}
diff --git a/src/Elastic.Documentation.Navigation/Isolated/DocumentationSetNavigation.cs b/src/Elastic.Documentation.Navigation/Isolated/DocumentationSetNavigation.cs
new file mode 100644
index 000000000..3b78df7e1
--- /dev/null
+++ b/src/Elastic.Documentation.Navigation/Isolated/DocumentationSetNavigation.cs
@@ -0,0 +1,648 @@
+// 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.DocSet;
+using Elastic.Documentation.Extensions;
+
+namespace Elastic.Documentation.Navigation.Isolated;
+
+// A model for nodes in the navigation representing directories e.g., sets, toc's and folders.
+public interface IDocumentationFileFactory where TModel : IDocumentationFile
+{
+ TModel? TryCreateDocumentationFile(IFileInfo path, IFileSystem readFileSystem);
+}
+
+public static class DocumentationNavigationFactory
+{
+ public static ILeafNavigationItem CreateFileNavigationLeaf(TModel model, IFileInfo fileInfo, FileNavigationArgs args)
+ where TModel : IDocumentationFile =>
+ new FileNavigationLeaf(model, fileInfo, args) { NavigationIndex = args.NavigationIndex };
+
+ public static INodeNavigationItem CreateVirtualFileNavigation(TModel model, IFileInfo fileInfo,
+ VirtualFileNavigationArgs args)
+ where TModel : IDocumentationFile =>
+ new VirtualFileNavigation(model, fileInfo, args) { NavigationIndex = args.NavigationIndex };
+}
+
+public interface IDocumentationSetNavigation : IRootNavigationItem
+{
+ IReadOnlyDictionary> TableOfContentNodes { get; }
+}
+
+public class DocumentationSetNavigation
+ : IDocumentationSetNavigation, INavigationPathPrefixProvider, IPathPrefixProvider
+
+ where TModel : IDocumentationFile
+{
+ private readonly IDocumentationFileFactory _factory;
+
+ public DocumentationSetNavigation(
+ DocumentationSetFile documentationSet,
+ IDocumentationSetContext context,
+ IDocumentationFileFactory factory,
+ IRootNavigationItem? parent = null,
+ IRootNavigationItem? root = null,
+ IPathPrefixProvider? pathPrefixProvider = null
+ )
+ {
+ _factory = factory;
+ // Initialize root properties
+ NavigationRoot = root ?? this;
+ Parent = parent;
+ Depth = 0;
+ Hidden = false;
+ IsCrossLink = false;
+ PathPrefixProvider = pathPrefixProvider ?? this;
+ _pathPrefix = pathPrefixProvider?.PathPrefix ?? string.Empty;
+ Id = ShortId.Create(documentationSet.Project ?? "root");
+ IsUsingNavigationDropdown = documentationSet.Features.PrimaryNav ?? false;
+ Git = context.Git;
+ Identifier = new Uri($"{Git.RepositoryName}://");
+ _ = _tableOfContentNodes.TryAdd(Identifier, this);
+
+ // Convert TOC items to navigation items
+ var items = new List();
+ var index = -1;
+ foreach (var tocItem in documentationSet.Toc)
+ {
+ var navItem = ConvertToNavigationItem(
+ tocItem,
+ index++,
+ context,
+ parent: null,
+ root: NavigationRoot,
+ prefixProvider: PathPrefixProvider,
+ depth: Depth,
+ parentPath: ""
+ );
+
+ if (navItem != null)
+ items.Add(navItem);
+ }
+
+ NavigationItems = items;
+ _ = this.UpdateNavigationIndex(context);
+ Index = this.FindIndex(new NotFoundModel($"{PathPrefix}/index.md"));
+
+ }
+
+ private readonly string _pathPrefix;
+
+ ///
+ /// Gets the path prefix. When PathPrefixProvider is set to a different instance, returns that provider's prefix.
+ /// Otherwise returns the prefix set during construction.
+ ///
+ public string PathPrefix => PathPrefixProvider == this ? _pathPrefix : PathPrefixProvider.PathPrefix;
+
+ public IPathPrefixProvider PathPrefixProvider { get; set; }
+
+ public GitCheckoutInformation Git { get; }
+
+ private readonly Dictionary> _tableOfContentNodes = [];
+ public IReadOnlyDictionary> TableOfContentNodes => _tableOfContentNodes;
+
+ public Uri Identifier { get; }
+
+ ///
+ public string Url
+ {
+ get
+ {
+ var rootUrl = PathPrefixProvider.PathPrefix.TrimEnd('/');
+ return string.IsNullOrEmpty(rootUrl) ? "/" : rootUrl;
+ }
+ }
+
+ ///
+ public string NavigationTitle => Index.NavigationTitle;
+
+ ///
+ public IRootNavigationItem NavigationRoot { get; }
+
+ ///
+ public INodeNavigationItem? Parent { get; set; }
+
+ ///
+ public bool Hidden { get; }
+
+ ///
+ public int NavigationIndex { get; set; }
+
+ ///
+ public bool IsCrossLink { get; }
+
+ ///
+ public int Depth { get; }
+
+ ///
+ public string Id { get; }
+
+ ///
+ public ILeafNavigationItem Index { get; }
+
+ ///
+ public bool IsUsingNavigationDropdown { get; }
+
+ ///
+ public IReadOnlyCollection NavigationItems { get; }
+
+ private INavigationItem? ConvertToNavigationItem(
+ ITableOfContentsItem tocItem,
+ int index,
+ IDocumentationSetContext context,
+ INodeNavigationItem? parent,
+ IRootNavigationItem root,
+ IPathPrefixProvider prefixProvider,
+ int depth,
+ string parentPath
+ ) =>
+ tocItem switch
+ {
+ FileRef fileRef => CreateFileNavigation(fileRef, index, context, parent, root, prefixProvider, parentPath),
+ CrossLinkRef crossLinkRef => CreateCrossLinkNavigation(crossLinkRef, index, parent, root),
+ FolderRef folderRef => CreateFolderNavigation(folderRef, index, context, parent, root, prefixProvider, depth, parentPath),
+ IsolatedTableOfContentsRef tocRef => CreateTocNavigation(tocRef, index, context, parent, root, prefixProvider, depth, parentPath),
+ _ => null
+ };
+
+ #region CreateFileNavigation Helper Methods
+
+ ///
+ /// Creates a temporary file navigation placeholder used during construction before children are processed.
+ /// This is distinct from the factory method to make it clear this is a temporary instance.
+ ///
+ private INodeNavigationItem CreateTemporaryFileNavigation(
+ TModel documentationFile,
+ IFileInfo fileInfo,
+ string fullPath,
+ bool hidden,
+ int index,
+ INodeNavigationItem? parent,
+ IRootNavigationItem root,
+ IPathPrefixProvider prefixProvider)
+ {
+ var virtualFileNavigationArgs = new VirtualFileNavigationArgs(fullPath, hidden, index, 0, parent, root, prefixProvider, []);
+ return new VirtualFileNavigation(documentationFile, fileInfo, virtualFileNavigationArgs);
+ }
+
+ ///
+ /// Resolves the relative path for URL generation, handling parent path and deeplinked paths.
+ ///
+ private static string ResolveFileRelativePath(string fileRefPath, string parentPath)
+ {
+ if (string.IsNullOrEmpty(parentPath))
+ return fileRefPath;
+
+ // Extract parent's directory (everything before the last /)
+ var parentDir = parentPath.Contains('/')
+ ? parentPath[..parentPath.LastIndexOf('/')]
+ : "";
+
+ // Extract child's directory from fileRef.Path
+ var childDir = fileRefPath.Contains('/')
+ ? fileRefPath[..fileRefPath.LastIndexOf('/')]
+ : "";
+
+ // Check for deeplinked paths where child's directory is already in parent path
+ // Case 1: parentDir ends with childDir (e.g., parentPath="guides/clients/getting-started", childDir="clients")
+ // Case 2: parentPath ends with childDir (e.g., parentPath="guides/clients", childDir="clients")
+ return !string.IsNullOrEmpty(childDir) &&
+ (parentDir == childDir || parentDir.EndsWith($"/{childDir}", StringComparison.Ordinal) ||
+ parentPath.EndsWith(childDir, StringComparison.Ordinal))
+ ? fileRefPath[(childDir.Length + 1)..] // Strip child's directory from path
+ : fileRefPath.StartsWith($"{parentPath}/", StringComparison.Ordinal)
+ ? fileRefPath[(parentPath.Length + 1)..] // If file path starts with parent path, extract just the relative part
+ : fileRefPath;
+ }
+
+ ///
+ /// Combines parent path with relative path to create the full file path.
+ ///
+ private static string CreateFullFilePath(string relativePathForUrl, string parentPath) =>
+ string.IsNullOrEmpty(parentPath)
+ ? relativePathForUrl
+ : $"{parentPath}/{relativePathForUrl}";
+
+ ///
+ /// Resolves the file info based on the context and prefix provider.
+ ///
+ private static IFileInfo ResolveFileInfo(
+ IDocumentationSetContext context,
+ IPathPrefixProvider prefixProvider,
+ string relativePathForUrl,
+ string fullPath)
+ {
+ var fs = context.ReadFileSystem;
+
+ // When inside a TOC, files are relative to the TOC directory, not the parent path
+ // Check both actual TableOfContentsNavigation and temporary placeholders
+ var tocDirectory = prefixProvider switch
+ {
+ TableOfContentsNavigation toc => toc.TableOfContentsDirectory,
+ TemporaryNavigationPlaceholder placeholder => placeholder.TableOfContentsDirectory,
+ _ => null
+ };
+
+ if (tocDirectory != null)
+ {
+ // For TOC children, use the TOC directory as the base
+ return fs.FileInfo.New(fs.Path.Combine(tocDirectory.FullName, relativePathForUrl));
+ }
+
+ // For other files, use the documentation source directory + full path
+ return fs.FileInfo.New(fs.Path.Combine(context.DocumentationSourceDirectory.FullName, fullPath));
+ }
+
+ ///
+ /// Creates the documentation file from the factory, emitting an error if creation fails.
+ ///
+ private TModel? CreateDocumentationFile(
+ IFileInfo fileInfo,
+ IFileSystem fileSystem,
+ IDocumentationSetContext context,
+ string fullPath)
+ {
+ var documentationFile = _factory.TryCreateDocumentationFile(fileInfo, fileSystem);
+ if (documentationFile == null)
+ context.EmitError(context.ConfigurationPath, $"File navigation '{fullPath}' could not be created");
+
+ return documentationFile;
+ }
+
+ ///
+ /// Computes the parent path for children by removing .md extension and /index suffix if present.
+ ///
+ private static string DetermineParentPathForChildren(string fullPath)
+ {
+ // Remove .md extension
+ var parentPathForChildren = fullPath.EndsWith(".md", StringComparison.OrdinalIgnoreCase)
+ ? fullPath[..^3]
+ : fullPath;
+
+ // If this is an index file, also remove the /index suffix for children's parent path
+ if (parentPathForChildren.EndsWith("/index", StringComparison.OrdinalIgnoreCase))
+ parentPathForChildren = parentPathForChildren[..^6]; // Remove "/index"
+
+ return parentPathForChildren;
+ }
+
+ ///
+ /// Processes children recursively and returns the list of navigation items.
+ ///
+ private List ProcessFileChildren(
+ FileRef fileRef,
+ IDocumentationSetContext context,
+ INodeNavigationItem tempFileNavigation,
+ IRootNavigationItem root,
+ IPathPrefixProvider prefixProvider,
+ string parentPathForChildren)
+ {
+ var children = new List();
+ var childIndex = 0;
+
+ foreach (var child in fileRef.Children)
+ {
+ var childNav = ConvertToNavigationItem(
+ child, childIndex++, context,
+ (INodeNavigationItem)tempFileNavigation, root,
+ prefixProvider, // Files don't change the URL root
+ 0, // Depth will be set by child
+ parentPathForChildren
+ );
+ if (childNav != null)
+ children.Add(childNav);
+ }
+
+ return children;
+ }
+
+ ///
+ /// Ensures the first item in the navigation items is the index file (index.md or the first file in the list).
+ ///
+ private static void EnsureIndexIsFirst(List children)
+ {
+ if (children.Count == 0)
+ return;
+
+ // Find an item named "index" or "index.md"
+ var indexItem = children.FirstOrDefault(c =>
+ c is ILeafNavigationItem leaf &&
+ (leaf.Model.NavigationTitle.Equals("index", StringComparison.OrdinalIgnoreCase) ||
+ (leaf is FileNavigationLeaf fileLeaf &&
+ fileLeaf.FileInfo.Name.Equals("index.md", StringComparison.OrdinalIgnoreCase))));
+
+ // If found and it's not already first, move it to the front
+ if (indexItem != null && children[0] != indexItem)
+ {
+ _ = children.Remove(indexItem);
+ children.Insert(0, indexItem);
+ }
+ }
+
+ ///
+ /// Validates that navigation items has at least one item, emitting an error if not.
+ ///
+ private static void ValidateNavigationItems(
+ List children,
+ IDocumentationSetContext context,
+ string fullPath)
+ {
+ if (children.Count < 1)
+ {
+ context.EmitError(context.ConfigurationPath,
+ $"File navigation '{fullPath}' has children defined but none could be created");
+ }
+ }
+
+ #endregion
+
+ private INavigationItem? CreateFileNavigation(
+ FileRef fileRef,
+ int index,
+ IDocumentationSetContext context,
+ INodeNavigationItem? parent,
+ IRootNavigationItem root,
+ IPathPrefixProvider prefixProvider,
+ string parentPath
+ )
+ {
+ // Resolve paths
+ var relativePathForUrl = ResolveFileRelativePath(fileRef.Path, parentPath);
+ var fullPath = CreateFullFilePath(relativePathForUrl, parentPath);
+
+ // Create file info and documentation file
+ var fileInfo = ResolveFileInfo(context, prefixProvider, relativePathForUrl, fullPath);
+ var documentationFile = CreateDocumentationFile(fileInfo, context.ReadFileSystem, context, fullPath);
+ if (documentationFile == null)
+ return null;
+
+ // Handle leaf case (no children)
+ if (fileRef.Children.Count <= 0)
+ {
+ var leafNavigationArgs = new FileNavigationArgs(fullPath, fileRef.Hidden, index, parent, root, prefixProvider);
+ return DocumentationNavigationFactory.CreateFileNavigationLeaf(documentationFile, fileInfo, leafNavigationArgs);
+ }
+
+ // Create temporary file navigation for children to reference
+ var tempFileNavigation = CreateTemporaryFileNavigation(documentationFile, fileInfo, fullPath, fileRef.Hidden, index, parent, root, prefixProvider);
+
+ // Process children recursively
+ var parentPathForChildren = DetermineParentPathForChildren(fullPath);
+ var children = ProcessFileChildren(fileRef, context, tempFileNavigation, root, prefixProvider, parentPathForChildren);
+
+ // Validate and order children
+ ValidateNavigationItems(children, context, fullPath);
+ EnsureIndexIsFirst(children);
+
+ // Create final file navigation with actual children
+ var virtualFileNavigationArgs = new VirtualFileNavigationArgs(
+ fullPath,
+ fileRef.Hidden,
+ index,
+ parent?.Depth + 1 ?? 0,
+ parent,
+ root,
+ prefixProvider,
+ children
+ );
+
+ var finalFileNavigation = DocumentationNavigationFactory.CreateVirtualFileNavigation(documentationFile, fileInfo, virtualFileNavigationArgs);
+
+ // Update children's Parent to point to the final file navigation
+ foreach (var child in children)
+ child.Parent = (INodeNavigationItem)finalFileNavigation;
+
+ return finalFileNavigation;
+ }
+
+ private INavigationItem CreateCrossLinkNavigation(
+ CrossLinkRef crossLinkRef,
+ int index,
+ INodeNavigationItem? parent,
+ IRootNavigationItem root)
+ {
+ var title = crossLinkRef.Title ?? crossLinkRef.CrossLinkUri.OriginalString;
+ var model = new CrossLinkModel(crossLinkRef.CrossLinkUri, title);
+
+ return new CrossLinkNavigationLeaf(
+ model,
+ crossLinkRef.CrossLinkUri.OriginalString,
+ crossLinkRef.Hidden,
+ parent,
+ root
+ )
+ {
+ NavigationIndex = index
+ };
+ }
+
+ private INavigationItem? CreateFolderNavigation(
+ FolderRef folderRef,
+ int index,
+ IDocumentationSetContext context,
+ INodeNavigationItem? parent,
+ IRootNavigationItem root,
+ IPathPrefixProvider prefixProvider,
+ int depth,
+ string parentPath
+ )
+ {
+ var folderPath = string.IsNullOrEmpty(parentPath)
+ ? folderRef.Path
+ : $"{parentPath}/{folderRef.Path}";
+
+ // Create temporary placeholder for parent reference
+ var children = new List();
+ var childIndex = 0;
+
+ var placeholderNavigation = new TemporaryNavigationPlaceholder(
+ depth + 1,
+ ShortId.Create(folderPath),
+ parent,
+ root,
+ prefixProvider,
+ folderPath
+ );
+
+ foreach (var child in folderRef.Children)
+ {
+ var childNav = ConvertToNavigationItem(
+ child,
+ childIndex++,
+ context,
+ placeholderNavigation,
+ root,
+ prefixProvider, // Keep parent's prefix provider
+ depth + 1,
+ folderPath // Pass folder path for children
+ );
+
+ if (childNav != null)
+ children.Add(childNav);
+ }
+
+ // Validate folders have children
+ if (children.Count == 0)
+ {
+ if (folderRef.Children.Count == 0)
+ context.EmitError(context.ConfigurationPath, $"Folder navigation '{folderPath}' has no children defined");
+ else
+ context.EmitError(context.ConfigurationPath, $"Folder navigation '{folderPath}' has children defined but none could be created");
+ return null;
+ }
+
+ // Create folder navigation with actual children
+ var finalFolderNavigation = new FolderNavigation(depth + 1, folderPath, parent, root, children)
+ {
+ NavigationIndex = index
+ };
+
+ // Update children's Parent to point to the final folder navigation
+ foreach (var child in children)
+ child.Parent = finalFolderNavigation;
+
+ return finalFolderNavigation;
+ }
+
+ private INavigationItem? CreateTocNavigation(
+ IsolatedTableOfContentsRef tocRef,
+ int index,
+ IDocumentationSetContext context,
+ INodeNavigationItem? parent,
+ IRootNavigationItem root,
+ IPathPrefixProvider prefixProvider,
+ int depth,
+ string parentPath
+ )
+ {
+ // Determine the full TOC path for file system operations
+ IDirectoryInfo tocDirectory;
+ string tocPath;
+
+ // Check if parent is a TOC (or placeholder for a TOC being constructed)
+ var parentTocDirectory = parent switch
+ {
+ TableOfContentsNavigation toc => toc.TableOfContentsDirectory,
+ TemporaryNavigationPlaceholder placeholder when placeholder.TableOfContentsDirectory != null => placeholder.TableOfContentsDirectory,
+ _ => null
+ };
+
+ if (parentTocDirectory != null)
+ {
+ // Nested TOC: use parent TOC's directory as base
+ tocDirectory = context.ReadFileSystem.DirectoryInfo.New(
+ context.ReadFileSystem.Path.Combine(parentTocDirectory.FullName, tocRef.Source)
+ );
+ // Extract the relative path from the documentation source directory
+ var fullPath = tocDirectory.FullName;
+ var basePath = context.DocumentationSourceDirectory.FullName.TrimEnd('/');
+ tocPath = fullPath.StartsWith(basePath, StringComparison.Ordinal)
+ ? fullPath[(basePath.Length + 1)..]
+ : tocRef.Source;
+ }
+ else
+ {
+ // Root-level TOC: use parentPath (which comes from folder structure)
+ tocPath = string.IsNullOrEmpty(parentPath)
+ ? tocRef.Source
+ : $"{parentPath}/{tocRef.Source}";
+ tocDirectory = context.ReadFileSystem.DirectoryInfo.New(
+ context.ReadFileSystem.Path.Combine(context.DocumentationSourceDirectory.FullName, tocPath)
+ );
+ }
+
+ // Read and deserialize the toc.yml file
+ var tocFilePath = context.ReadFileSystem.Path.Combine(tocDirectory.FullName, "toc.yml");
+ TableOfContentsFile? tocFile = null;
+
+ if (context.ReadFileSystem.File.Exists(tocFilePath))
+ tocFile = TableOfContentsFile.Deserialize(context.ReadFileSystem.File.ReadAllText(tocFilePath));
+ else
+ context.EmitError(context.ConfigurationPath, $"Table of contents file not found: {tocFilePath}");
+
+ // Create the TOC navigation that will be the parent for children
+ // For TOCs nested under other TOCs, use just the source name since the parent TOC's path is the base
+ // For TOCs at root or under folders, use the full tocPath
+ var navigationParentPath = (parent != null && parentTocDirectory != null) ? tocRef.Source : tocPath;
+
+ var placeholderNavigation = new TemporaryNavigationPlaceholder(
+ depth + 1,
+ ShortId.Create(navigationParentPath),
+ parent,
+ root,
+ prefixProvider,
+ navigationParentPath,
+ tocDirectory
+ );
+
+ // Convert children
+ var children = new List();
+ var childIndex = 0;
+
+ // First, process items from the toc.yml file if it exists
+ if (tocFile != null)
+ {
+ foreach (var child in tocFile.Toc)
+ {
+ var childNav = ConvertToNavigationItem(
+ child,
+ childIndex++,
+ context,
+ placeholderNavigation,
+ root,
+ placeholderNavigation, // Placeholder acts as the new prefix provider for children
+ depth + 1,
+ "" // Reset parentPath since TOC is new prefixProvider - children paths are relative to this TOC
+ );
+
+ if (childNav != null)
+ children.Add(childNav);
+ }
+ }
+
+ // Validate that TOC references should not have children defined in navigation
+ if (tocRef.Children.Count > 0)
+ {
+ context.EmitError(
+ context.ConfigurationPath,
+ $"TableOfContents '{tocRef.Source}' may not contain children, define children in '{tocRef.Source}/toc.yml' instead."
+ );
+ }
+
+ // Validate TOCs have children
+ if (children.Count == 0)
+ {
+ var hasTocFileChildren = tocFile?.Toc.Count > 0;
+ var hasTocRefChildren = tocRef.Children.Count > 0;
+
+ if (!hasTocFileChildren && !hasTocRefChildren)
+ context.EmitError(context.ConfigurationPath, $"Table of contents navigation '{navigationParentPath}' has no children defined");
+ else
+ context.EmitError(context.ConfigurationPath, $"Table of contents navigation '{navigationParentPath}' has children defined but none could be created");
+ return null;
+ }
+
+ var finalTocNavigation = new TableOfContentsNavigation(
+ tocDirectory,
+ depth + 1,
+ navigationParentPath,
+ parent,
+ prefixProvider,
+ children,
+ Git,
+ _tableOfContentNodes
+ )
+ {
+ NavigationIndex = index
+ };
+
+ // Update children's Parent to point to the final TOC navigation
+ foreach (var child in children)
+ child.Parent = finalTocNavigation;
+
+ return finalTocNavigation;
+ }
+
+}
diff --git a/src/Elastic.Documentation.Navigation/Isolated/FileNavigationLeaf.cs b/src/Elastic.Documentation.Navigation/Isolated/FileNavigationLeaf.cs
new file mode 100644
index 000000000..7e386ae11
--- /dev/null
+++ b/src/Elastic.Documentation.Navigation/Isolated/FileNavigationLeaf.cs
@@ -0,0 +1,69 @@
+// 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.Navigation.Isolated;
+
+public record FileNavigationArgs(
+ string RelativePath,
+ bool Hidden,
+ int NavigationIndex,
+ INodeNavigationItem? Parent,
+ IRootNavigationItem NavigationRoot,
+ IPathPrefixProvider PrefixProvider
+);
+
+public class FileNavigationLeaf(TModel model, IFileInfo fileInfo, FileNavigationArgs args)
+ : ILeafNavigationItem
+ where TModel : IDocumentationFile
+{
+ public IFileInfo FileInfo { get; } = fileInfo;
+
+ ///
+ public TModel Model { get; init; } = model;
+
+ ///
+ public string Url
+ {
+ get
+ {
+ var rootUrl = args.PrefixProvider.PathPrefix.TrimEnd('/');
+ // Remove extension while preserving the directory path
+ var relativePath = args.RelativePath;
+ var path = relativePath.EndsWith(".md", StringComparison.OrdinalIgnoreCase)
+ ? relativePath[..^3] // Remove last 3 characters (.md)
+ : relativePath;
+
+ // If a path ends with /index or is just index, omit it from the URL
+ if (path.EndsWith("/index", StringComparison.OrdinalIgnoreCase))
+ path = path[..^6]; // Remove "/index"
+ else if (path.Equals("index", StringComparison.OrdinalIgnoreCase))
+ return string.IsNullOrEmpty(rootUrl) ? "/" : rootUrl;
+
+ if (string.IsNullOrEmpty(path))
+ return string.IsNullOrEmpty(rootUrl) ? "/" : rootUrl;
+
+ return $"{rootUrl}/{path}";
+ }
+ }
+
+ ///
+ public bool Hidden { get; init; } = args.Hidden;
+
+ ///
+ public IRootNavigationItem NavigationRoot { get; init; } = args.NavigationRoot;
+
+ ///
+ public INodeNavigationItem? Parent { get; set; } = args.Parent;
+
+ ///
+ public string NavigationTitle => Model.NavigationTitle;
+
+ ///
+ public int NavigationIndex { get; set; }
+
+ ///
+ public bool IsCrossLink { get; }
+}
diff --git a/src/Elastic.Documentation.Navigation/Isolated/FolderNavigation.cs b/src/Elastic.Documentation.Navigation/Isolated/FolderNavigation.cs
new file mode 100644
index 000000000..355710555
--- /dev/null
+++ b/src/Elastic.Documentation.Navigation/Isolated/FolderNavigation.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
+
+using Elastic.Documentation.Extensions;
+
+namespace Elastic.Documentation.Navigation.Isolated;
+
+public class FolderNavigation : INodeNavigationItem
+{
+ public FolderNavigation(
+ int depth,
+ string parentPath,
+ INodeNavigationItem? parent,
+ IRootNavigationItem navigationRoot,
+ IReadOnlyCollection navigationItems
+ )
+ {
+ FolderPath = parentPath;
+ NavigationItems = navigationItems;
+ Index = this.FindIndex(new NotFoundModel($"{FolderPath}/index.md"));
+ NavigationRoot = navigationRoot;
+ Parent = parent;
+ Depth = depth;
+ Hidden = false;
+ IsCrossLink = false;
+ Id = ShortId.Create(parentPath);
+ }
+
+ public string FolderPath { get; }
+
+ ///
+ public string Url => Index.Url;
+
+ ///
+ public string NavigationTitle => Index.NavigationTitle;
+
+ ///
+ public IRootNavigationItem NavigationRoot { get; }
+
+ ///
+ public INodeNavigationItem? Parent { get; set; }
+
+ ///
+ public bool Hidden { get; }
+
+ ///
+ public int NavigationIndex { get; set; }
+
+ ///
+ public bool IsCrossLink { get; }
+
+ ///
+ public int Depth { get; }
+
+ ///
+ public string Id { get; }
+
+ ///
+ public ILeafNavigationItem Index { get; }
+
+ public IReadOnlyCollection NavigationItems { get; }
+}
diff --git a/src/Elastic.Documentation.Navigation/Isolated/IPathPrefixProvider.cs b/src/Elastic.Documentation.Navigation/Isolated/IPathPrefixProvider.cs
new file mode 100644
index 000000000..efa35bb05
--- /dev/null
+++ b/src/Elastic.Documentation.Navigation/Isolated/IPathPrefixProvider.cs
@@ -0,0 +1,22 @@
+// 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.Navigation.Isolated;
+
+public interface IPathPrefixProvider
+{
+ string PathPrefix { get; }
+}
+
+public interface INavigationPathPrefixProvider
+{
+ IPathPrefixProvider PathPrefixProvider { get; set; }
+}
+
+public class PathPrefixProvider(string pathPrefix) : IPathPrefixProvider
+{
+ ///
+ public string PathPrefix { get; } = pathPrefix;
+}
+
diff --git a/src/Elastic.Documentation.Navigation/Isolated/TableOfContentsNavigation.cs b/src/Elastic.Documentation.Navigation/Isolated/TableOfContentsNavigation.cs
new file mode 100644
index 000000000..993b43bce
--- /dev/null
+++ b/src/Elastic.Documentation.Navigation/Isolated/TableOfContentsNavigation.cs
@@ -0,0 +1,103 @@
+// 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.Extensions;
+
+namespace Elastic.Documentation.Navigation.Isolated;
+
+public interface IDocumentationFile : INavigationModel
+{
+ string NavigationTitle { get; }
+}
+
+public class TableOfContentsNavigation : IRootNavigationItem
+ , INavigationPathPrefixProvider
+ , IPathPrefixProvider
+{
+ public TableOfContentsNavigation(
+ IDirectoryInfo tableOfContentsDirectory,
+ int depth,
+ string parentPath,
+ INodeNavigationItem? parent,
+ IPathPrefixProvider pathPrefixProvider,
+ IReadOnlyCollection navigationItems,
+ GitCheckoutInformation git,
+ Dictionary> tocNodes
+ )
+ {
+ TableOfContentsDirectory = tableOfContentsDirectory;
+ NavigationItems = navigationItems;
+ Index = this.FindIndex(new NotFoundModel($"{parentPath}/index.md"));
+ Parent = parent;
+ PathPrefixProvider = pathPrefixProvider;
+ NavigationRoot = this;
+ Hidden = false;
+ IsUsingNavigationDropdown = false;
+ IsCrossLink = false;
+ Id = ShortId.Create(parentPath);
+ Depth = depth;
+ ParentPath = parentPath;
+
+ // Create an identifier for this TOC
+ Identifier = new Uri($"{git.RepositoryName}://{parentPath}");
+ _ = tocNodes.TryAdd(Identifier, this);
+ }
+
+ ///
+ /// The composed path prefix for this TOC, which is the parent's prefix + this TOC's parent path.
+ /// This is used by children to build their URLs.
+ ///
+ public string PathPrefix
+ {
+ get
+ {
+ var parentPrefix = PathPrefixProvider.PathPrefix.TrimEnd('/');
+ return string.IsNullOrEmpty(parentPrefix) ? $"/{ParentPath}" : $"{parentPrefix}/{ParentPath}";
+ }
+ }
+
+ ///
+ public string Url => Index.Url;
+
+ ///
+ public string NavigationTitle => Index.NavigationTitle;
+
+ ///
+ public IRootNavigationItem NavigationRoot { get; }
+
+ ///
+ public INodeNavigationItem? Parent { get; set; }
+
+ public IPathPrefixProvider PathPrefixProvider { get; set; }
+
+ ///
+ public bool Hidden { get; }
+
+ ///
+ public int NavigationIndex { get; set; }
+
+ ///
+ public bool IsCrossLink { get; }
+
+ ///
+ public int Depth { get; }
+
+ public string ParentPath { get; }
+
+ ///
+ public string Id { get; }
+
+ ///
+ public ILeafNavigationItem Index { get; }
+
+ ///
+ public bool IsUsingNavigationDropdown { get; }
+
+ public IDirectoryInfo TableOfContentsDirectory { get; }
+
+ public Uri Identifier { get; }
+
+ public IReadOnlyCollection NavigationItems { get; }
+}
diff --git a/src/Elastic.Documentation.Navigation/Isolated/TemporaryNavigationPlaceholder.cs b/src/Elastic.Documentation.Navigation/Isolated/TemporaryNavigationPlaceholder.cs
new file mode 100644
index 000000000..8d23c20ba
--- /dev/null
+++ b/src/Elastic.Documentation.Navigation/Isolated/TemporaryNavigationPlaceholder.cs
@@ -0,0 +1,59 @@
+// 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.Navigation.Isolated;
+
+///
+/// Temporary placeholder used during navigation construction when children need a parent reference
+/// before the final navigation item is created with its children collection.
+/// This placeholder should never appear in the final navigation tree.
+///
+internal sealed class TemporaryNavigationPlaceholder(
+ int depth,
+ string id,
+ INodeNavigationItem? parent,
+ IRootNavigationItem navigationRoot,
+ IPathPrefixProvider pathPrefixProvider,
+ string parentPath,
+ IDirectoryInfo? tocDirectory = null) : INodeNavigationItem, IPathPrefixProvider
+{
+ public int Depth { get; } = depth;
+ public string Id { get; } = id;
+ public INodeNavigationItem? Parent { get; set; } = parent;
+ public IRootNavigationItem NavigationRoot { get; } = navigationRoot;
+
+ ///
+ /// The parent path used for constructing nested paths, matching TableOfContentsNavigation behavior.
+ ///
+ public string ParentPath { get; } = parentPath;
+
+ ///
+ /// When this placeholder represents a TOC, this contains the TOC directory.
+ /// This is needed for resolving file paths relative to the TOC directory.
+ ///
+ public IDirectoryInfo? TableOfContentsDirectory { get; } = tocDirectory;
+
+ ///
+ /// Computes the path prefix for this placeholder, mimicking TableOfContentsNavigation behavior.
+ ///
+ public string PathPrefix
+ {
+ get
+ {
+ var parentPrefix = pathPrefixProvider.PathPrefix.TrimEnd('/');
+ return string.IsNullOrEmpty(parentPrefix) ? $"/{ParentPath}" : $"{parentPrefix}/{ParentPath}";
+ }
+ }
+
+ // Properties that should never be accessed on a placeholder
+ public string Url => throw new InvalidOperationException("TemporaryNavigationPlaceholder should not appear in final navigation");
+ public string NavigationTitle => throw new InvalidOperationException("TemporaryNavigationPlaceholder should not appear in final navigation");
+ public bool Hidden => throw new InvalidOperationException("TemporaryNavigationPlaceholder should not appear in final navigation");
+ public int NavigationIndex { get; set; }
+ public bool IsCrossLink => throw new InvalidOperationException("TemporaryNavigationPlaceholder should not appear in final navigation");
+ public ILeafNavigationItem Index => throw new InvalidOperationException("TemporaryNavigationPlaceholder should not appear in final navigation");
+ public IReadOnlyCollection NavigationItems => throw new InvalidOperationException("TemporaryNavigationPlaceholder should not appear in final navigation");
+}
diff --git a/src/Elastic.Documentation.Navigation/Isolated/VirtualFileNavigation.cs b/src/Elastic.Documentation.Navigation/Isolated/VirtualFileNavigation.cs
new file mode 100644
index 000000000..392d6051a
--- /dev/null
+++ b/src/Elastic.Documentation.Navigation/Isolated/VirtualFileNavigation.cs
@@ -0,0 +1,58 @@
+// 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.Extensions;
+
+namespace Elastic.Documentation.Navigation.Isolated;
+
+public record VirtualFileNavigationArgs(
+ string RelativePath,
+ bool Hidden,
+ int NavigationIndex,
+ int Depth,
+ INodeNavigationItem? Parent,
+ IRootNavigationItem NavigationRoot,
+ IPathPrefixProvider PrefixProvider,
+ IReadOnlyCollection NavigationItems
+);
+
+/// Represents a file navigation item that defines children which are not part of the file tree.
+public class VirtualFileNavigation(TModel model, IFileInfo fileInfo, VirtualFileNavigationArgs args)
+ : INodeNavigationItem
+ where TModel : IDocumentationFile
+{
+ ///
+ public string Url => Index.Url;
+
+ ///
+ public string NavigationTitle => Index.NavigationTitle;
+
+ ///
+ public IRootNavigationItem NavigationRoot { get; init; } = args.NavigationRoot;
+
+ ///
+ public INodeNavigationItem? Parent { get; set; } = args.Parent;
+
+ ///
+ public bool Hidden { get; init; } = args.Hidden;
+
+ ///
+ public int NavigationIndex { get; set; }
+
+ ///
+ public bool IsCrossLink { get; }
+
+ ///
+ public int Depth { get; init; } = args.Depth;
+
+ ///
+ public string Id { get; } = ShortId.Create(args.RelativePath);
+
+ ///
+ public ILeafNavigationItem Index { get; init; } =
+ new FileNavigationLeaf(model, fileInfo, new FileNavigationArgs(args.RelativePath, args.Hidden, args.NavigationIndex, args.Parent, args.NavigationRoot, args.PrefixProvider));
+
+ public IReadOnlyCollection NavigationItems { get; init; } = args.NavigationItems;
+}
diff --git a/src/Elastic.Documentation.Navigation/NavigationItemExtensions.cs b/src/Elastic.Documentation.Navigation/NavigationItemExtensions.cs
new file mode 100644
index 000000000..1119dcc58
--- /dev/null
+++ b/src/Elastic.Documentation.Navigation/NavigationItemExtensions.cs
@@ -0,0 +1,90 @@
+// 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.Navigation.Isolated;
+
+namespace Elastic.Documentation.Navigation;
+
+public record NotFoundModel(string NavigationTitle) : IDocumentationFile;
+
+public class NotFoundLeafNavigationItem(TModel model, INodeNavigationItem parent
+)
+ : ILeafNavigationItem
+ where TModel : IDocumentationFile
+{
+ ///
+ public string Url => string.Empty;
+
+ ///
+ public string NavigationTitle => string.Empty;
+
+ ///
+ public IRootNavigationItem NavigationRoot { get; } = parent.NavigationRoot;
+
+ ///
+ public INodeNavigationItem? Parent { get; set; } = parent;
+
+ ///
+ public bool Hidden { get; }
+
+ ///
+ public int NavigationIndex { get; set; }
+
+ ///
+ public bool IsCrossLink { get; }
+
+ ///
+ public TModel Model { get; } = model;
+}
+
+public static class NavigationItemExtensions
+{
+ public static ILeafNavigationItem FindIndex(this INodeNavigationItem node, TModel fallback)
+ where TModel : IDocumentationFile
+ {
+ var leaf = node.NavigationItems.OfType>().FirstOrDefault();
+ if (leaf is not null)
+ return leaf;
+
+ var nodes = node.NavigationItems.OfType>().ToList();
+ if (nodes.Count == 0)
+ return new NotFoundLeafNavigationItem(fallback, node);
+
+ return nodes.First().Index;
+
+ }
+
+ public static int UpdateNavigationIndex(this IRootNavigationItem node, IDocumentationContext context)
+ {
+ var navigationIndex = -1;
+ ProcessNavigationItem(context, ref navigationIndex, node);
+ return navigationIndex;
+
+ }
+
+ private static void UpdateNavigationIndex(IReadOnlyCollection navigationItems, IDocumentationContext context, ref int navigationIndex)
+ {
+ foreach (var item in navigationItems)
+ ProcessNavigationItem(context, ref navigationIndex, item);
+ }
+
+ private static void ProcessNavigationItem(IDocumentationContext context, ref int navigationIndex, INavigationItem item)
+ {
+ switch (item)
+ {
+ case ILeafNavigationItem leaf:
+ var fileIndex = Interlocked.Increment(ref navigationIndex);
+ leaf.NavigationIndex = fileIndex;
+ break;
+ case INodeNavigationItem node:
+ var groupIndex = Interlocked.Increment(ref navigationIndex);
+ node.NavigationIndex = groupIndex;
+ UpdateNavigationIndex(node.NavigationItems, context, ref navigationIndex);
+ break;
+ default:
+ context.EmitError(context.ConfigurationPath, $"{nameof(UpdateNavigationIndex)}: Unhandled navigation item type: {item.GetType()}");
+ break;
+ }
+ }
+}
diff --git a/src/Elastic.Documentation.Site/Elastic.Documentation.Site.csproj b/src/Elastic.Documentation.Site/Elastic.Documentation.Site.csproj
index c8b68628d..5e230f469 100644
--- a/src/Elastic.Documentation.Site/Elastic.Documentation.Site.csproj
+++ b/src/Elastic.Documentation.Site/Elastic.Documentation.Site.csproj
@@ -15,6 +15,7 @@
+
diff --git a/src/Elastic.Documentation.Site/Navigation/INavigationHtmlWriter.cs b/src/Elastic.Documentation.Site/Navigation/INavigationHtmlWriter.cs
index 3f598bcac..cdb149f3a 100644
--- a/src/Elastic.Documentation.Site/Navigation/INavigationHtmlWriter.cs
+++ b/src/Elastic.Documentation.Site/Navigation/INavigationHtmlWriter.cs
@@ -2,6 +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.Navigation;
using RazorSlices;
namespace Elastic.Documentation.Site.Navigation;
diff --git a/src/Elastic.Documentation.Site/Navigation/IsolatedBuildNavigationHtmlWriter.cs b/src/Elastic.Documentation.Site/Navigation/IsolatedBuildNavigationHtmlWriter.cs
index 198405d18..89bbfd6b8 100644
--- a/src/Elastic.Documentation.Site/Navigation/IsolatedBuildNavigationHtmlWriter.cs
+++ b/src/Elastic.Documentation.Site/Navigation/IsolatedBuildNavigationHtmlWriter.cs
@@ -5,6 +5,7 @@
using System.Collections.Concurrent;
using Elastic.Documentation.Configuration;
using Elastic.Documentation.Extensions;
+using Elastic.Documentation.Navigation;
namespace Elastic.Documentation.Site.Navigation;
diff --git a/src/Elastic.Documentation.Site/Navigation/NavigationTreeItem.cs b/src/Elastic.Documentation.Site/Navigation/NavigationTreeItem.cs
index 8d05df7ca..f5b074d6f 100644
--- a/src/Elastic.Documentation.Site/Navigation/NavigationTreeItem.cs
+++ b/src/Elastic.Documentation.Site/Navigation/NavigationTreeItem.cs
@@ -2,6 +2,8 @@
// 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.Navigation;
+
namespace Elastic.Documentation.Site.Navigation;
public class NavigationTreeItem
diff --git a/src/Elastic.Documentation.Site/Navigation/NavigationViewModel.cs b/src/Elastic.Documentation.Site/Navigation/NavigationViewModel.cs
index 0c21cf7a0..035f69e89 100644
--- a/src/Elastic.Documentation.Site/Navigation/NavigationViewModel.cs
+++ b/src/Elastic.Documentation.Site/Navigation/NavigationViewModel.cs
@@ -2,6 +2,8 @@
// 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.Navigation;
+
namespace Elastic.Documentation.Site.Navigation;
public class NavigationViewModel
diff --git a/src/Elastic.Documentation.Site/Navigation/_TocTreeNav.cshtml b/src/Elastic.Documentation.Site/Navigation/_TocTreeNav.cshtml
index fefab98fa..27a0f8936 100644
--- a/src/Elastic.Documentation.Site/Navigation/_TocTreeNav.cshtml
+++ b/src/Elastic.Documentation.Site/Navigation/_TocTreeNav.cshtml
@@ -1,3 +1,4 @@
+@using Elastic.Documentation.Navigation
@using Elastic.Documentation.Site.Navigation
@inherits RazorSlice
@{
@@ -9,6 +10,11 @@
{
continue;
}
+
+ if (item.Parent is not null && item.Parent.Index == item)
+ {
+ continue;
+ }
if (item is INodeNavigationItem { NavigationItems.Count: 0, Index: not null } group)
{
diff --git a/src/Elastic.Documentation.Site/_ViewModels.cs b/src/Elastic.Documentation.Site/_ViewModels.cs
index 26894bb2a..25b95a962 100644
--- a/src/Elastic.Documentation.Site/_ViewModels.cs
+++ b/src/Elastic.Documentation.Site/_ViewModels.cs
@@ -4,8 +4,8 @@
using Elastic.Documentation.Configuration.Assembler;
using Elastic.Documentation.Configuration.Builder;
+using Elastic.Documentation.Navigation;
using Elastic.Documentation.Site.FileProviders;
-using Elastic.Documentation.Site.Navigation;
namespace Elastic.Documentation.Site;
diff --git a/src/Elastic.Documentation/Extensions/IFileInfoExtensions.cs b/src/Elastic.Documentation/Extensions/IFileInfoExtensions.cs
index 38cf92d35..b49b65f1a 100644
--- a/src/Elastic.Documentation/Extensions/IFileInfoExtensions.cs
+++ b/src/Elastic.Documentation/Extensions/IFileInfoExtensions.cs
@@ -34,6 +34,13 @@ public static bool HasParent(this IFileInfo file, string parentName)
var parent = file.Directory;
return parent is not null && parent.HasParent(parentName);
}
+
+ public static IFileInfo NewCombine(this IFileInfoFactory fileInfo, params string[] paths)
+ {
+ paths = paths.Select(f => f.OptionalWindowsReplace()).ToArray();
+ var fi = fileInfo.New(Path.Combine(paths));
+ return fi;
+ }
}
public static class IDirectoryInfoExtensions
@@ -95,4 +102,20 @@ public static bool HasParent(this IDirectoryInfo directory, string parentName, S
return false;
}
+
+ /// Gets the first , parent of
+ public static IDirectoryInfo? GetParent(this IDirectoryInfo directory, string parentName, StringComparison comparison = OrdinalIgnoreCase)
+ {
+ if (string.Equals(directory.Name, parentName, comparison))
+ return directory;
+ var parent = directory;
+ do
+ {
+ if (string.Equals(parent.Name, parentName, comparison))
+ return parent;
+ parent = parent.Parent;
+ } while (parent != null);
+
+ return null;
+ }
}
diff --git a/src/Elastic.Documentation/Extensions/StringExtensions.cs b/src/Elastic.Documentation/Extensions/StringExtensions.cs
new file mode 100644
index 000000000..5a5daf85c
--- /dev/null
+++ b/src/Elastic.Documentation/Extensions/StringExtensions.cs
@@ -0,0 +1,17 @@
+// 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.Runtime.InteropServices;
+
+namespace Elastic.Documentation.Extensions;
+
+public static class StringExtensions
+{
+ public static string OptionalWindowsReplace(this string relativePath)
+ {
+ if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+ relativePath = relativePath.Replace('\\', '/');
+ return relativePath;
+ }
+}
diff --git a/src/Elastic.Documentation/GitCheckoutInformation.cs b/src/Elastic.Documentation/GitCheckoutInformation.cs
index 729377bb3..aa0dda3b7 100644
--- a/src/Elastic.Documentation/GitCheckoutInformation.cs
+++ b/src/Elastic.Documentation/GitCheckoutInformation.cs
@@ -5,6 +5,7 @@
using System.IO.Abstractions;
using System.Text.Json.Serialization;
using System.Text.RegularExpressions;
+using Elastic.Documentation.Extensions;
using Microsoft.Extensions.Logging;
using SoftCircuits.IniFileParser;
@@ -48,23 +49,39 @@ public static GitCheckoutInformation Create(IDirectoryInfo? source, IFileSystem
RepositoryName = "docs-builder"
};
}
-
var fakeRef = Guid.NewGuid().ToString()[..16];
- var gitConfig = Git(source, Path.Combine(".git", "config"));
+
+ var gitDir = GitDir(source, ".git");
+ if (!gitDir.Exists)
+ {
+ // try a worktree .git file
+ var worktreeFile = Git(source, ".git");
+ if (!worktreeFile.Exists)
+ return Unavailable;
+ var workTreePath = Read(source, ".git")?.Replace("gitdir: ", string.Empty);
+ if (workTreePath is null)
+ return Unavailable;
+ //TODO read branch info from worktree do not fall through
+ gitDir = fileSystem.DirectoryInfo.New(workTreePath).GetParent(".git");
+ if (gitDir is null || !gitDir.Exists)
+ return Unavailable;
+ }
+
+ var gitConfig = Git(gitDir, "config");
if (!gitConfig.Exists)
{
logger?.LogInformation("Git checkout information not available.");
return Unavailable;
}
- var head = Read(source, Path.Combine(".git", "HEAD")) ?? fakeRef;
+ var head = Read(gitDir, "HEAD") ?? fakeRef;
var gitRef = head;
var branch = head.Replace("refs/heads/", string.Empty);
//not detached HEAD
if (head.StartsWith("ref:", StringComparison.OrdinalIgnoreCase))
{
head = head.Replace("ref: ", string.Empty);
- gitRef = Read(source, Path.Combine(".git", head)) ?? fakeRef;
+ gitRef = Read(gitDir, head) ?? fakeRef;
branch = branch.Replace("ref: ", string.Empty);
}
else
@@ -117,6 +134,9 @@ public static GitCheckoutInformation Create(IDirectoryInfo? source, IFileSystem
IFileInfo Git(IDirectoryInfo directoryInfo, string path) =>
fileSystem.FileInfo.New(Path.Combine(directoryInfo.FullName, path));
+ IDirectoryInfo GitDir(IDirectoryInfo directoryInfo, string path) =>
+ fileSystem.DirectoryInfo.New(Path.Combine(directoryInfo.FullName, path));
+
string? Read(IDirectoryInfo directoryInfo, string path)
{
var gitPath = Git(directoryInfo, path).FullName;
diff --git a/src/Elastic.Documentation/IDocumentationContext.cs b/src/Elastic.Documentation/IDocumentationContext.cs
index f21e8568c..1dd420537 100644
--- a/src/Elastic.Documentation/IDocumentationContext.cs
+++ b/src/Elastic.Documentation/IDocumentationContext.cs
@@ -13,21 +13,21 @@ public interface IDocumentationContext
IFileSystem ReadFileSystem { get; }
IFileSystem WriteFileSystem { get; }
IDirectoryInfo OutputDirectory { get; }
+ IFileInfo ConfigurationPath { get; }
}
public interface IDocumentationSetContext : IDocumentationContext
{
IDirectoryInfo DocumentationSourceDirectory { get; }
GitCheckoutInformation Git { get; }
- IFileInfo ConfigurationPath { get; }
}
public static class DocumentationContextExtensions
{
- public static void EmitError(this IDocumentationSetContext context, IFileInfo file, string message, Exception? e = null) =>
+ public static void EmitError(this IDocumentationContext context, IFileInfo file, string message, Exception? e = null) =>
context.Collector.EmitError(file, message, e);
- public static void EmitWarning(this IDocumentationSetContext context, IFileInfo file, string message) =>
+ public static void EmitWarning(this IDocumentationContext context, IFileInfo file, string message) =>
context.Collector.EmitWarning(file, message);
}
diff --git a/src/Elastic.Markdown/DocumentationGenerator.cs b/src/Elastic.Markdown/DocumentationGenerator.cs
index 97e7a1db8..ef7c06be2 100644
--- a/src/Elastic.Markdown/DocumentationGenerator.cs
+++ b/src/Elastic.Markdown/DocumentationGenerator.cs
@@ -48,7 +48,6 @@ public class DocumentationGenerator
public DocumentationSet DocumentationSet { get; }
public BuildContext Context { get; }
- public ICrossLinkResolver CrossLinkResolver { get; }
public IMarkdownStringRenderer MarkdownStringRenderer => HtmlWriter;
public DocumentationGenerator(
@@ -58,8 +57,7 @@ public DocumentationGenerator(
IDocumentationFileOutputProvider? documentationFileOutputProvider = null,
IMarkdownExporter[]? markdownExporters = null,
IConversionCollector? conversionCollector = null,
- ILegacyUrlMapper? legacyUrlMapper = null,
- IPositionalNavigation? positionalNavigation = null
+ ILegacyUrlMapper? legacyUrlMapper = null
)
{
_markdownExporters = markdownExporters ?? [];
@@ -70,9 +68,7 @@ public DocumentationGenerator(
DocumentationSet = docSet;
Context = docSet.Context;
- CrossLinkResolver = docSet.CrossLinkResolver;
- HtmlWriter = new HtmlWriter(DocumentationSet, _writeFileSystem, new DescriptionGenerator(), navigationHtmlWriter, legacyUrlMapper,
- positionalNavigation);
+ HtmlWriter = new HtmlWriter(DocumentationSet, _writeFileSystem, new DescriptionGenerator(), navigationHtmlWriter, legacyUrlMapper);
_documentationFileExporter =
docSet.Context.AvailableExporters.Contains(Exporter.Html)
? docSet.EnabledExtensions.FirstOrDefault(e => e.FileExporter != null)?.FileExporter
@@ -155,9 +151,10 @@ private async Task ProcessDocumentationFiles(HashSet offendingFiles, Dat
await Parallel.ForEachAsync(DocumentationSet.Files, ctx, async (file, token) =>
{
var processedFiles = Interlocked.Increment(ref processedFileCount);
+ var (fp, doc) = file;
try
{
- await ProcessFile(offendingFiles, file, outputSeenChanges, token);
+ await ProcessFile(offendingFiles, doc, outputSeenChanges, token);
}
catch (Exception e)
{
@@ -165,7 +162,7 @@ await Parallel.ForEachAsync(DocumentationSet.Files, ctx, async (file, token) =>
// this is not the main error logging mechanism
// if we hit this from too many files fail hard
if (currentCount <= 25)
- Context.Collector.EmitError(file.RelativePath, "Uncaught exception while processing file", e);
+ Context.Collector.EmitError(fp.RelativePath, "Uncaught exception while processing file", e);
else
throw;
}
@@ -252,7 +249,8 @@ private async Task ProcessFile(HashSet offendingFiles, DocumentationFile
{
foreach (var exporter in _markdownExporters)
{
- var document = context.MarkdownDocument ??= await markdown.ParseFullAsync(ctx);
+ var document = context.MarkdownDocument ??= await markdown.ParseFullAsync(DocumentationSet.TryFindDocumentByRelativePath, ctx);
+ var navigationItem = DocumentationSet.FindNavigationByMarkdown(markdown);
_ = await exporter.ExportAsync(new MarkdownExportFileContext
{
BuildContext = Context,
@@ -260,7 +258,8 @@ private async Task ProcessFile(HashSet offendingFiles, DocumentationFile
Document = document,
SourceFile = markdown,
DefaultOutputFile = outputFile,
- DocumentationSet = DocumentationSet
+ DocumentationSet = DocumentationSet,
+ NavigationItem = navigationItem
}, ctx);
}
}
@@ -349,14 +348,14 @@ private async Task GenerateDocumentationState(Cancel ctx)
public async Task RenderLlmMarkdown(MarkdownFile markdown, Cancel ctx)
{
- await DocumentationSet.Tree.Resolve(ctx);
- var document = await markdown.ParseFullAsync(ctx);
+ await DocumentationSet.ResolveDirectoryTree(ctx);
+ var document = await markdown.ParseFullAsync(DocumentationSet.TryFindDocumentByRelativePath, ctx);
return LlmMarkdownExporter.ConvertToLlmMarkdown(document, DocumentationSet.Context);
}
public async Task RenderLayout(MarkdownFile markdown, Cancel ctx)
{
- await DocumentationSet.Tree.Resolve(ctx);
+ await DocumentationSet.ResolveDirectoryTree(ctx);
return await HtmlWriter.RenderLayout(markdown, ctx);
}
}
diff --git a/src/Elastic.Markdown/Elastic.Markdown.csproj b/src/Elastic.Markdown/Elastic.Markdown.csproj
index 9d5529c8d..117d86c3f 100644
--- a/src/Elastic.Markdown/Elastic.Markdown.csproj
+++ b/src/Elastic.Markdown/Elastic.Markdown.csproj
@@ -18,6 +18,7 @@
+
@@ -37,6 +38,7 @@
+
diff --git a/src/Elastic.Markdown/Exporters/ElasticsearchMarkdownExporter.cs b/src/Elastic.Markdown/Exporters/ElasticsearchMarkdownExporter.cs
index b3747212d..1353ad404 100644
--- a/src/Elastic.Markdown/Exporters/ElasticsearchMarkdownExporter.cs
+++ b/src/Elastic.Markdown/Exporters/ElasticsearchMarkdownExporter.cs
@@ -256,7 +256,7 @@ private async ValueTask TryWrite(DocumentationDocument document, Cancel ct
public async ValueTask ExportAsync(MarkdownExportFileContext fileContext, Cancel ctx)
{
var file = fileContext.SourceFile;
- var url = file.Url;
+ var url = fileContext.NavigationItem.Url;
if (url is "/docs" or "/docs/404")
{
diff --git a/src/Elastic.Markdown/Exporters/IMarkdownExporter.cs b/src/Elastic.Markdown/Exporters/IMarkdownExporter.cs
index 492df8ced..8df5c6133 100644
--- a/src/Elastic.Markdown/Exporters/IMarkdownExporter.cs
+++ b/src/Elastic.Markdown/Exporters/IMarkdownExporter.cs
@@ -4,6 +4,7 @@
using System.IO.Abstractions;
using Elastic.Documentation.Configuration;
+using Elastic.Documentation.Navigation;
using Elastic.Markdown.IO;
using Elastic.Markdown.Myst;
using Markdig.Syntax;
@@ -19,6 +20,7 @@ public record MarkdownExportFileContext
public required IFileInfo DefaultOutputFile { get; init; }
public string? LLMText { get; set; }
public required DocumentationSet DocumentationSet { get; init; }
+ public required INavigationItem NavigationItem { get; init; }
}
public interface IMarkdownExporter
diff --git a/src/Elastic.Markdown/Exporters/LlmMarkdownExporter.cs b/src/Elastic.Markdown/Exporters/LlmMarkdownExporter.cs
index 365e1286f..c337f285b 100644
--- a/src/Elastic.Markdown/Exporters/LlmMarkdownExporter.cs
+++ b/src/Elastic.Markdown/Exporters/LlmMarkdownExporter.cs
@@ -19,26 +19,26 @@ public class LlmMarkdownExporter : IMarkdownExporter
{
private const string LlmsTxtTemplate = """
# Elastic Documentation
-
+
> Elastic provides an open source search, analytics, and AI platform, and out-of-the-box solutions for observability and security. The Search AI platform combines the power of search and generative AI to provide near real-time search and analysis with relevance to reduce your time to value.
>
>Elastic offers the following solutions or types of projects:
>
- >* [Elasticsearch](https://www.elastic.co/docs/solutions/search): Build powerful search and RAG applications using Elasticsearch's vector database, AI toolkit, and advanced retrieval capabilities.
+ >* [Elasticsearch](https://www.elastic.co/docs/solutions/search): Build powerful search and RAG applications using Elasticsearch's vector database, AI toolkit, and advanced retrieval capabilities.
>* [Elastic Observability](https://www.elastic.co/docs/solutions/observability): Gain comprehensive visibility into applications, infrastructure, and user experience through logs, metrics, traces, and other telemetry data, all in a single interface.
>* [Elastic Security](https://www.elastic.co/docs/solutions/security): Combine SIEM, endpoint security, and cloud security to provide comprehensive tools for threat detection and prevention, investigation, and response.
-
+
The documentation is organized to guide you through your journey with Elastic, from learning the basics to deploying and managing complex solutions. Here is a detailed breakdown of the documentation structure:
-
- * [**Elastic fundamentals**](https://www.elastic.co/docs/get-started): Understand the basics about the deployment options, platform, and solutions, and features of the documentation.
- * [**Solutions and use cases**](https://www.elastic.co/docs/solutions): Learn use cases, evaluate, and implement Elastic's solutions: Observability, Search, and Security.
- * [**Manage data**](https://www.elastic.co/docs/manage-data): Learn about data store primitives, ingestion and enrichment, managing the data lifecycle, and migrating data.
- * [**Explore and analyze**](https://www.elastic.co/docs/explore-analyze): Get value from data through querying, visualization, machine learning, and alerting.
- * [**Deploy and manage**](https://www.elastic.co/docs/deploy-manage): Deploy and manage production-ready clusters. Covers deployment options and maintenance tasks.
- * [**Manage your Cloud account**](https://www.elastic.co/docs/cloud-account): A dedicated section for user-facing cloud account tasks like resetting passwords.
- * [**Troubleshoot**](https://www.elastic.co/docs/troubleshoot): Identify and resolve problems.
- * [**Extend and contribute**](https://www.elastic.co/docs/extend): How to contribute to or integrate with Elastic, from open source to plugins to integrations.
- * [**Release notes**](https://www.elastic.co/docs/release-notes): Contains release notes and changelogs for each new release.
+
+ * [**Elastic fundamentals**](https://www.elastic.co/docs/get-started): Understand the basics about the deployment options, platform, and solutions, and features of the documentation.
+ * [**Solutions and use cases**](https://www.elastic.co/docs/solutions): Learn use cases, evaluate, and implement Elastic's solutions: Observability, Search, and Security.
+ * [**Manage data**](https://www.elastic.co/docs/manage-data): Learn about data store primitives, ingestion and enrichment, managing the data lifecycle, and migrating data.
+ * [**Explore and analyze**](https://www.elastic.co/docs/explore-analyze): Get value from data through querying, visualization, machine learning, and alerting.
+ * [**Deploy and manage**](https://www.elastic.co/docs/deploy-manage): Deploy and manage production-ready clusters. Covers deployment options and maintenance tasks.
+ * [**Manage your Cloud account**](https://www.elastic.co/docs/cloud-account): A dedicated section for user-facing cloud account tasks like resetting passwords.
+ * [**Troubleshoot**](https://www.elastic.co/docs/troubleshoot): Identify and resolve problems.
+ * [**Extend and contribute**](https://www.elastic.co/docs/extend): How to contribute to or integrate with Elastic, from open source to plugins to integrations.
+ * [**Release notes**](https://www.elastic.co/docs/release-notes): Contains release notes and changelogs for each new release.
* [**Reference**](https://www.elastic.co/docs/reference): Reference material for core tasks and manuals for optional products.
""";
@@ -145,8 +145,7 @@ private string CreateLlmContentWithMetadata(MarkdownExportFileContext context, s
_ = metadata.AppendLine($"description: {generateDescription}");
}
- if (!string.IsNullOrEmpty(sourceFile.Url))
- _ = metadata.AppendLine($"url: {context.BuildContext.CanonicalBaseUrl?.Scheme}://{context.BuildContext.CanonicalBaseUrl?.Host}{sourceFile.Url}");
+ _ = metadata.AppendLine($"url: {context.BuildContext.CanonicalBaseUrl?.Scheme}://{context.BuildContext.CanonicalBaseUrl?.Host}{context.NavigationItem.Url}");
var pageProducts = GetPageProducts(sourceFile.YamlFrontMatter?.Products);
if (pageProducts.Count > 0)
diff --git a/src/Elastic.Markdown/Extensions/DetectionRules/DetectionRuleFile.cs b/src/Elastic.Markdown/Extensions/DetectionRules/DetectionRuleFile.cs
index babc2258a..3dab100bc 100644
--- a/src/Elastic.Markdown/Extensions/DetectionRules/DetectionRuleFile.cs
+++ b/src/Elastic.Markdown/Extensions/DetectionRules/DetectionRuleFile.cs
@@ -14,8 +14,8 @@ namespace Elastic.Markdown.Extensions.DetectionRules;
public record DetectionRuleOverviewFile : MarkdownFile
{
- public DetectionRuleOverviewFile(IFileInfo sourceFile, IDirectoryInfo rootPath, MarkdownParser parser, BuildContext build, DocumentationSet set)
- : base(sourceFile, rootPath, parser, build, set)
+ public DetectionRuleOverviewFile(IFileInfo sourceFile, IDirectoryInfo rootPath, MarkdownParser parser, BuildContext build)
+ : base(sourceFile, rootPath, parser, build)
{
}
@@ -68,7 +68,8 @@ private string GetMarkdown()
""";
foreach (var r in group.OrderBy(r => r.Rule.Name))
{
- var url = Files[r.RelativePath].Url;
+ // TODO update this to use the new URL from navigation
+ var url = "does-not-exist-yet";
markdown +=
$"""
[{r.Rule.Name}](!{url})
@@ -96,9 +97,8 @@ public DetectionRuleFile(
IFileInfo sourceFile,
IDirectoryInfo rootPath,
MarkdownParser parser,
- BuildContext build,
- DocumentationSet set
- ) : base(sourceFile, rootPath, parser, build, set)
+ BuildContext build
+ ) : base(sourceFile, rootPath, parser, build)
{
RuleSourceMarkdownPath = SourcePath(sourceFile, build);
LinkReferenceRelativePath = Path.GetRelativePath(build.DocumentationSourceDirectory.FullName, RuleSourceMarkdownPath.FullName);
@@ -121,7 +121,7 @@ public static IFileInfo OutputPath(IFileInfo rulePath, BuildContext build)
return rulePath.FileSystem.FileInfo.New(newPath);
}
- protected override string RelativePathUrl => RelativePath.AsSpan().TrimStart("../").ToString();
+ //protected override string RelativePathUrl => RelativePath.AsSpan().TrimStart("../").ToString();
protected override Task GetMinimalParseDocumentAsync(Cancel ctx)
{
diff --git a/src/Elastic.Markdown/Extensions/DetectionRules/DetectionRulesDocsBuilderExtension.cs b/src/Elastic.Markdown/Extensions/DetectionRules/DetectionRulesDocsBuilderExtension.cs
index ee3257e75..859fdb0f0 100644
--- a/src/Elastic.Markdown/Extensions/DetectionRules/DetectionRulesDocsBuilderExtension.cs
+++ b/src/Elastic.Markdown/Extensions/DetectionRules/DetectionRulesDocsBuilderExtension.cs
@@ -8,6 +8,8 @@
using Elastic.Documentation.Configuration.TableOfContents;
using Elastic.Markdown.Exporters;
using Elastic.Markdown.IO;
+using Elastic.Markdown.IO.NewNavigation;
+using Elastic.Markdown.Myst;
namespace Elastic.Markdown.Extensions.DetectionRules;
@@ -37,26 +39,27 @@ public void Visit(DocumentationFile file, ITocItem tocItem)
}
}
- public DocumentationFile? CreateDocumentationFile(IFileInfo file, DocumentationSet documentationSet)
+ public DocumentationFile? CreateDocumentationFile(IFileInfo file, MarkdownParser markdownParser)
{
if (file.Extension != ".toml")
return null;
- return new DetectionRuleFile(file, Build.DocumentationSourceDirectory, documentationSet.MarkdownParser, Build, documentationSet);
+ return new DetectionRuleFile(file, Build.DocumentationSourceDirectory, markdownParser, Build);
}
- public MarkdownFile? CreateMarkdownFile(IFileInfo file, IDirectoryInfo sourceDirectory, DocumentationSet documentationSet) =>
+ public MarkdownFile? CreateMarkdownFile(IFileInfo file, IDirectoryInfo sourceDirectory, MarkdownParser markdownParser) =>
file.Name == "index.md"
- ? new DetectionRuleOverviewFile(file, sourceDirectory, documentationSet.MarkdownParser, Build, documentationSet)
+ ? new DetectionRuleOverviewFile(file, sourceDirectory, markdownParser, Build)
: null;
public bool TryGetDocumentationFileBySlug(DocumentationSet documentationSet, string slug, out DocumentationFile? documentationFile)
{
var tomlFile = $"../{slug}.toml";
- return documentationSet.FlatMappedFiles.TryGetValue(tomlFile, out documentationFile);
+ var filePath = new FilePath(tomlFile, Build.DocumentationSourceDirectory);
+ return documentationSet.Files.TryGetValue(filePath, out documentationFile);
}
- public IReadOnlyCollection ScanDocumentationFiles(
+ public IReadOnlyCollection<(IFileInfo, DocumentationFile)> ScanDocumentationFiles(
Func defaultFileHandling
)
{
@@ -69,7 +72,7 @@ Func defaultFileHandling
return rules.Select(r =>
{
var file = Build.ReadFileSystem.FileInfo.New(Path.Combine(sourceDirectory.FullName, r.RelativePath));
- return defaultFileHandling(file, sourceDirectory);
+ return (file, defaultFileHandling(file, sourceDirectory));
}).ToArray();
}
diff --git a/src/Elastic.Markdown/Extensions/IDocsBuilderExtension.cs b/src/Elastic.Markdown/Extensions/IDocsBuilderExtension.cs
index d5c4fd7d5..0a9f4e667 100644
--- a/src/Elastic.Markdown/Extensions/IDocsBuilderExtension.cs
+++ b/src/Elastic.Markdown/Extensions/IDocsBuilderExtension.cs
@@ -6,6 +6,7 @@
using Elastic.Documentation.Configuration.TableOfContents;
using Elastic.Markdown.Exporters;
using Elastic.Markdown.IO;
+using Elastic.Markdown.Myst;
namespace Elastic.Markdown.Extensions;
@@ -18,13 +19,13 @@ public interface IDocsBuilderExtension
/// Create an instance of if it matches the .
/// Return `null` to let another extension handle this.
- DocumentationFile? CreateDocumentationFile(IFileInfo file, DocumentationSet documentationSet);
+ DocumentationFile? CreateDocumentationFile(IFileInfo file, MarkdownParser markdownParser);
/// Attempts to locate a documentation file by slug, used to locate the document for `docs-builder serve` command
bool TryGetDocumentationFileBySlug(DocumentationSet documentationSet, string slug, out DocumentationFile? documentationFile);
/// Allows the extension to discover more documentation files for
- IReadOnlyCollection ScanDocumentationFiles(Func defaultFileHandling);
+ IReadOnlyCollection<(IFileInfo, DocumentationFile)> ScanDocumentationFiles(Func defaultFileHandling);
- MarkdownFile? CreateMarkdownFile(IFileInfo file, IDirectoryInfo sourceDirectory, DocumentationSet documentationSet);
+ MarkdownFile? CreateMarkdownFile(IFileInfo file, IDirectoryInfo sourceDirectory, MarkdownParser markdownParser);
}
diff --git a/src/Elastic.Markdown/HtmlWriter.cs b/src/Elastic.Markdown/HtmlWriter.cs
index 804d12e01..0654e8d05 100644
--- a/src/Elastic.Markdown/HtmlWriter.cs
+++ b/src/Elastic.Markdown/HtmlWriter.cs
@@ -8,6 +8,7 @@
using Elastic.Documentation.Configuration.LegacyUrlMappings;
using Elastic.Documentation.Configuration.Products;
using Elastic.Documentation.Configuration.Versions;
+using Elastic.Documentation.Navigation;
using Elastic.Documentation.Site.FileProviders;
using Elastic.Documentation.Site.Navigation;
using Elastic.Markdown.Extensions.DetectionRules;
@@ -32,7 +33,7 @@ public class HtmlWriter(
private DocumentationSet DocumentationSet { get; } = documentationSet;
private INavigationHtmlWriter NavigationHtmlWriter { get; } =
- navigationHtmlWriter ?? new IsolatedBuildNavigationHtmlWriter(documentationSet.Context, documentationSet.Tree);
+ navigationHtmlWriter ?? new IsolatedBuildNavigationHtmlWriter(documentationSet.Context, documentationSet.Navigation);
private StaticFileContentHashProvider StaticFileContentHashProvider { get; } = new(new EmbeddedOrPhysicalFileProvider(documentationSet.Context));
private ILegacyUrlMapper LegacyUrlMapper { get; } = legacyUrlMapper ?? new NoopLegacyUrlMapper();
@@ -48,17 +49,20 @@ public string Render(string markdown, IFileInfo? source)
public async Task RenderLayout(MarkdownFile markdown, Cancel ctx = default)
{
- var document = await markdown.ParseFullAsync(ctx);
+ var document = await markdown.ParseFullAsync(DocumentationSet.TryFindDocumentByRelativePath, ctx);
return await RenderLayout(markdown, document, ctx);
}
private async Task RenderLayout(MarkdownFile markdown, MarkdownDocument document, Cancel ctx = default)
{
var html = MarkdownFile.CreateHtml(document);
- await DocumentationSet.Tree.Resolve(ctx);
+ await DocumentationSet.ResolveDirectoryTree(ctx);
+ var navigationItem = DocumentationSet.FindNavigationByMarkdown(markdown);
- var fullNavigationRenderResult = await NavigationHtmlWriter.RenderNavigation(markdown.NavigationRoot, INavigationHtmlWriter.AllLevels, ctx);
- var miniNavigationRenderResult = await NavigationHtmlWriter.RenderNavigation(markdown.NavigationRoot, 1, ctx);
+ var root = navigationItem.NavigationRoot;
+
+ var fullNavigationRenderResult = await NavigationHtmlWriter.RenderNavigation(root, INavigationHtmlWriter.AllLevels, ctx);
+ var miniNavigationRenderResult = await NavigationHtmlWriter.RenderNavigation(root, 1, ctx);
var navigationHtmlRenderResult = DocumentationSet.Context.Configuration.Features.LazyLoadNavigation
? miniNavigationRenderResult
@@ -81,18 +85,19 @@ private async Task RenderLayout(MarkdownFile markdown, MarkdownDoc
Uri? reportLinkParameter = null;
if (DocumentationSet.Context.CanonicalBaseUrl is not null)
- reportLinkParameter = new Uri(DocumentationSet.Context.CanonicalBaseUrl, Path.Combine(DocumentationSet.Context.UrlPathPrefix ?? string.Empty, markdown.Url));
+ reportLinkParameter = new Uri(DocumentationSet.Context.CanonicalBaseUrl, Path.Combine(DocumentationSet.Context.UrlPathPrefix ?? string.Empty, current.Url));
var reportUrl = $"https://github.com/elastic/docs-content/issues/new?template=issue-report.yaml&link={reportLinkParameter}&labels=source:web";
- var siteName = DocumentationSet.Tree.Index.Title ?? "Elastic Documentation";
+ var siteName = DocumentationSet.Navigation.NavigationTitle;
var legacyPages = LegacyUrlMapper.MapLegacyUrl(markdown.YamlFrontMatter?.MappedPages);
var pageProducts = GetPageProducts(markdown.YamlFrontMatter?.Products);
string? allVersionsUrl = null;
- if (PositionalNavigation.MarkdownNavigationLookup.TryGetValue("docs-content://versions.md", out var item))
- allVersionsUrl = item.Url;
+ // TODO exposese allversions again
+ //if (PositionalNavigation.MarkdownNavigationLookup.TryGetValue("docs-content://versions.md", out var item))
+ // allVersionsUrl = item.Url;
var navigationFileName = $"{fullNavigationRenderResult.Id}.nav.html";
@@ -131,7 +136,7 @@ private async Task RenderLayout(MarkdownFile markdown, MarkdownDoc
UrlPathPrefix = markdown.UrlPathPrefix,
AppliesTo = markdown.YamlFrontMatter?.AppliesTo,
GithubEditUrl = editUrl,
- MarkdownUrl = markdown.Url.TrimEnd('/') + ".md",
+ MarkdownUrl = current.Url.TrimEnd('/') + ".md",
AllowIndexing = DocumentationSet.Context.AllowIndexing && (markdown.CrossLink.Equals("docs-content://index.md", StringComparison.OrdinalIgnoreCase) || markdown is DetectionRuleFile || !current.Hidden),
CanonicalBaseUrl = DocumentationSet.Context.CanonicalBaseUrl,
GoogleTagManager = DocumentationSet.Context.GoogleTagManager,
@@ -203,7 +208,7 @@ public async Task WriteAsync(IDirectoryInfo outBaseDir, IFileI
: Path.Combine(dir, "index.html");
}
- var document = await markdown.ParseFullAsync(ctx);
+ var document = await markdown.ParseFullAsync(DocumentationSet.TryFindDocumentByRelativePath, ctx);
var rendered = await RenderLayout(markdown, document, ctx);
collector?.Collect(markdown, document, rendered.Html);
diff --git a/src/Elastic.Markdown/IO/DocumentationFile.cs b/src/Elastic.Markdown/IO/DocumentationFile.cs
index a4d53b89e..7b0ecd50a 100644
--- a/src/Elastic.Markdown/IO/DocumentationFile.cs
+++ b/src/Elastic.Markdown/IO/DocumentationFile.cs
@@ -1,7 +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.Collections.Frozen;
using System.IO.Abstractions;
+using Elastic.Documentation.Diagnostics;
using Elastic.Markdown.Myst;
using Elastic.Markdown.Myst.FrontMatter;
@@ -44,7 +47,8 @@ public record SnippetFile(IFileInfo SourceFile, IDirectoryInfo RootPath, string
private bool _parsed;
public SnippetAnchors? GetAnchors(
- DocumentationSet set,
+ IDiagnosticsCollector collector,
+ Func documentationFileLookup,
MarkdownParser parser,
YamlFrontMatter? frontMatter
)
@@ -58,7 +62,7 @@ public record SnippetFile(IFileInfo SourceFile, IDirectoryInfo RootPath, string
}
var document = parser.MinimalParseAsync(SourceFile, default).GetAwaiter().GetResult();
- var toc = MarkdownFile.GetAnchors(set, parser, frontMatter, document, new Dictionary(), out var anchors);
+ var toc = MarkdownFile.GetAnchors(collector, documentationFileLookup, parser, frontMatter, document, new Dictionary(), out var anchors);
Anchors = new SnippetAnchors(anchors, toc);
_parsed = true;
return Anchors;
diff --git a/src/Elastic.Markdown/IO/DocumentationSet.cs b/src/Elastic.Markdown/IO/DocumentationSet.cs
index 303f637fa..acb88a53f 100644
--- a/src/Elastic.Markdown/IO/DocumentationSet.cs
+++ b/src/Elastic.Markdown/IO/DocumentationSet.cs
@@ -5,101 +5,25 @@
using System.Collections.Concurrent;
using System.Collections.Frozen;
using System.IO.Abstractions;
+using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using Elastic.Documentation;
using Elastic.Documentation.Configuration;
using Elastic.Documentation.Configuration.Builder;
-using Elastic.Documentation.Configuration.TableOfContents;
using Elastic.Documentation.Links;
using Elastic.Documentation.Links.CrossLinks;
+using Elastic.Documentation.Navigation;
+using Elastic.Documentation.Navigation.Isolated;
using Elastic.Documentation.Site.Navigation;
using Elastic.Markdown.Extensions;
using Elastic.Markdown.Extensions.DetectionRules;
-using Elastic.Markdown.IO.Navigation;
+using Elastic.Markdown.IO.NewNavigation;
using Elastic.Markdown.Myst;
using Microsoft.Extensions.Logging;
namespace Elastic.Markdown.IO;
-public interface INavigationLookups
-{
- FrozenDictionary FlatMappedFiles { get; }
- IReadOnlyCollection TableOfContents { get; }
- IReadOnlyCollection EnabledExtensions { get; }
- FrozenDictionary FilesGroupedByFolder { get; }
- ICrossLinkResolver CrossLinkResolver { get; }
-}
-
-public interface IPositionalNavigation
-{
- FrozenDictionary MarkdownNavigationLookup { get; }
- FrozenDictionary NavigationIndexedByOrder { get; }
-
- INavigationItem? GetPrevious(MarkdownFile current)
- {
- if (!MarkdownNavigationLookup.TryGetValue(current.CrossLink, out var currentNavigation))
- return null;
- var index = currentNavigation.NavigationIndex;
- do
- {
- var previous = NavigationIndexedByOrder.GetValueOrDefault(index - 1);
- if (previous is not null && !previous.Hidden)
- return previous;
- index--;
- } while (index > 0);
-
- return null;
- }
-
- INavigationItem? GetNext(MarkdownFile current)
- {
- if (!MarkdownNavigationLookup.TryGetValue(current.CrossLink, out var currentNavigation))
- return null;
- var index = currentNavigation.NavigationIndex;
- do
- {
- var next = NavigationIndexedByOrder.GetValueOrDefault(index + 1);
- if (next is not null && !next.Hidden && next.Url != currentNavigation.Url)
- return next;
- index++;
- } while (index <= NavigationIndexedByOrder.Count - 1);
-
- return null;
- }
-
- INavigationItem GetCurrent(MarkdownFile file) =>
- MarkdownNavigationLookup.GetValueOrDefault(file.CrossLink) ?? throw new InvalidOperationException($"Could not find {file.CrossLink} in navigation");
-
- INavigationItem[] GetParents(INavigationItem current)
- {
- var parents = new List();
- var parent = current.Parent;
- do
- {
- if (parent is null)
- continue;
- if (parents.All(i => i.Url != parent.Url))
- parents.Add(parent);
-
- parent = parent.Parent;
- } while (parent != null);
-
- return [.. parents];
- }
- INavigationItem[] GetParentsOfMarkdownFile(MarkdownFile file) =>
- MarkdownNavigationLookup.TryGetValue(file.CrossLink, out var navigationItem) ? GetParents(navigationItem) : [];
-}
-
-public record NavigationLookups : INavigationLookups
-{
- public required FrozenDictionary FlatMappedFiles { get; init; }
- public required IReadOnlyCollection TableOfContents { get; init; }
- public required IReadOnlyCollection EnabledExtensions { get; init; }
- public required FrozenDictionary FilesGroupedByFolder { get; init; }
- public required ICrossLinkResolver CrossLinkResolver { get; init; }
-}
-
-public class DocumentationSet : INavigationLookups, IPositionalNavigation
+public class DocumentationSet : IPositionalNavigation
{
private readonly ILogger _logger;
public BuildContext Context { get; }
@@ -118,19 +42,9 @@ public class DocumentationSet : INavigationLookups, IPositionalNavigation
public ICrossLinkResolver CrossLinkResolver { get; }
- public TableOfContentsTree Tree { get; }
-
- public Uri Source { get; }
-
- public IReadOnlyCollection