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 [![ci](https://github.com/elastic/docs-builder/actions/workflows/ci.yml/badge.svg?branch=main&event=push)](https://github.com/elastic/docs-builder/actions/workflows/ci.yml) diff --git a/docs-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/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..6aead3a94 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; 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..89a984875 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; 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/ConfigurationFileProvider.cs b/src/Elastic.Documentation.Configuration/ConfigurationFileProvider.cs index 7b6f5c03b..6cf2bdcc0 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; @@ -20,7 +21,10 @@ public partial class ConfigurationFileProvider private readonly ILogger _logger; 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..4572f0a06 --- /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")] + public bool? PrimaryNav { get; set; } +} + +public class TableOfContents : List +{ + public TableOfContents() { } + + public TableOfContents(IEnumerable items) : base(items) { } +} + + +public interface ITableOfContentsItem; + +public record FileRef(string RelativePath, bool Hidden, IReadOnlyCollection Children) + : ITableOfContentsItem; + +public record IndexFileRef(string RelativePath, bool Hidden, IReadOnlyCollection Children) + : FileRef(RelativePath, Hidden, Children); + +public record CrossLinkRef(Uri CrossLinkUri, string? Title, bool Hidden, IReadOnlyCollection Children) + : ITableOfContentsItem; + +public record FolderRef(string RelativePath, 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/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..df1d5e9bd 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 @@ -51,6 +53,7 @@ private static SemVersion ToSemVersion(string semVer) internal sealed record VersionsConfigDto { + [YamlMember(Alias = "versioning_systems")] public Dictionary VersioningSystems { get; set; } = []; } diff --git a/src/Elastic.Documentation.Navigation/Assembler/AssembledNavigation.cs b/src/Elastic.Documentation.Navigation/Assembler/AssembledNavigation.cs new file mode 100644 index 000000000..4afbc74e7 --- /dev/null +++ b/src/Elastic.Documentation.Navigation/Assembler/AssembledNavigation.cs @@ -0,0 +1,217 @@ +// 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 System.Runtime.CompilerServices; +using Elastic.Documentation.Configuration.Assembler; +using Elastic.Documentation.Configuration.DocSet; +using Elastic.Documentation.Extensions; +using Elastic.Documentation.Navigation.Isolated; + +namespace Elastic.Documentation.Navigation.Assembler; + + +public record SiteModel(string NavigationTitle) : INavigationModel; + +public class SiteNavigation : IRootNavigationItem +{ + public SiteNavigation( + SiteNavigationFile siteNavigationFile, + IDocumentationSetContext context, + IReadOnlyCollection documentationSetNavigations + ) + { + // Initialize root properties + NavigationRoot = this; + Parent = null; + Depth = 0; + Hidden = false; + IsCrossLink = false; + Id = ShortId.Create("site"); + Index = new SiteModel("Site Navigation"); + IsUsingNavigationDropdown = false; + _nodes = []; + foreach (var setNavigation in documentationSetNavigations) + { + foreach (var (identifier, node) in setNavigation.TableOfContentNodes) + { + if (_nodes.ContainsKey(identifier)) + { + //TODO configurationFileProvider navigation path + context.EmitError(context.ConfigurationPath, $"Duplicate navigation identifier: {identifier} in navigation.yml"); + continue; + } + _nodes.Add(identifier, node); + } + } + + // 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; + } + + private readonly Dictionary> _nodes; + public IReadOnlyDictionary> Nodes => _nodes; + + /// + 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 SiteModel Index { get; } + + /// + public bool IsUsingNavigationDropdown { get; } + + /// + public IReadOnlyCollection NavigationItems { get; } + + private INavigationItem? CreateSiteTableOfContentsNavigation( + SiteTableOfContentsRef tocRef, + int index, + IDocumentationSetContext context + ) + { + // Validate that path_prefix is set + if (string.IsNullOrWhiteSpace(tocRef.PathPrefix)) + { + context.EmitError(context.ConfigurationPath, $"path_prefix is required for TOC reference: {tocRef.Source}"); + return null; + } + + // 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(tocRef.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. +/// +internal sealed class SiteTableOfContentsNavigation( + INodeNavigationItem wrappedNode, + IPathPrefixProvider pathPrefixProvider, + IReadOnlyCollection children + ) : INodeNavigationItem, INavigationPathPrefixProvider +{ + // 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 INavigationModel 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 98% rename from src/Elastic.Documentation.Site/Navigation/INavigationItem.cs rename to src/Elastic.Documentation.Navigation/INavigationItem.cs index 4ec006872..d16ab0cb6 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 diff --git a/src/Elastic.Documentation.Navigation/Isolated/TableOfContentsNavigation.cs b/src/Elastic.Documentation.Navigation/Isolated/TableOfContentsNavigation.cs new file mode 100644 index 000000000..4dcaf0388 --- /dev/null +++ b/src/Elastic.Documentation.Navigation/Isolated/TableOfContentsNavigation.cs @@ -0,0 +1,799 @@ +// 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.ComponentModel.Design; +using System.IO.Abstractions; +using Elastic.Documentation.Configuration.DocSet; +using Elastic.Documentation.Extensions; + +namespace Elastic.Documentation.Navigation.Isolated; + +public interface IDocumentationFile : INavigationModel +{ + string NavigationTitle { get; } +} + +public interface IPathPrefixProvider +{ + string PathPrefix { get; } +} + +public interface INavigationPathPrefixProvider +{ + IPathPrefixProvider PathPrefixProvider { get; set; } +} + +public class PathPrefixProvider(string pathPrefix) : IPathPrefixProvider +{ + /// + public string PathPrefix { get; } = pathPrefix; +} + +public record DocumentationDirectory(string NavigationTitle) : IDocumentationFile; + +public class DocumentationSetNavigation : + IRootNavigationItem, INavigationPathPrefixProvider, IPathPrefixProvider +{ + public DocumentationSetNavigation( + DocumentationSetFile documentationSet, + IDocumentationSetContext context, + IRootNavigationItem? parent = null, + IRootNavigationItem? root = null, + IPathPrefixProvider? pathPrefixProvider = null + ) + { + // 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"); + Index = new DocumentationDirectory(documentationSet.Project ?? "Documentation"); + 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 = 0; + 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; + } + + 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 virtual 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 IDocumentationFile 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 + }; + + private INavigationItem CreateFileNavigation( + FileRef fileRef, + int index, + IDocumentationSetContext context, + INodeNavigationItem? parent, + IRootNavigationItem root, + IPathPrefixProvider prefixProvider, + string parentPath + ) + { + // Extract title from file path + var title = context.ReadFileSystem.Path.GetFileNameWithoutExtension(fileRef.RelativePath); + + // Combine parent path with file path + var fullPath = string.IsNullOrEmpty(parentPath) + ? fileRef.RelativePath + : $"{parentPath}/{fileRef.RelativePath}"; + + // Create model + var model = new CrossLinkModel(new Uri(fileRef.RelativePath, UriKind.Relative), title); + + // Check if file has children + if (fileRef.Children.Count > 0) + { + // Validate: index files may not have children + if (fileRef is IndexFileRef) + { + context.EmitError(context.ConfigurationPath, + $"File navigation '{fileRef.RelativePath}' is an index file and may not have children"); + // Return a leaf to prevent further errors + return new FileNavigationLeaf(model, fullPath, fileRef.Hidden, parent, root, prefixProvider) + { + NavigationIndex = index + }; + } + + // Create temporary file navigation for children to reference + var tempFileNavigation = new FileNavigation(model, fullPath, fileRef.Hidden, 0, parent, root, prefixProvider, []); + + // Process children recursively + var children = new List(); + var childIndex = 0; + foreach (var child in fileRef.Children) + { + var childNav = ConvertToNavigationItem( + child, childIndex++, context, + tempFileNavigation, root, + prefixProvider, // Files don't change the URL root + 0, // Depth will be set by child + fullPath.EndsWith(".md", StringComparison.OrdinalIgnoreCase) + ? fullPath[..^3] // Remove .md extension for children's parent path + : fullPath + ); + if (childNav != null) + children.Add(childNav); + } + + // Create final file navigation with actual children + var finalFileNavigation = new FileNavigation( + model, fullPath, fileRef.Hidden, + parent?.Depth + 1 ?? 0, + parent, root, prefixProvider, children) + { + NavigationIndex = index + }; + + // Update children's Parent to point to the final file navigation + foreach (var child in children) + child.Parent = finalFileNavigation; + + return finalFileNavigation; + } + + // No children - return a leaf + return new FileNavigationLeaf(model, fullPath, fileRef.Hidden, parent, root, prefixProvider) + { + NavigationIndex = index + }; + } + + 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.RelativePath + : $"{parentPath}/{folderRef.RelativePath}"; + + // Create temporary folder navigation for parent reference + var children = new List(); + var childIndex = 0; + + var folderNavigation = new FolderNavigation( + depth + 1, + folderPath, + parent, + root, + prefixProvider, + [] + ); + + foreach (var child in folderRef.Children) + { + var childNav = ConvertToNavigationItem( + child, + childIndex++, + context, + folderNavigation, + root, + prefixProvider, // Folders don't change the URL root + depth + 1, + folderPath + ); + + if (childNav != null) + children.Add(childNav); + } + + // Create folder navigation with actual children + var finalFolderNavigation = new FolderNavigation(depth + 1, folderPath, parent, root, prefixProvider, 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 static DocumentationSetNavigation GetDocumentationSetRoot(IRootNavigationItem root) + { + // Walk up the tree to find the DocumentationSetNavigation root + var current = root; + while (current is TableOfContentsNavigation toc && toc.Parent is IRootNavigationItem parentRoot) + current = parentRoot; + + if (current is DocumentationSetNavigation docSetNav) + return docSetNav; + + throw new InvalidOperationException("Could not find DocumentationSetNavigation root in navigation tree"); + } + + 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 + string tocPath; + if (parent is TableOfContentsNavigation parentToc) + { + // Nested TOC: use parent TOC's path as base + tocPath = $"{parentToc.ParentPath}/{tocRef.Source}"; + } + else + { + // Root-level TOC: use parentPath (which comes from folder structure) + tocPath = string.IsNullOrEmpty(parentPath) + ? tocRef.Source + : $"{parentPath}/{tocRef.Source}"; + } + + // Resolve the TOC directory + var 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 nested TOCs, use just the source name as parentPath since prefixProvider handles the full path + // For root-level TOCs, use the full tocPath + var navigationParentPath = parent is TableOfContentsNavigation ? tocRef.Source : tocPath; + + var tocNavigation = new TableOfContentsNavigation( + tocDirectory, + depth + 1, + navigationParentPath, + parent, + prefixProvider, + [], + Git, + _tableOfContentNodes + ); + + // 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, + tocNavigation, + root, + tocNavigation, // TOC navigation becomes the new URL root for its children + depth + 1, + "" // Reset parentPath since TOC is new prefixProvider - children paths are relative to this TOC + ); + + if (childNav != null) + children.Add(childNav); + } + } + + // Then, process items from tocRef.Children + // In DocumentationSetFile, TOC references can only have other TOC references as children + foreach (var child in tocRef.Children) + { + // Validate that TOC children are only other TOC references + if (child is not IsolatedTableOfContentsRef) + { + context.EmitError( + context.ConfigurationPath, + $"TableOfContents navigation does not allow nested children, found: {child.GetType().Name}" + ); + continue; + } + + var childNav = ConvertToNavigationItem( + child, + childIndex++, + context, + tocNavigation, + root, + tocNavigation, // TOC navigation becomes the new URL root for its children + depth + 1, + "" // Reset parentPath since TOC is new prefixProvider - children paths are relative to this TOC + ); + + if (childNav != null) + children.Add(childNav); + } + + // If no children, add a placeholder + if (children.Count == 0) + { + var placeholderModel = new CrossLinkModel(new Uri(tocRef.Source, UriKind.Relative), tocRef.Source); + children.Add(new FileNavigationLeaf(placeholderModel, tocRef.Source, false, tocNavigation, root, tocNavigation)); + } + + 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; + } + +} + +public class FolderNavigation : INodeNavigationItem +{ + private readonly string _folderPath; + private readonly IPathPrefixProvider _pathPrefixProvider; + + public FolderNavigation( + int depth, + string parentPath, + INodeNavigationItem? parent, + IRootNavigationItem navigationRoot, + IPathPrefixProvider pathPrefixProvider, + IReadOnlyCollection navigationItems + ) + { + _folderPath = parentPath; + _pathPrefixProvider = pathPrefixProvider; + NavigationItems = navigationItems; + NavigationRoot = navigationRoot; + Parent = parent; + var title = navigationItems.FirstOrDefault()?.NavigationTitle ?? parentPath; + Index = new DocumentationDirectory(title); + Depth = depth; + Hidden = false; + IsCrossLink = false; + Id = ShortId.Create(parentPath); + } + + /// + public string Url + { + get + { + // Check if there's an index file among the children + var hasIndexChild = NavigationItems.Any(item => + item is FileNavigationLeaf && + item.NavigationTitle.Equals("index", StringComparison.OrdinalIgnoreCase)); + + // If no index child exists, use the first child's URL + if (!hasIndexChild && NavigationItems.Count > 0) + return NavigationItems.First().Url; + + // Otherwise, use the folder path + var rootUrl = _pathPrefixProvider.PathPrefix.TrimEnd('/'); + return string.IsNullOrEmpty(rootUrl) ? $"/{_folderPath}" : $"{rootUrl}/{_folderPath}"; + } + } + + /// + 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 IDocumentationFile Index { get; } + + public IReadOnlyCollection NavigationItems { get; } +} + +public class FileNavigation( + CrossLinkModel model, + string relativePath, + bool hidden, + int depth, + INodeNavigationItem? parent, + IRootNavigationItem navigationRoot, + IPathPrefixProvider prefixProvider, + IReadOnlyCollection navigationItems +) : INodeNavigationItem +{ + /// + public string Url + { + get + { + var rootUrl = prefixProvider.PathPrefix.TrimEnd('/'); + // Remove extension while preserving directory path + var path = relativePath.EndsWith(".md", StringComparison.OrdinalIgnoreCase) + ? relativePath[..^3] // Remove last 3 characters (.md) + : relativePath; + + // If 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 string NavigationTitle => Index.NavigationTitle; + + /// + public IRootNavigationItem NavigationRoot { get; init; } = navigationRoot; + + /// + public INodeNavigationItem? Parent { get; set; } = parent; + + /// + public bool Hidden { get; init; } = hidden; + + /// + public int NavigationIndex { get; set; } + + /// + public bool IsCrossLink { get; } + + /// + public int Depth { get; init; } = depth; + + /// + public string Id { get; } = ShortId.Create(relativePath); + + /// + public IDocumentationFile Index { get; init; } = model; + + public IReadOnlyCollection NavigationItems { get; init; } = navigationItems; +} + +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; + Parent = parent; + PathPrefixProvider = pathPrefixProvider; + var title = navigationItems.FirstOrDefault()?.NavigationTitle ?? parentPath; + Index = new DocumentationDirectory(title); + NavigationRoot = this; + Hidden = false; + IsUsingNavigationDropdown = false; + IsCrossLink = false; + Id = ShortId.Create(parentPath); + Depth = depth; + ParentPath = parentPath; + + // Create 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 virtual string Url => PathPrefix; + + /// + 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 IDocumentationFile Index { get; } + + /// + public bool IsUsingNavigationDropdown { get; } + + public IDirectoryInfo TableOfContentsDirectory { get; } + + public Uri Identifier { get; } + + public IReadOnlyCollection NavigationItems { get; } +} + +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; + +} + +public class FileNavigationLeaf( + CrossLinkModel model, + string relativePath, + bool hidden, + INodeNavigationItem? parent, + IRootNavigationItem navigationRoot, + IPathPrefixProvider prefixProvider +) + : ILeafNavigationItem +{ + /// + public IDocumentationFile Model { get; init; } = model; + + /// + public string Url + { + get + { + var rootUrl = prefixProvider.PathPrefix.TrimEnd('/'); + // Remove extension while preserving directory path + var path = relativePath.EndsWith(".md", StringComparison.OrdinalIgnoreCase) + ? relativePath[..^3] // Remove last 3 characters (.md) + : relativePath; + + // If 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; } = 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 { get; } +} + 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..32a0a3534 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 @{ 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.Markdown/Elastic.Markdown.csproj b/src/Elastic.Markdown/Elastic.Markdown.csproj index 9d5529c8d..800752836 100644 --- a/src/Elastic.Markdown/Elastic.Markdown.csproj +++ b/src/Elastic.Markdown/Elastic.Markdown.csproj @@ -37,6 +37,7 @@ + diff --git a/src/Elastic.Markdown/HtmlWriter.cs b/src/Elastic.Markdown/HtmlWriter.cs index 804d12e01..f6b423dbd 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; diff --git a/src/Elastic.Markdown/IO/DocumentationSet.cs b/src/Elastic.Markdown/IO/DocumentationSet.cs index 303f637fa..d97f4b01a 100644 --- a/src/Elastic.Markdown/IO/DocumentationSet.cs +++ b/src/Elastic.Markdown/IO/DocumentationSet.cs @@ -12,6 +12,7 @@ using Elastic.Documentation.Configuration.TableOfContents; using Elastic.Documentation.Links; using Elastic.Documentation.Links.CrossLinks; +using Elastic.Documentation.Navigation; using Elastic.Documentation.Site.Navigation; using Elastic.Markdown.Extensions; using Elastic.Markdown.Extensions.DetectionRules; diff --git a/src/Elastic.Markdown/IO/MarkdownFile.cs b/src/Elastic.Markdown/IO/MarkdownFile.cs index d9dfb8358..f0992711c 100644 --- a/src/Elastic.Markdown/IO/MarkdownFile.cs +++ b/src/Elastic.Markdown/IO/MarkdownFile.cs @@ -8,7 +8,6 @@ using Elastic.Documentation.Diagnostics; using Elastic.Documentation.Links.CrossLinks; using Elastic.Documentation.Navigation; -using Elastic.Documentation.Site.Navigation; using Elastic.Markdown.Helpers; using Elastic.Markdown.Myst; using Elastic.Markdown.Myst.Directives; diff --git a/src/Elastic.Markdown/IO/Navigation/CrossLinkNavigationItem.cs b/src/Elastic.Markdown/IO/Navigation/CrossLinkNavigationItem.cs index 6dd24c352..c35eaae1d 100644 --- a/src/Elastic.Markdown/IO/Navigation/CrossLinkNavigationItem.cs +++ b/src/Elastic.Markdown/IO/Navigation/CrossLinkNavigationItem.cs @@ -4,7 +4,7 @@ using System.Diagnostics; using System.Diagnostics.CodeAnalysis; -using Elastic.Documentation.Site.Navigation; +using Elastic.Documentation.Navigation; namespace Elastic.Markdown.IO.Navigation; diff --git a/src/Elastic.Markdown/IO/Navigation/DocumentationGroup.cs b/src/Elastic.Markdown/IO/Navigation/DocumentationGroup.cs index fd121a2c5..98197cb9d 100644 --- a/src/Elastic.Markdown/IO/Navigation/DocumentationGroup.cs +++ b/src/Elastic.Markdown/IO/Navigation/DocumentationGroup.cs @@ -7,7 +7,7 @@ using Elastic.Documentation.Configuration; using Elastic.Documentation.Configuration.TableOfContents; using Elastic.Documentation.Extensions; -using Elastic.Documentation.Site.Navigation; +using Elastic.Documentation.Navigation; namespace Elastic.Markdown.IO.Navigation; diff --git a/src/Elastic.Markdown/IO/Navigation/FileNavigationItem.cs b/src/Elastic.Markdown/IO/Navigation/FileNavigationItem.cs index ff34bd750..cf46f185e 100644 --- a/src/Elastic.Markdown/IO/Navigation/FileNavigationItem.cs +++ b/src/Elastic.Markdown/IO/Navigation/FileNavigationItem.cs @@ -3,7 +3,7 @@ // See the LICENSE file in the project root for more information using System.Diagnostics; -using Elastic.Documentation.Site.Navigation; +using Elastic.Documentation.Navigation; namespace Elastic.Markdown.IO.Navigation; diff --git a/src/Elastic.Markdown/IO/Navigation/TableOfContentsTree.cs b/src/Elastic.Markdown/IO/Navigation/TableOfContentsTree.cs index d9caeb648..60a79029e 100644 --- a/src/Elastic.Markdown/IO/Navigation/TableOfContentsTree.cs +++ b/src/Elastic.Markdown/IO/Navigation/TableOfContentsTree.cs @@ -4,7 +4,7 @@ using System.Diagnostics; using Elastic.Documentation.Configuration; -using Elastic.Documentation.Site.Navigation; +using Elastic.Documentation.Navigation; namespace Elastic.Markdown.IO.Navigation; diff --git a/src/Elastic.Markdown/MarkdownLayoutViewModel.cs b/src/Elastic.Markdown/MarkdownLayoutViewModel.cs index 7b947637b..954b0dc60 100644 --- a/src/Elastic.Markdown/MarkdownLayoutViewModel.cs +++ b/src/Elastic.Markdown/MarkdownLayoutViewModel.cs @@ -3,8 +3,8 @@ // See the LICENSE file in the project root for more information using Elastic.Documentation.Configuration.LegacyUrlMappings; +using Elastic.Documentation.Navigation; using Elastic.Documentation.Site; -using Elastic.Documentation.Site.Navigation; namespace Elastic.Markdown; diff --git a/src/Elastic.Markdown/Myst/Renderers/HtmxLinkInlineRenderer.cs b/src/Elastic.Markdown/Myst/Renderers/HtmxLinkInlineRenderer.cs index 1fc323598..834ef80ee 100644 --- a/src/Elastic.Markdown/Myst/Renderers/HtmxLinkInlineRenderer.cs +++ b/src/Elastic.Markdown/Myst/Renderers/HtmxLinkInlineRenderer.cs @@ -2,8 +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; using Elastic.Documentation.Site; -using Elastic.Documentation.Site.Navigation; using Elastic.Markdown.IO; using Markdig; using Markdig.Renderers; diff --git a/src/Elastic.Markdown/Page/IndexViewModel.cs b/src/Elastic.Markdown/Page/IndexViewModel.cs index 7c3ce4761..044210fa1 100644 --- a/src/Elastic.Markdown/Page/IndexViewModel.cs +++ b/src/Elastic.Markdown/Page/IndexViewModel.cs @@ -9,8 +9,8 @@ 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.IO; using Elastic.Markdown.IO.Navigation; diff --git a/src/services/Elastic.Documentation.Assembler/Building/SitemapBuilder.cs b/src/services/Elastic.Documentation.Assembler/Building/SitemapBuilder.cs index 98156b4e0..d4fbb5b3f 100644 --- a/src/services/Elastic.Documentation.Assembler/Building/SitemapBuilder.cs +++ b/src/services/Elastic.Documentation.Assembler/Building/SitemapBuilder.cs @@ -5,7 +5,7 @@ using System.Globalization; using System.IO.Abstractions; using System.Xml.Linq; -using Elastic.Documentation.Site.Navigation; +using Elastic.Documentation.Navigation; using Elastic.Markdown.Extensions.DetectionRules; using Elastic.Markdown.IO.Navigation; diff --git a/src/services/Elastic.Documentation.Assembler/Navigation/GlobalNavigation.cs b/src/services/Elastic.Documentation.Assembler/Navigation/GlobalNavigation.cs index e0ba7e920..3b08d593b 100644 --- a/src/services/Elastic.Documentation.Assembler/Navigation/GlobalNavigation.cs +++ b/src/services/Elastic.Documentation.Assembler/Navigation/GlobalNavigation.cs @@ -7,7 +7,7 @@ using Elastic.Documentation.Configuration.Assembler; using Elastic.Documentation.Configuration.Navigation; using Elastic.Documentation.Configuration.TableOfContents; -using Elastic.Documentation.Site.Navigation; +using Elastic.Documentation.Navigation; using Elastic.Markdown.IO; using Elastic.Markdown.IO.Navigation; diff --git a/src/services/Elastic.Documentation.Assembler/Navigation/GlobalNavigationHtmlWriter.cs b/src/services/Elastic.Documentation.Assembler/Navigation/GlobalNavigationHtmlWriter.cs index a79fd8734..232c3e89f 100644 --- a/src/services/Elastic.Documentation.Assembler/Navigation/GlobalNavigationHtmlWriter.cs +++ b/src/services/Elastic.Documentation.Assembler/Navigation/GlobalNavigationHtmlWriter.cs @@ -4,6 +4,7 @@ using System.Collections.Concurrent; using Elastic.Documentation.Diagnostics; +using Elastic.Documentation.Navigation; using Elastic.Documentation.Site.Navigation; using Elastic.Markdown.IO.Navigation; using Microsoft.Extensions.Logging; diff --git a/src/services/Elastic.Documentation.Assembler/Navigation/LlmsNavigationEnhancer.cs b/src/services/Elastic.Documentation.Assembler/Navigation/LlmsNavigationEnhancer.cs index e269cb7d3..3c9185653 100644 --- a/src/services/Elastic.Documentation.Assembler/Navigation/LlmsNavigationEnhancer.cs +++ b/src/services/Elastic.Documentation.Assembler/Navigation/LlmsNavigationEnhancer.cs @@ -8,7 +8,7 @@ using System.Text; using Elastic.Documentation.Assembler; using Elastic.Documentation.Assembler.Navigation; -using Elastic.Documentation.Site.Navigation; +using Elastic.Documentation.Navigation; using Elastic.Markdown.IO; using Elastic.Markdown.IO.Navigation; using Elastic.Markdown.Myst.Renderers.LlmMarkdown; diff --git a/tests/Elastic.Documentation.Configuration.Tests/DocumentationSetFileTests.cs b/tests/Elastic.Documentation.Configuration.Tests/DocumentationSetFileTests.cs new file mode 100644 index 000000000..b7a8d7c9b --- /dev/null +++ b/tests/Elastic.Documentation.Configuration.Tests/DocumentationSetFileTests.cs @@ -0,0 +1,356 @@ +// 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 FluentAssertions; + +namespace Elastic.Documentation.Configuration.Tests; + +public class DocumentationSetFileTests +{ + private DocumentationSetFile Deserialize(string yaml) => DocumentationSetFile.Deserialize(yaml); + + [Fact] + public void DeserializesBasicProperties() + { + // language=yaml + var yaml = """ + project: 'test-project' + max_toc_depth: 3 + dev_docs: true + cross_links: + - docs-content + - other-docs + exclude: + - '_*.md' + - '*.tmp' + """; + + var result = Deserialize(yaml); + + result.Project.Should().Be("test-project"); + result.MaxTocDepth.Should().Be(3); + result.DevDocs.Should().BeTrue(); + result.CrossLinks.Should().HaveCount(2) + .And.Contain("docs-content") + .And.Contain("other-docs"); + result.Exclude.Should().HaveCount(2) + .And.Contain("_*.md") + .And.Contain("*.tmp"); + } + + [Fact] + public void DeserializesSubstitutions() + { + // language=yaml + var yaml = """ + project: 'test-project' + subs: + stack: Elastic Stack + ecloud: Elastic Cloud + dbuild: docs-builder + """; + + var result = Deserialize(yaml); + + result.Subs.Should().HaveCount(3) + .And.ContainKey("stack").WhoseValue.Should().Be("Elastic Stack"); + result.Subs.Should().ContainKey("ecloud").WhoseValue.Should().Be("Elastic Cloud"); + result.Subs.Should().ContainKey("dbuild").WhoseValue.Should().Be("docs-builder"); + } + + [Fact] + public void DeserializesFeatures() + { + // language=yaml + var yaml = """ + project: 'test-project' + features: + primary-nav: false + """; + + var result = Deserialize(yaml); + + result.Features.Should().NotBeNull(); + result.Features.PrimaryNav.Should().BeFalse(); + } + + [Fact] + public void DeserializesApiConfiguration() + { + // language=yaml + var yaml = """ + project: 'test-project' + api: + elasticsearch: elasticsearch-openapi.json + kibana: kibana-openapi.json + """; + + var result = Deserialize(yaml); + + result.Api.Should().HaveCount(2) + .And.ContainKey("elasticsearch").WhoseValue.Should().Be("elasticsearch-openapi.json"); + result.Api.Should().ContainKey("kibana").WhoseValue.Should().Be("kibana-openapi.json"); + } + + [Fact] + public void DeserializesFileReference() + { + // language=yaml + var yaml = """ + project: 'test-project' + toc: + - file: index.md + - file: getting-started.md + """; + + var result = Deserialize(yaml); + + result.Toc.Should().HaveCount(2); + result.Toc.ElementAt(0).Should().BeOfType() + .Which.RelativePath.Should().Be("index.md"); + result.Toc.ElementAt(1).Should().BeOfType() + .Which.RelativePath.Should().Be("getting-started.md"); + } + + [Fact] + public void DeserializesHiddenFileReference() + { + // language=yaml + var yaml = """ + project: 'test-project' + toc: + - file: index.md + - hidden: 404.md + - hidden: developer-notes.md + """; + + var result = Deserialize(yaml); + + result.Toc.Should().HaveCount(3); + result.Toc.ElementAt(0).Should().BeOfType() + .Which.Hidden.Should().BeFalse(); + result.Toc.ElementAt(1).Should().BeOfType() + .Which.Hidden.Should().BeTrue(); + result.Toc.ElementAt(1).Should().BeOfType() + .Which.RelativePath.Should().Be("404.md"); + result.Toc.ElementAt(2).Should().BeOfType() + .Which.Hidden.Should().BeTrue(); + } + + [Fact] + public void DeserializesFolderReference() + { + // language=yaml + var yaml = """ + project: 'test-project' + toc: + - folder: contribute + children: + - file: index.md + - file: locally.md + """; + + var result = Deserialize(yaml); + + result.Toc.Should().HaveCount(1); + var folder = result.Toc.ElementAt(0).Should().BeOfType().Subject; + folder.RelativePath.Should().Be("contribute"); + folder.Children.Should().HaveCount(2); + folder.Children.ElementAt(0).Should().BeOfType() + .Which.RelativePath.Should().Be("index.md"); + folder.Children.ElementAt(1).Should().BeOfType() + .Which.RelativePath.Should().Be("locally.md"); + } + + [Fact] + public void DeserializesTocReference() + { + // language=yaml + var yaml = """ + project: 'test-project' + toc: + - file: index.md + - toc: development + """; + + var result = Deserialize(yaml); + + result.Toc.Should().HaveCount(2); + result.Toc.ElementAt(0).Should().BeOfType(); + result.Toc.ElementAt(1).Should().BeOfType() + .Which.Source.Should().Be("development"); + } + + [Fact] + public void DeserializesCrossLinkReference() + { + // language=yaml + var yaml = """ + project: 'test-project' + toc: + - file: index.md + - file: cross-links.md + children: + - title: "Getting Started Guide" + crosslink: docs-content://get-started/introduction.md + """; + + var result = Deserialize(yaml); + + result.Toc.Should().HaveCount(2); + var fileWithChildren = result.Toc.ElementAt(1).Should().BeOfType().Subject; + fileWithChildren.Children.Should().HaveCount(1); + var crosslink = fileWithChildren.Children.ElementAt(0).Should().BeOfType().Subject; + crosslink.Title.Should().Be("Getting Started Guide"); + crosslink.CrossLinkUri.ToString().Should().Be("docs-content://get-started/introduction.md"); + } + + [Fact] + public void DeserializesNestedStructure() + { + // language=yaml + var yaml = """ + project: 'test-project' + toc: + - folder: configure + children: + - file: index.md + - folder: site + children: + - file: index.md + - file: content.md + - file: navigation.md + """; + + var result = Deserialize(yaml); + + result.Toc.Should().HaveCount(1); + var topFolder = result.Toc.ElementAt(0).Should().BeOfType().Subject; + topFolder.RelativePath.Should().Be("configure"); + topFolder.Children.Should().HaveCount(2); + + topFolder.Children.ElementAt(0).Should().BeOfType() + .Which.RelativePath.Should().Be("index.md"); + + var nestedFolder = topFolder.Children.ElementAt(1).Should().BeOfType().Subject; + nestedFolder.RelativePath.Should().Be("site"); + nestedFolder.Children.Should().HaveCount(3); + nestedFolder.Children.ElementAt(0).Should().BeOfType() + .Which.RelativePath.Should().Be("index.md"); + nestedFolder.Children.ElementAt(1).Should().BeOfType() + .Which.RelativePath.Should().Be("content.md"); + nestedFolder.Children.ElementAt(2).Should().BeOfType() + .Which.RelativePath.Should().Be("navigation.md"); + } + + [Fact] + public void DeserializesCompleteDocsetYaml() + { + // language=yaml + var yaml = """ + project: 'doc-builder' + max_toc_depth: 2 + dev_docs: true + cross_links: + - docs-content + exclude: + - '_*.md' + subs: + stack: Elastic Stack + serverless-short: Serverless + ecloud: Elastic Cloud + features: + primary-nav: false + api: + elasticsearch: elasticsearch-openapi.json + kibana: kibana-openapi.json + toc: + - file: index.md + - hidden: 404.md + - folder: configure + children: + - file: index.md + - folder: site + children: + - file: index.md + - file: content.md + - file: navigation.md + - file: page.md + children: + - title: "Getting Started Guide" + crosslink: docs-content://get-started/introduction.md + - toc: development + """; + + var result = Deserialize(yaml); + + // Assert top-level docset properties + result.Project.Should().Be("doc-builder"); + result.MaxTocDepth.Should().Be(2); + result.DevDocs.Should().BeTrue(); + result.CrossLinks.Should().ContainSingle().Which.Should().Be("docs-content"); + result.Exclude.Should().ContainSingle().Which.Should().Be("_*.md"); + result.Subs.Should().HaveCount(3); + result.Features.PrimaryNav.Should().BeFalse(); + result.Api.Should().HaveCount(2); + + // Assert TOC structure - 4 root items + result.Toc.Should().HaveCount(4); + + // First item: simple file reference + var firstItem = result.Toc.ElementAt(0).Should().BeOfType().Subject; + firstItem.RelativePath.Should().Be("index.md"); + firstItem.Hidden.Should().BeFalse(); + firstItem.Children.Should().BeEmpty(); + + // Second item: hidden file reference + var secondItem = result.Toc.ElementAt(1).Should().BeOfType().Subject; + secondItem.RelativePath.Should().Be("404.md"); + secondItem.Hidden.Should().BeTrue(); + secondItem.Children.Should().BeEmpty(); + + // Third item: folder with a deeply nested structure + var configureFolder = result.Toc.ElementAt(2).Should().BeOfType().Subject; + configureFolder.RelativePath.Should().Be("configure"); + configureFolder.Children.Should().HaveCount(3); + + // First child: file reference + var configureIndexFile = configureFolder.Children.ElementAt(0).Should().BeOfType().Subject; + configureIndexFile.RelativePath.Should().Be("index.md"); + configureIndexFile.Hidden.Should().BeFalse(); + + // Second child: nested folder with 3 files + var siteFolder = configureFolder.Children.ElementAt(1).Should().BeOfType().Subject; + siteFolder.RelativePath.Should().Be("site"); + siteFolder.Children.Should().HaveCount(3); + + // Assert nested folder's children + var siteIndexFile = siteFolder.Children.ElementAt(0).Should().BeOfType().Subject; + siteIndexFile.RelativePath.Should().Be("index.md"); + + var contentFile = siteFolder.Children.ElementAt(1).Should().BeOfType().Subject; + contentFile.RelativePath.Should().Be("content.md"); + + var navigationFile = siteFolder.Children.ElementAt(2).Should().BeOfType().Subject; + navigationFile.RelativePath.Should().Be("navigation.md"); + + // Third child: file with crosslink child + var pageFile = configureFolder.Children.ElementAt(2).Should().BeOfType().Subject; + pageFile.RelativePath.Should().Be("page.md"); + pageFile.Children.Should().HaveCount(1); + + // Assert crosslink reference as a child of page.md + var crosslink = pageFile.Children.ElementAt(0).Should().BeOfType().Subject; + crosslink.Title.Should().Be("Getting Started Guide"); + crosslink.CrossLinkUri.ToString().Should().Be("docs-content://get-started/introduction.md"); + crosslink.Hidden.Should().BeFalse(); + crosslink.Children.Should().BeEmpty(); + + // Fourth item: toc reference + var tocRef = result.Toc.ElementAt(3).Should().BeOfType().Subject; + tocRef.Source.Should().Be("development"); + tocRef.Children.Should().BeEmpty(); + } +} diff --git a/tests/Elastic.Documentation.Configuration.Tests/Elastic.Documentation.Configuration.Tests.csproj b/tests/Elastic.Documentation.Configuration.Tests/Elastic.Documentation.Configuration.Tests.csproj new file mode 100644 index 000000000..f5acc84c4 --- /dev/null +++ b/tests/Elastic.Documentation.Configuration.Tests/Elastic.Documentation.Configuration.Tests.csproj @@ -0,0 +1,11 @@ + + + + net9.0 + + + + + + + diff --git a/tests/Elastic.Documentation.Configuration.Tests/SiteNavigationFileTests.cs b/tests/Elastic.Documentation.Configuration.Tests/SiteNavigationFileTests.cs new file mode 100644 index 000000000..6f241e224 --- /dev/null +++ b/tests/Elastic.Documentation.Configuration.Tests/SiteNavigationFileTests.cs @@ -0,0 +1,145 @@ +// 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 FluentAssertions; + +namespace Elastic.Documentation.Configuration.Tests; + +public class SiteNavigationFileTests +{ + [Fact] + public void DeserializesSiteNavigationFile() + { + // language=yaml + var yaml = """ + phantoms: + - toc: elasticsearch://reference + - toc: docs-content:// + toc: + - toc: serverless/observability + path_prefix: /serverless/observability + - toc: serverless/search + path_prefix: /serverless/search + - toc: serverless/security + path_prefix: /serverless/security + """; + + var siteNav = SiteNavigationFile.Deserialize(yaml); + + siteNav.Should().NotBeNull(); + siteNav.Phantoms.Should().HaveCount(2); + siteNav.Phantoms.ElementAt(0).Source.Should().Be("elasticsearch://reference"); + siteNav.Phantoms.ElementAt(1).Source.Should().Be("docs-content://"); + + siteNav.TableOfContents.Should().HaveCount(3); + + var observability = siteNav.TableOfContents.ElementAt(0); + observability.Source.ToString().Should().Be("docs-content://serverless/observability"); + observability.PathPrefix.Should().Be("/serverless/observability"); + observability.Children.Should().BeEmpty(); + + var search = siteNav.TableOfContents.ElementAt(1); + search.Source.ToString().Should().Be("docs-content://serverless/search"); + search.PathPrefix.Should().Be("/serverless/search"); + + var security = siteNav.TableOfContents.ElementAt(2); + security.Source.ToString().Should().Be("docs-content://serverless/security"); + security.PathPrefix.Should().Be("/serverless/security"); + } + + [Fact] + public void DeserializesSiteNavigationFileWithNestedChildren() + { + // language=yaml + var yaml = """ + toc: + - toc: platform + path_prefix: /platform + children: + - toc: platform/deployment-guide + path_prefix: /platform/deployment + - toc: platform/cloud-guide + path_prefix: /platform/cloud + """; + + var siteNav = SiteNavigationFile.Deserialize(yaml); + + siteNav.TableOfContents.Should().HaveCount(1); + + var platform = siteNav.TableOfContents.First(); + platform.Source.ToString().Should().Be("docs-content://platform/"); + platform.PathPrefix.Should().Be("/platform"); + platform.Children.Should().HaveCount(2); + + var deployment = platform.Children.ElementAt(0); + deployment.Source.ToString().Should().Be("docs-content://platform/deployment-guide"); + deployment.PathPrefix.Should().Be("/platform/deployment"); + + var cloud = platform.Children.ElementAt(1); + cloud.Source.ToString().Should().Be("docs-content://platform/cloud-guide"); + cloud.PathPrefix.Should().Be("/platform/cloud"); + } + + [Fact] + public void DeserializesWithMissingPath() + { + // language=yaml + var yaml = """ + toc: + - toc: elasticsearch/reference + """; + + var siteNav = SiteNavigationFile.Deserialize(yaml); + + siteNav.TableOfContents.Should().HaveCount(1); + var ref1 = siteNav.TableOfContents.First(); + ref1.Source.ToString().Should().Be("docs-content://elasticsearch/reference"); + ref1.PathPrefix.Should().BeEmpty(); + } + + [Fact] + public void PreservesSchemeWhenPresent() + { + // language=yaml + var yaml = """ + toc: + - toc: elasticsearch://reference/current + - toc: kibana://reference/8.0 + - toc: serverless/observability + """; + + var siteNav = SiteNavigationFile.Deserialize(yaml); + + siteNav.TableOfContents.Should().HaveCount(3); + + // With elasticsearch:// scheme + var elasticsearch = siteNav.TableOfContents.ElementAt(0); + elasticsearch.Source.ToString().Should().Be("elasticsearch://reference/current"); + + // With kibana:// scheme + var kibana = siteNav.TableOfContents.ElementAt(1); + kibana.Source.ToString().Should().Be("kibana://reference/8.0"); + + // Without scheme - should get docs-content:// + var serverless = siteNav.TableOfContents.ElementAt(2); + serverless.Source.ToString().Should().Be("docs-content://serverless/observability"); + } + + [Fact] + public void ThrowsExceptionForInvalidUri() + { + // language=yaml + var yaml = """ + toc: + - toc: ://invalid + """; + + var act = () => SiteNavigationFile.Deserialize(yaml); + + act.Should().Throw() + .WithInnerException() + .WithMessage("Invalid TOC source: '://invalid' could not be parsed as a URI"); + } +} diff --git a/tests/Navigation.Tests/Assembler/ComplexSiteNavigationTests.cs b/tests/Navigation.Tests/Assembler/ComplexSiteNavigationTests.cs new file mode 100644 index 000000000..1de8eb21c --- /dev/null +++ b/tests/Navigation.Tests/Assembler/ComplexSiteNavigationTests.cs @@ -0,0 +1,339 @@ +// 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.Navigation.Assembler; +using Elastic.Documentation.Navigation.Isolated; +using FluentAssertions; + +namespace Elastic.Documentation.Navigation.Tests.Assembler; + +public class ComplexSiteNavigationTests(ITestOutputHelper output) +{ + [Fact] + public void ComplexNavigationWithMultipleNestedTocsAppliesPathPrefixToRootUrls() + { + // language=yaml + var siteNavYaml = """ + toc: + - toc: observability:// + path_prefix: /serverless/observability + - toc: serverless-search:// + path_prefix: /serverless/search + - toc: platform:// + path_prefix: /platform + children: + - toc: platform://deployment-guide + path_prefix: /platform/deployment + - toc: platform://cloud-guide + path_prefix: /platform/cloud + - toc: elasticsearch-reference:// + path_prefix: /elasticsearch/reference + """; + + var siteNavFile = SiteNavigationFile.Deserialize(siteNavYaml); + var fileSystem = SiteNavigationTestFixture.CreateMultiRepositoryFileSystem(); + + // Create all DocumentationSetNavigation instances + var checkoutDir = fileSystem.DirectoryInfo.New("/checkouts/current"); + var repositories = checkoutDir.GetDirectories(); + + var documentationSets = new List(); + + foreach (var repo in repositories) + { + var context = SiteNavigationTestFixture.CreateContext(fileSystem, repo.FullName, output); + + var docsetPath = fileSystem.File.Exists($"{repo.FullName}/docs/docset.yml") + ? $"{repo.FullName}/docs/docset.yml" + : $"{repo.FullName}/docs/_docset.yml"; + + var docsetYaml = fileSystem.File.ReadAllText(docsetPath); + var docset = DocumentationSetFile.Deserialize(docsetYaml); + + var navigation = new DocumentationSetNavigation(docset, context); + documentationSets.Add(navigation); + } + + var siteContext = SiteNavigationTestFixture.CreateContext( + fileSystem, "/checkouts/current/observability", output); + + var siteNavigation = new SiteNavigation(siteNavFile, siteContext, documentationSets); + + // Verify we have all expected top-level items + siteNavigation.NavigationItems.Should().HaveCount(4); + + // Test 1: Observability - verify root URL has path prefix + var observability = siteNavigation.NavigationItems.ElementAt(0); + observability.Should().NotBeNull(); + observability.Url.Should().Be("/serverless/observability"); + observability.NavigationTitle.Should().Be("serverless-observability"); + + // Test 2: Serverless Search - verify root URL has path prefix + var search = siteNavigation.NavigationItems.ElementAt(1); + search.Should().NotBeNull(); + search.Url.Should().Be("/serverless/search"); + + // Test 3: Platform - verify root URL has path prefix + var platform = siteNavigation.NavigationItems.ElementAt(2) as INodeNavigationItem; + platform.Should().NotBeNull(); + platform.Url.Should().Be("/platform"); + platform.NavigationItems.Should().HaveCount(2, "platform should only show the two nested TOCs as children"); + + // Verify nested TOC URLs have their specified path prefixes + var deploymentGuide = platform.NavigationItems.ElementAt(0); + deploymentGuide.Should().NotBeNull(); + deploymentGuide.Url.Should().Be("/platform/deployment"); + deploymentGuide.NavigationTitle.Should().Be("deployment-guide"); + + var cloudGuide = platform.NavigationItems.ElementAt(1); + cloudGuide.Should().NotBeNull(); + cloudGuide.Url.Should().Be("/platform/cloud"); + cloudGuide.NavigationTitle.Should().Be("cloud-guide"); + + // Test 4: Elasticsearch Reference - verify root URL has path prefix + var elasticsearch = siteNavigation.NavigationItems.ElementAt(3) as INodeNavigationItem; + elasticsearch.Should().NotBeNull(); + elasticsearch.Url.Should().Be("/elasticsearch/reference"); + elasticsearch.NavigationItems.Should().HaveCount(3, "elasticsearch should have read its toc"); + + // rest-apis is a folder (not a TOC) + var restApis = elasticsearch.NavigationItems.ElementAt(1).Should().BeOfType().Subject; + restApis.Url.Should().Be("/elasticsearch/reference/rest-apis"); + restApis.NavigationItems.Should().HaveCount(3, "rest-apis folder should have 3 files"); + + // Verify the file inside the folder has the correct path prefix + var documentApisFile = restApis.NavigationItems.ElementAt(1).Should().BeOfType().Subject; + documentApisFile.Url.Should().Be("/elasticsearch/reference/rest-apis/document-apis"); + documentApisFile.NavigationTitle.Should().Be("document-apis"); + } + + [Fact] + public void DeeplyNestedNavigationMaintainsPathPrefixThroughoutHierarchy() + { + // language=yaml - test without specifying children for nested TOCs + var siteNavYaml = """ + toc: + - toc: platform:// + path_prefix: /docs/platform + """; + + var siteNavFile = SiteNavigationFile.Deserialize(siteNavYaml); + var fileSystem = SiteNavigationTestFixture.CreateMultiRepositoryFileSystem(); + + var platformContext = SiteNavigationTestFixture.CreateContext( + fileSystem, "/checkouts/current/platform", output); + var platformDocset = DocumentationSetFile.Deserialize( + fileSystem.File.ReadAllText("/checkouts/current/platform/docs/docset.yml")); + + var documentationSets = new List + { + new(platformDocset, platformContext) + }; + + var siteContext = SiteNavigationTestFixture.CreateContext( + fileSystem, "/checkouts/current/platform", output); + + var siteNavigation = new SiteNavigation(siteNavFile, siteContext, documentationSets); + + var platform = siteNavigation.NavigationItems.First() as INodeNavigationItem; + platform.Should().NotBeNull(); + platform!.Url.Should().Be("/docs/platform"); + + // Platform should have its children (index, deployment-guide, cloud-guide) + platform.NavigationItems.Should().HaveCount(3); + + // Find the deployment-guide TOC (it's the second item after index) + var deploymentGuide = platform.NavigationItems.ElementAt(1) as INodeNavigationItem; + deploymentGuide.Should().NotBeNull(); + deploymentGuide!.Should().BeOfType(); + deploymentGuide.Url.Should().StartWith("/docs/platform"); + + // Walk through the entire tree and verify every single URL starts with path prefix + var allUrls = CollectAllUrls(platform.NavigationItems); + allUrls.Should().NotBeEmpty(); + allUrls.Should().OnlyContain(url => url.StartsWith("/docs/platform"), + "all URLs in platform should start with /docs/platform"); + } + + [Fact] + public void FileNavigationLeafUrlsReflectPathPrefixInDeeplyNestedStructures() + { + // language=yaml - don't specify children so we can access the actual file leaves + var siteNavYaml = """ + toc: + - toc: platform:// + path_prefix: /platform + """; + + var siteNavFile = SiteNavigationFile.Deserialize(siteNavYaml); + var fileSystem = SiteNavigationTestFixture.CreateMultiRepositoryFileSystem(); + + var platformContext = SiteNavigationTestFixture.CreateContext( + fileSystem, "/checkouts/current/platform", output); + var platformDocset = DocumentationSetFile.Deserialize( + fileSystem.File.ReadAllText("/checkouts/current/platform/docs/docset.yml")); + + var documentationSets = new List + { + new(platformDocset, platformContext) + }; + + var siteContext = SiteNavigationTestFixture.CreateContext( + fileSystem, "/checkouts/current/platform", output); + + var siteNavigation = new SiteNavigation(siteNavFile, siteContext, documentationSets); + + var platform = siteNavigation.NavigationItems.First() as INodeNavigationItem; + platform.Should().NotBeNull(); + + // Platform should have its children including deployment-guide TOC + platform!.NavigationItems.Should().HaveCount(3); + + // Get deployment-guide TOC (second item after index) + var deploymentGuide = platform.NavigationItems.ElementAt(1) as INodeNavigationItem; + deploymentGuide.Should().NotBeNull(); + deploymentGuide!.Should().BeOfType(); + + // Find all FileNavigationLeaf items recursively + var fileLeaves = CollectAllFileLeaves(deploymentGuide.NavigationItems); + fileLeaves.Should().NotBeEmpty("deployment-guide should contain file leaves"); + + // Verify every single file leaf has the correct path prefix + foreach (var fileLeaf in fileLeaves) + { + fileLeaf.Url.Should().StartWith("/platform", + $"file '{fileLeaf.NavigationTitle}' should have URL starting with /platform but got '{fileLeaf.Url}'"); + } + + // Verify at least one specific file to ensure we're testing real data + var indexFile = fileLeaves.FirstOrDefault(f => f.NavigationTitle == "index"); + indexFile.Should().NotBeNull(); + indexFile!.Url.Should().StartWith("/platform"); + } + + [Fact] + public void FolderNavigationWithinNestedTocsHasCorrectPathPrefix() + { + // language=yaml - don't specify children so we can access the actual folders + var siteNavYaml = """ + toc: + - toc: platform:// + path_prefix: /platform/cloud + """; + + var siteNavFile = SiteNavigationFile.Deserialize(siteNavYaml); + var fileSystem = SiteNavigationTestFixture.CreateMultiRepositoryFileSystem(); + + var platformContext = SiteNavigationTestFixture.CreateContext( + fileSystem, "/checkouts/current/platform", output); + var platformDocset = DocumentationSetFile.Deserialize( + fileSystem.File.ReadAllText("/checkouts/current/platform/docs/docset.yml")); + + var documentationSets = new List + { + new(platformDocset, platformContext) + }; + + var siteContext = SiteNavigationTestFixture.CreateContext( + fileSystem, "/checkouts/current/platform", output); + + var siteNavigation = new SiteNavigation(siteNavFile, siteContext, documentationSets); + + var platform = siteNavigation.NavigationItems.First() as INodeNavigationItem; + platform.Should().NotBeNull(); + + // Platform should have its children including cloud-guide TOC + platform!.NavigationItems.Should().HaveCount(3); + + // Get cloud-guide TOC (third item after index and deployment-guide) + var cloudGuide = platform.NavigationItems.ElementAt(2) as INodeNavigationItem; + cloudGuide.Should().NotBeNull(); + cloudGuide!.Should().BeOfType(); + + // cloud-guide should have folders (index, aws, azure) + var folders = cloudGuide.NavigationItems + .OfType() + .ToList(); + + folders.Should().NotBeEmpty("cloud-guide should contain folders"); + + // Verify each folder and all its contents have correct path prefix + foreach (var folder in folders) + { + folder.Url.Should().StartWith("/platform/cloud", + $"folder '{folder.NavigationTitle}' should have URL starting with /platform/cloud"); + + // Verify all items within the folder + AssertAllUrlsStartWith(folder.NavigationItems, "/platform/cloud"); + + // Verify specific file leaves within the folder + var filesInFolder = CollectAllFileLeaves(folder.NavigationItems); + foreach (var file in filesInFolder) + { + file.Url.Should().StartWith("/platform/cloud", + $"file '{file.NavigationTitle}' in folder '{folder.NavigationTitle}' should have URL starting with /platform/cloud"); + } + } + } + + /// + /// Helper method to recursively assert all URLs start with a given prefix + /// + private static void AssertAllUrlsStartWith(IEnumerable items, string expectedPrefix) + { + foreach (var item in items) + { + item.Url.Should().StartWith(expectedPrefix, + $"item '{item.NavigationTitle}' should have URL starting with '{expectedPrefix}' but got '{item.Url}'"); + + if (item is INodeNavigationItem nodeItem) + { + AssertAllUrlsStartWith(nodeItem.NavigationItems, expectedPrefix); + } + } + } + + /// + /// Helper method to collect all URLs recursively + /// + private static List CollectAllUrls(IEnumerable items) + { + var urls = new List(); + + foreach (var item in items) + { + urls.Add(item.Url); + + if (item is INodeNavigationItem nodeItem) + { + urls.AddRange(CollectAllUrls(nodeItem.NavigationItems)); + } + } + + return urls; + } + + /// + /// Helper method to collect all FileNavigationLeaf items recursively + /// + private static List CollectAllFileLeaves(IEnumerable items) + { + var fileLeaves = new List(); + + foreach (var item in items) + { + if (item is FileNavigationLeaf fileLeaf) + { + fileLeaves.Add(fileLeaf); + } + else if (item is INodeNavigationItem nodeItem) + { + fileLeaves.AddRange(CollectAllFileLeaves(nodeItem.NavigationItems)); + } + } + + return fileLeaves; + } +} diff --git a/tests/Navigation.Tests/Assembler/IdentifierCollectionTests.cs b/tests/Navigation.Tests/Assembler/IdentifierCollectionTests.cs new file mode 100644 index 000000000..f6bc853ed --- /dev/null +++ b/tests/Navigation.Tests/Assembler/IdentifierCollectionTests.cs @@ -0,0 +1,120 @@ +// 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.Navigation.Isolated; +using FluentAssertions; + +namespace Elastic.Documentation.Navigation.Tests.Assembler; + +public class IdentifierCollectionTests(ITestOutputHelper output) +{ + [Fact] + public void DocumentationSetNavigationCollectsRootIdentifier() + { + var fileSystem = SiteNavigationTestFixture.CreateMultiRepositoryFileSystem(); + + // Test platform repository + var platformContext = SiteNavigationTestFixture.CreateContext( + fileSystem, "/checkouts/current/platform", output); + var platformDocset = DocumentationSetFile.Deserialize( + fileSystem.File.ReadAllText("/checkouts/current/platform/docs/docset.yml")); + var platformNav = new DocumentationSetNavigation(platformDocset, platformContext); + + // Root identifier should be :// + platformNav.Identifier.Should().Be(new Uri("platform://")); + platformNav.TableOfContentNodes.Keys.Should().Contain(new Uri("platform://")); + } + + [Fact] + public void DocumentationSetNavigationCollectsNestedTocIdentifiers() + { + var fileSystem = SiteNavigationTestFixture.CreateMultiRepositoryFileSystem(); + + // Test platform repository with nested TOCs + var platformContext = SiteNavigationTestFixture.CreateContext( + fileSystem, "/checkouts/current/platform", output); + var platformDocset = DocumentationSetFile.Deserialize( + fileSystem.File.ReadAllText("/checkouts/current/platform/docs/docset.yml")); + var platformNav = new DocumentationSetNavigation(platformDocset, platformContext); + + // Should collect identifiers from nested TOCs + platformNav.TableOfContentNodes.Keys.Should().Contain( + [ + new Uri("platform://"), + new Uri("platform://deployment-guide"), + new Uri("platform://cloud-guide") + ]); + + platformNav.TableOfContentNodes.Should().HaveCount(3); + } + + [Fact] + public void DocumentationSetNavigationWithSimpleStructure() + { + var fileSystem = SiteNavigationTestFixture.CreateMultiRepositoryFileSystem(); + + // Test observability repository (no nested TOCs) + var observabilityContext = SiteNavigationTestFixture.CreateContext( + fileSystem, "/checkouts/current/observability", output); + var observabilityDocset = DocumentationSetFile.Deserialize( + fileSystem.File.ReadAllText("/checkouts/current/observability/docs/docset.yml")); + var observabilityNav = new DocumentationSetNavigation(observabilityDocset, observabilityContext); + + // Should only have root identifier + observabilityNav.TableOfContentNodes.Keys.Should().Contain(new Uri("observability://")); + observabilityNav.TableOfContentNodes.Should().HaveCount(1); + } + + [Fact] + public void TableOfContentsNavigationHasCorrectIdentifier() + { + var fileSystem = SiteNavigationTestFixture.CreateMultiRepositoryFileSystem(); + + // Test platform repository with nested TOCs + var platformContext = SiteNavigationTestFixture.CreateContext( + fileSystem, "/checkouts/current/platform", output); + var platformDocset = DocumentationSetFile.Deserialize( + fileSystem.File.ReadAllText("/checkouts/current/platform/docs/docset.yml")); + var platformNav = new DocumentationSetNavigation(platformDocset, platformContext); + + // Get the deployment-guide TOC + var deploymentGuide = platformNav.NavigationItems.ElementAt(1) as TableOfContentsNavigation; + deploymentGuide.Should().NotBeNull(); + deploymentGuide!.Identifier.Should().Be(new Uri("platform://deployment-guide")); + + // Get the cloud-guide TOC + var cloudGuide = platformNav.NavigationItems.ElementAt(2) as TableOfContentsNavigation; + cloudGuide.Should().NotBeNull(); + cloudGuide!.Identifier.Should().Be(new Uri("platform://cloud-guide")); + } + + [Fact] + public void MultipleDocumentationSetsHaveDistinctIdentifiers() + { + var fileSystem = SiteNavigationTestFixture.CreateMultiRepositoryFileSystem(); + + // Create multiple documentation sets + var platformContext = SiteNavigationTestFixture.CreateContext( + fileSystem, "/checkouts/current/platform", output); + var platformDocset = DocumentationSetFile.Deserialize( + fileSystem.File.ReadAllText("/checkouts/current/platform/docs/docset.yml")); + var platformNav = new DocumentationSetNavigation(platformDocset, platformContext); + + var observabilityContext = SiteNavigationTestFixture.CreateContext( + fileSystem, "/checkouts/current/observability", output); + var observabilityDocset = DocumentationSetFile.Deserialize( + fileSystem.File.ReadAllText("/checkouts/current/observability/docs/docset.yml")); + var observabilityNav = new DocumentationSetNavigation(observabilityDocset, observabilityContext); + + // Each should have its own set of identifiers + platformNav.TableOfContentNodes.Keys.Should().NotIntersectWith(observabilityNav.TableOfContentNodes.Keys); + + // Platform should have repository name in its identifiers + platformNav.TableOfContentNodes.Keys.Should().AllSatisfy(id => id.Scheme.Should().Be("platform")); + + // Observability should have repository name in its identifiers + observabilityNav.TableOfContentNodes.Keys.Should().AllSatisfy(id => id.Scheme.Should().Be("observability")); + } +} diff --git a/tests/Navigation.Tests/Assembler/SiteDocumentationSetsTests.cs b/tests/Navigation.Tests/Assembler/SiteDocumentationSetsTests.cs new file mode 100644 index 000000000..6628b3f17 --- /dev/null +++ b/tests/Navigation.Tests/Assembler/SiteDocumentationSetsTests.cs @@ -0,0 +1,402 @@ +// 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.Navigation.Assembler; +using Elastic.Documentation.Navigation.Isolated; +using FluentAssertions; + +namespace Elastic.Documentation.Navigation.Tests.Assembler; + +public class SiteDocumentationSetsTests(ITestOutputHelper output) +{ + [Fact] + public void CreatesDocumentationSetNavigationsFromCheckoutFolders() + { + var fileSystem = SiteNavigationTestFixture.CreateMultiRepositoryFileSystem(); + + // Discover all repositories in /checkouts/current + var checkoutDir = fileSystem.DirectoryInfo.New("/checkouts/current"); + var repositories = checkoutDir.GetDirectories(); + + repositories.Should().HaveCount(5); + repositories.Select(r => r.Name).Should().Contain( + [ + "observability", + "serverless-search", + "serverless-security", + "platform", + "elasticsearch-reference" + ]); + + // Create DocumentationSetNavigation for each repository + var documentationSets = new List(); + + foreach (var repo in repositories) + { + var context = SiteNavigationTestFixture.CreateContext(fileSystem, repo.FullName, output); + + // Read the docset file + var docsetPath = fileSystem.File.Exists($"{repo.FullName}/docs/docset.yml") + ? $"{repo.FullName}/docs/docset.yml" + : $"{repo.FullName}/docs/_docset.yml"; + + var docsetYaml = fileSystem.File.ReadAllText(docsetPath); + var docset = DocumentationSetFile.Deserialize(docsetYaml); + + var navigation = new DocumentationSetNavigation(docset, context); + documentationSets.Add(navigation); + } + + documentationSets.Should().HaveCount(5); + + // Verify each documentation set has navigation items + foreach (var docSet in documentationSets) + docSet.NavigationItems.Should().NotBeEmpty(); + } + + [Fact] + public void SiteNavigationIntegratesWithDocumentationSets() + { + // language=yaml + var siteNavYaml = """ + toc: + - toc: observability:// + path_prefix: /serverless/observability + - toc: serverless-search:// + path_prefix: /serverless/search + - toc: serverless-security:// + path_prefix: /serverless/security + """; + + var siteNavFile = SiteNavigationFile.Deserialize(siteNavYaml); + var fileSystem = SiteNavigationTestFixture.CreateMultiRepositoryFileSystem(); + + // Create DocumentationSetNavigation instances + var documentationSets = new List(); + + var observabilityContext = SiteNavigationTestFixture.CreateContext(fileSystem, "/checkouts/current/observability", output); + var observabilityDocset = DocumentationSetFile.Deserialize(fileSystem.File.ReadAllText("/checkouts/current/observability/docs/docset.yml")); + documentationSets.Add(new DocumentationSetNavigation(observabilityDocset, observabilityContext)); + + var searchContext = SiteNavigationTestFixture.CreateContext(fileSystem, "/checkouts/current/serverless-search", output); + var searchDocset = DocumentationSetFile.Deserialize(fileSystem.File.ReadAllText("/checkouts/current/serverless-search/docs/docset.yml")); + documentationSets.Add(new DocumentationSetNavigation(searchDocset, searchContext)); + + var securityContext = SiteNavigationTestFixture.CreateContext(fileSystem, "/checkouts/current/serverless-security", output); + var securityDocset = DocumentationSetFile.Deserialize(fileSystem.File.ReadAllText("/checkouts/current/serverless-security/docs/_docset.yml")); + documentationSets.Add(new DocumentationSetNavigation(securityDocset, securityContext)); + + // Create site navigation context (using any repository's filesystem) + var siteContext = SiteNavigationTestFixture.CreateContext(fileSystem, "/checkouts/current/observability", output); + + var siteNavigation = new SiteNavigation(siteNavFile, siteContext, documentationSets); + + siteNavigation.Should().NotBeNull(); + siteNavigation.NavigationItems.Should().HaveCount(3); + + var observability = siteNavigation.NavigationItems.ElementAt(0); + observability.Url.Should().Be("/serverless/observability"); + observability.NavigationTitle.Should().NotBeNullOrEmpty(); + + var search = siteNavigation.NavigationItems.ElementAt(1); + search.Url.Should().Be("/serverless/search"); + + var security = siteNavigation.NavigationItems.ElementAt(2); + security.Url.Should().Be("/serverless/security"); + } + + [Fact] + public void SiteNavigationWithNestedTocs() + { + // language=yaml + var siteNavYaml = """ + toc: + - toc: platform:// + path_prefix: /platform + children: + - toc: platform://deployment-guide + path_prefix: /platform/deployment + - toc: platform://cloud-guide + path_prefix: /platform/cloud + """; + + var siteNavFile = SiteNavigationFile.Deserialize(siteNavYaml); + var fileSystem = SiteNavigationTestFixture.CreateMultiRepositoryFileSystem(); + + // Create DocumentationSetNavigation for platform + var platformContext = SiteNavigationTestFixture.CreateContext(fileSystem, "/checkouts/current/platform", output); + var platformDocset = DocumentationSetFile.Deserialize(fileSystem.File.ReadAllText("/checkouts/current/platform/docs/docset.yml")); + var platformNav = new DocumentationSetNavigation(platformDocset, platformContext); + + var documentationSets = new List { platformNav }; + + var siteContext = SiteNavigationTestFixture.CreateContext(fileSystem, "/checkouts/current/platform", output); + + var siteNavigation = new SiteNavigation(siteNavFile, siteContext, documentationSets); + + siteNavigation.NavigationItems.Should().HaveCount(1); + + var platform = siteNavigation.NavigationItems.First() as INodeNavigationItem; + platform.Should().NotBeNull(); + platform!.Url.Should().Be("/platform"); + platform.NavigationItems.Should().HaveCount(2); + + var deployment = platform.NavigationItems.ElementAt(0); + deployment.Url.Should().Be("/platform/deployment"); + + var cloud = platform.NavigationItems.ElementAt(1); + cloud.Url.Should().Be("/platform/cloud"); + } + + [Fact] + public void SiteNavigationWithAllRepositories() + { + // language=yaml + var siteNavYaml = """ + toc: + - toc: observability:// + path_prefix: /serverless/observability + - toc: serverless-search:// + path_prefix: /serverless/search + - toc: serverless-security:// + path_prefix: /serverless/security + - toc: platform:// + path_prefix: /platform + children: + - toc: platform://deployment-guide + path_prefix: /platform/deployment + - toc: platform://cloud-guide + path_prefix: /platform/cloud + - toc: elasticsearch-reference:// + path_prefix: /elasticsearch/reference + """; + + var siteNavFile = SiteNavigationFile.Deserialize(siteNavYaml); + var fileSystem = SiteNavigationTestFixture.CreateMultiRepositoryFileSystem(); + + // Create all DocumentationSetNavigation instances + var checkoutDir = fileSystem.DirectoryInfo.New("/checkouts/current"); + var repositories = checkoutDir.GetDirectories(); + + var documentationSets = new List(); + + foreach (var repo in repositories) + { + var context = SiteNavigationTestFixture.CreateContext(fileSystem, repo.FullName, output); + + var docsetPath = fileSystem.File.Exists($"{repo.FullName}/docs/docset.yml") + ? $"{repo.FullName}/docs/docset.yml" + : $"{repo.FullName}/docs/_docset.yml"; + + var docsetYaml = fileSystem.File.ReadAllText(docsetPath); + var docset = DocumentationSetFile.Deserialize(docsetYaml); + + var navigation = new DocumentationSetNavigation(docset, context); + documentationSets.Add(navigation); + } + + var siteContext = SiteNavigationTestFixture.CreateContext( + fileSystem, "/checkouts/current/observability", output); + + var siteNavigation = new SiteNavigation(siteNavFile, siteContext, documentationSets); + + siteNavigation.Should().NotBeNull(); + siteNavigation.NavigationItems.Should().HaveCount(5); + + // Verify top-level items + var observability = siteNavigation.NavigationItems.ElementAt(0); + observability.Url.Should().Be("/serverless/observability"); + + var search = siteNavigation.NavigationItems.ElementAt(1); + search.Url.Should().Be("/serverless/search"); + + var security = siteNavigation.NavigationItems.ElementAt(2); + security.Url.Should().Be("/serverless/security"); + + var platform = siteNavigation.NavigationItems.ElementAt(3) as INodeNavigationItem; + platform.Should().NotBeNull(); + platform!.Url.Should().Be("/platform"); + platform.NavigationItems.Should().HaveCount(2); + + var elasticsearch = siteNavigation.NavigationItems.ElementAt(4); + elasticsearch.Url.Should().Be("/elasticsearch/reference"); + } + + [Fact] + public void DocumentationSetNavigationHasCorrectStructure() + { + var fileSystem = SiteNavigationTestFixture.CreateMultiRepositoryFileSystem(); + + // Test observability repository structure + var observabilityContext = SiteNavigationTestFixture.CreateContext(fileSystem, "/checkouts/current/observability", output); + var observabilityDocset = DocumentationSetFile.Deserialize(fileSystem.File.ReadAllText("/checkouts/current/observability/docs/docset.yml")); + var observabilityNav = new DocumentationSetNavigation(observabilityDocset, observabilityContext); + + observabilityNav.NavigationTitle.Should().Be("serverless-observability"); + observabilityNav.NavigationItems.Should().HaveCount(3); // index.md, getting-started folder, monitoring folder + + var indexFile = observabilityNav.NavigationItems.ElementAt(0); + indexFile.Should().BeOfType(); + indexFile.Url.Should().Be("/"); + + var gettingStarted = observabilityNav.NavigationItems.ElementAt(1); + gettingStarted.Should().BeOfType(); + var gettingStartedFolder = (FolderNavigation)gettingStarted; + gettingStartedFolder.NavigationItems.Should().HaveCount(2); // quick-start.md, installation.md + + var monitoring = observabilityNav.NavigationItems.ElementAt(2); + monitoring.Should().BeOfType(); + var monitoringFolder = (FolderNavigation)monitoring; + monitoringFolder.NavigationItems.Should().HaveCount(4); // index.md, logs.md, metrics.md, traces.md + } + + [Fact] + public void DocumentationSetWithNestedTocs() + { + var fileSystem = SiteNavigationTestFixture.CreateMultiRepositoryFileSystem(); + + // Test platform repository with nested TOCs + var platformContext = SiteNavigationTestFixture.CreateContext(fileSystem, "/checkouts/current/platform", output); + var platformDocset = DocumentationSetFile.Deserialize(fileSystem.File.ReadAllText("/checkouts/current/platform/docs/docset.yml")); + var platformNav = new DocumentationSetNavigation(platformDocset, platformContext); + + platformNav.NavigationTitle.Should().Be("platform"); + platformNav.NavigationItems.Should().HaveCount(3); // index.md, deployment-guide TOC, cloud-guide TOC + + var indexFile = platformNav.NavigationItems.ElementAt(0); + indexFile.Should().BeOfType(); + indexFile.Url.Should().Be("/"); + + var deploymentGuide = platformNav.NavigationItems.ElementAt(1); + deploymentGuide.Should().BeOfType(); + deploymentGuide.Url.Should().Be("/deployment-guide"); + var deploymentToc = (TableOfContentsNavigation)deploymentGuide; + deploymentToc.NavigationItems.Should().HaveCount(2); // index.md, self-managed folder + + var cloudGuide = platformNav.NavigationItems.ElementAt(2); + cloudGuide.Should().BeOfType(); + cloudGuide.Url.Should().Be("/cloud-guide"); + var cloudToc = (TableOfContentsNavigation)cloudGuide; + cloudToc.NavigationItems.Should().HaveCount(3); // index.md, aws folder, azure folder + } + + [Fact] + public void DocumentationSetWithUnderscoreDocset() + { + var fileSystem = SiteNavigationTestFixture.CreateMultiRepositoryFileSystem(); + + // Test serverless-security repository with _docset.yml + var securityContext = SiteNavigationTestFixture.CreateContext(fileSystem, "/checkouts/current/serverless-security", output); + var securityDocset = DocumentationSetFile.Deserialize(fileSystem.File.ReadAllText("/checkouts/current/serverless-security/docs/_docset.yml")); + var securityNav = new DocumentationSetNavigation(securityDocset, securityContext); + + securityNav.NavigationTitle.Should().Be("serverless-security"); + securityNav.NavigationItems.Should().HaveCount(3); // index.md, authentication folder, authorization folder + + var authentication = securityNav.NavigationItems.ElementAt(1); + authentication.Should().BeOfType(); + var authenticationFolder = (FolderNavigation)authentication; + authenticationFolder.NavigationItems.Should().HaveCount(3); // index.md, api-keys.md, oauth.md + + var authorization = securityNav.NavigationItems.ElementAt(2); + authorization.Should().BeOfType(); + var authorizationFolder = (FolderNavigation)authorization; + authorizationFolder.NavigationItems.Should().HaveCount(2); // index.md, rbac.md + } + + [Fact] + public void SiteNavigationAppliesPathPrefixToAllUrls() + { + // language=yaml + var siteNavYaml = """ + toc: + - toc: observability:// + path_prefix: /serverless/observability + """; + + var siteNavFile = SiteNavigationFile.Deserialize(siteNavYaml); + var fileSystem = SiteNavigationTestFixture.CreateMultiRepositoryFileSystem(); + + var observabilityContext = SiteNavigationTestFixture.CreateContext(fileSystem, "/checkouts/current/observability", output); + var observabilityDocset = DocumentationSetFile.Deserialize(fileSystem.File.ReadAllText("/checkouts/current/observability/docs/docset.yml")); + var documentationSets = new List { new(observabilityDocset, observabilityContext) }; + + var siteContext = SiteNavigationTestFixture.CreateContext(fileSystem, "/checkouts/current/observability", output); + var siteNavigation = new SiteNavigation(siteNavFile, siteContext, documentationSets); + + // Verify root URL has path prefix + var root = siteNavigation.NavigationItems.First(); + root.Url.Should().StartWith("/serverless/observability"); + + // Verify all nested items also have the path prefix + if (root is INodeNavigationItem nodeItem) + { + foreach (var item in nodeItem.NavigationItems) + { + item.Url.Should().StartWith("/serverless/observability"); + } + } + } + + [Fact] + public void SiteNavigationWithNestedTocsAppliesCorrectPathPrefixes() + { + // language=yaml + var siteNavYaml = """ + toc: + - toc: platform:// + path_prefix: /platform + children: + - toc: platform://deployment-guide + path_prefix: /platform/deployment + - toc: platform://cloud-guide + path_prefix: /platform/cloud + """; + + var siteNavFile = SiteNavigationFile.Deserialize(siteNavYaml); + var fileSystem = SiteNavigationTestFixture.CreateMultiRepositoryFileSystem(); + + var platformContext = SiteNavigationTestFixture.CreateContext(fileSystem, "/checkouts/current/platform", output); + var platformDocset = DocumentationSetFile.Deserialize(fileSystem.File.ReadAllText("/checkouts/current/platform/docs/docset.yml")); + var documentationSets = new List { new(platformDocset, platformContext) }; + + var siteContext = SiteNavigationTestFixture.CreateContext(fileSystem, "/checkouts/current/platform", output); + var siteNavigation = new SiteNavigation(siteNavFile, siteContext, documentationSets); + + var platform = siteNavigation.NavigationItems.First() as INodeNavigationItem; + platform.Should().NotBeNull(); + platform!.Url.Should().Be("/platform"); + + // Verify child TOCs have their specific path prefixes + var deployment = platform.NavigationItems.ElementAt(0); + deployment.Url.Should().StartWith("/platform/deployment"); + + var cloud = platform.NavigationItems.ElementAt(1); + cloud.Url.Should().StartWith("/platform/cloud"); + } + + [Fact] + public void SiteNavigationRequiresPathPrefix() + { + // language=yaml - missing path_prefix + var siteNavYaml = """ + toc: + - toc: observability:// + """; + + var siteNavFile = SiteNavigationFile.Deserialize(siteNavYaml); + var fileSystem = SiteNavigationTestFixture.CreateMultiRepositoryFileSystem(); + + var observabilityContext = SiteNavigationTestFixture.CreateContext(fileSystem, "/checkouts/current/observability", output); + var observabilityDocset = DocumentationSetFile.Deserialize(fileSystem.File.ReadAllText("/checkouts/current/observability/docs/docset.yml")); + var documentationSets = new List { new(observabilityDocset, observabilityContext) }; + + var siteContext = SiteNavigationTestFixture.CreateContext(fileSystem, "/checkouts/current/observability", output); + var siteNavigation = new SiteNavigation(siteNavFile, siteContext, documentationSets); + + // Should have 0 items because path_prefix was required and missing + siteNavigation.NavigationItems.Should().BeEmpty(); + } +} diff --git a/tests/Navigation.Tests/Assembler/SiteNavigationTestFixture.cs b/tests/Navigation.Tests/Assembler/SiteNavigationTestFixture.cs new file mode 100644 index 000000000..390c5231d --- /dev/null +++ b/tests/Navigation.Tests/Assembler/SiteNavigationTestFixture.cs @@ -0,0 +1,235 @@ +// 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.TestingHelpers; +using Elastic.Documentation.Navigation.Tests.Isolation; + +namespace Elastic.Documentation.Navigation.Tests.Assembler; + +public class SiteNavigationTestFixture +{ + public static MockFileSystem CreateMultiRepositoryFileSystem() + { + var fileSystem = new MockFileSystem(); + + // Repository 1: serverless-observability + SetupServerlessObservabilityRepository(fileSystem); + + // Repository 2: serverless-search + SetupServerlessSearchRepository(fileSystem); + + // Repository 3: serverless-security + SetupServerlessSecurityRepository(fileSystem); + + // Repository 4: platform + SetupPlatformRepository(fileSystem); + + // Repository 5: elasticsearch-reference + SetupElasticsearchReferenceRepository(fileSystem); + + return fileSystem; + } + + private static void SetupServerlessObservabilityRepository(MockFileSystem fileSystem) + { + var baseDir = "/checkouts/current/observability"; + fileSystem.AddDirectory(baseDir); + + // Add docset.yml + // language=yaml + var docsetYaml = """ + project: serverless-observability + toc: + - file: index.md + - folder: getting-started + children: + - file: quick-start.md + - file: installation.md + - folder: monitoring + children: + - file: index.md + - file: logs.md + - file: metrics.md + - file: traces.md + """; + fileSystem.AddFile($"{baseDir}/docs/docset.yml", new MockFileData(docsetYaml)); + + // Add markdown files + fileSystem.AddFile($"{baseDir}/docs/index.md", new MockFileData("# Serverless Observability")); + fileSystem.AddFile($"{baseDir}/docs/getting-started/quick-start.md", new MockFileData("# Quick Start")); + fileSystem.AddFile($"{baseDir}/docs/getting-started/installation.md", new MockFileData("# Installation")); + fileSystem.AddFile($"{baseDir}/docs/monitoring/index.md", new MockFileData("# Monitoring")); + fileSystem.AddFile($"{baseDir}/docs/monitoring/logs.md", new MockFileData("# Logs")); + fileSystem.AddFile($"{baseDir}/docs/monitoring/metrics.md", new MockFileData("# Metrics")); + fileSystem.AddFile($"{baseDir}/docs/monitoring/traces.md", new MockFileData("# Traces")); + } + + private static void SetupServerlessSearchRepository(MockFileSystem fileSystem) + { + var baseDir = "/checkouts/current/serverless-search"; + fileSystem.AddDirectory(baseDir); + + // Add docset.yml + // language=yaml + var docsetYaml = """ + project: serverless-search + toc: + - file: index.md + - folder: indexing + children: + - file: index.md + - file: documents.md + - file: bulk-api.md + - folder: searching + children: + - file: index.md + - file: query-dsl.md + """; + fileSystem.AddFile($"{baseDir}/docs/docset.yml", new MockFileData(docsetYaml)); + + // Add markdown files + fileSystem.AddFile($"{baseDir}/docs/index.md", new MockFileData("# Serverless Search")); + fileSystem.AddFile($"{baseDir}/docs/indexing/index.md", new MockFileData("# Indexing")); + fileSystem.AddFile($"{baseDir}/docs/indexing/documents.md", new MockFileData("# Documents")); + fileSystem.AddFile($"{baseDir}/docs/indexing/bulk-api.md", new MockFileData("# Bulk API")); + fileSystem.AddFile($"{baseDir}/docs/searching/index.md", new MockFileData("# Searching")); + fileSystem.AddFile($"{baseDir}/docs/searching/query-dsl.md", new MockFileData("# Query DSL")); + } + + private static void SetupServerlessSecurityRepository(MockFileSystem fileSystem) + { + var baseDir = "/checkouts/current/serverless-security"; + fileSystem.AddDirectory(baseDir); + + // Add docset.yml with underscore prefix + // language=yaml + var docsetYaml = """ + project: serverless-security + toc: + - file: index.md + - folder: authentication + children: + - file: index.md + - file: api-keys.md + - file: oauth.md + - folder: authorization + children: + - file: index.md + - file: rbac.md + """; + fileSystem.AddFile($"{baseDir}/docs/_docset.yml", new MockFileData(docsetYaml)); + + // Add markdown files + fileSystem.AddFile($"{baseDir}/docs/index.md", new MockFileData("# Serverless Security")); + fileSystem.AddFile($"{baseDir}/docs/authentication/index.md", new MockFileData("# Authentication")); + fileSystem.AddFile($"{baseDir}/docs/authentication/api-keys.md", new MockFileData("# API Keys")); + fileSystem.AddFile($"{baseDir}/docs/authentication/oauth.md", new MockFileData("# OAuth")); + fileSystem.AddFile($"{baseDir}/docs/authorization/index.md", new MockFileData("# Authorization")); + fileSystem.AddFile($"{baseDir}/docs/authorization/rbac.md", new MockFileData("# RBAC")); + } + + private static void SetupPlatformRepository(MockFileSystem fileSystem) + { + var baseDir = "/checkouts/current/platform"; + fileSystem.AddDirectory(baseDir); + + // Add docset.yml + // language=yaml + var docsetYaml = """ + project: platform + toc: + - file: index.md + - toc: deployment-guide + - toc: cloud-guide + """; + fileSystem.AddFile($"{baseDir}/docs/docset.yml", new MockFileData(docsetYaml)); + fileSystem.AddFile($"{baseDir}/docs/index.md", new MockFileData("# Platform")); + + // Deployment guide sub-TOC + var deploymentBaseDir = $"{baseDir}/docs/deployment-guide"; + fileSystem.AddDirectory(deploymentBaseDir); + // language=yaml + var deploymentTocYaml = """ + toc: + - file: index.md + - folder: self-managed + children: + - file: installation.md + - file: configuration.md + """; + fileSystem.AddFile($"{deploymentBaseDir}/toc.yml", new MockFileData(deploymentTocYaml)); + fileSystem.AddFile($"{deploymentBaseDir}/index.md", new MockFileData("# Deployment Guide")); + fileSystem.AddFile($"{deploymentBaseDir}/self-managed/installation.md", new MockFileData("# Installation")); + fileSystem.AddFile($"{deploymentBaseDir}/self-managed/configuration.md", new MockFileData("# Configuration")); + + // Cloud guide sub-TOC + var cloudBaseDir = $"{baseDir}/docs/cloud-guide"; + fileSystem.AddDirectory(cloudBaseDir); + // language=yaml + var cloudTocYaml = """ + toc: + - file: index.md + - folder: aws + children: + - file: setup.md + - folder: azure + children: + - file: setup.md + """; + fileSystem.AddFile($"{cloudBaseDir}/toc.yml", new MockFileData(cloudTocYaml)); + fileSystem.AddFile($"{cloudBaseDir}/index.md", new MockFileData("# Cloud Guide")); + fileSystem.AddFile($"{cloudBaseDir}/aws/setup.md", new MockFileData("# AWS Setup")); + fileSystem.AddFile($"{cloudBaseDir}/azure/setup.md", new MockFileData("# Azure Setup")); + } + + private static void SetupElasticsearchReferenceRepository(MockFileSystem fileSystem) + { + var baseDir = "/checkouts/current/elasticsearch-reference"; + fileSystem.AddDirectory(baseDir); + + // Add docset.yml + // language=yaml + var docsetYaml = """ + project: elasticsearch-reference + toc: + - file: index.md + - folder: rest-apis + children: + - file: index.md + - file: document-apis.md + - file: search-apis.md + - folder: query-dsl + children: + - file: index.md + - file: term-queries.md + - file: full-text-queries.md + """; + fileSystem.AddFile($"{baseDir}/docs/docset.yml", new MockFileData(docsetYaml)); + + // Add markdown files + fileSystem.AddFile($"{baseDir}/docs/index.md", new MockFileData("# Elasticsearch Reference")); + fileSystem.AddFile($"{baseDir}/docs/rest-apis/index.md", new MockFileData("# REST APIs")); + fileSystem.AddFile($"{baseDir}/docs/rest-apis/document-apis.md", new MockFileData("# Document APIs")); + fileSystem.AddFile($"{baseDir}/docs/rest-apis/search-apis.md", new MockFileData("# Search APIs")); + fileSystem.AddFile($"{baseDir}/docs/query-dsl/index.md", new MockFileData("# Query DSL")); + fileSystem.AddFile($"{baseDir}/docs/query-dsl/term-queries.md", new MockFileData("# Term Queries")); + fileSystem.AddFile($"{baseDir}/docs/query-dsl/full-text-queries.md", new MockFileData("# Full Text Queries")); + } + + public static TestDocumentationSetContext CreateContext(MockFileSystem fileSystem, string repositoryPath, ITestOutputHelper output) + { + var sourceDir = fileSystem.DirectoryInfo.New($"{repositoryPath}/docs"); + var outputDir = fileSystem.DirectoryInfo.New("/output"); + + // Try to find docset.yml or _docset.yml + var configPath = fileSystem.File.Exists($"{sourceDir.FullName}/docset.yml") + ? fileSystem.FileInfo.New($"{sourceDir.FullName}/docset.yml") + : fileSystem.FileInfo.New($"{sourceDir.FullName}/_docset.yml"); + + // Extract repository name from path (e.g., "/checkouts/current/platform" -> "platform") + var repositoryName = fileSystem.Path.GetFileName(repositoryPath); + + return new TestDocumentationSetContext(fileSystem, sourceDir, outputDir, configPath, output, repositoryName); + } +} diff --git a/tests/Navigation.Tests/Assembler/SiteNavigationTests.cs b/tests/Navigation.Tests/Assembler/SiteNavigationTests.cs new file mode 100644 index 000000000..3c8a162e2 --- /dev/null +++ b/tests/Navigation.Tests/Assembler/SiteNavigationTests.cs @@ -0,0 +1,94 @@ +// 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.TestingHelpers; +using Elastic.Documentation.Configuration.DocSet; +using Elastic.Documentation.Navigation.Assembler; +using Elastic.Documentation.Navigation.Isolated; +using Elastic.Documentation.Navigation.Tests.Isolation; +using FluentAssertions; + +namespace Elastic.Documentation.Navigation.Tests.Assembler; + +public class SiteNavigationTests(ITestOutputHelper output) +{ + private TestDocumentationSetContext CreateContext(MockFileSystem? fileSystem = null) + { + fileSystem ??= new MockFileSystem(); + var sourceDir = fileSystem.DirectoryInfo.New("/docs"); + var outputDir = fileSystem.DirectoryInfo.New("/output"); + var configPath = fileSystem.FileInfo.New("/docs/navigation.yml"); + + return new TestDocumentationSetContext(fileSystem, sourceDir, outputDir, configPath, output); + } + + [Fact] + public void ConstructorCreatesSiteNavigation() + { + // language=yaml + var yaml = """ + toc: + - toc: observability:// + path_prefix: /serverless/observability + - toc: serverless-search:// + path_prefix: /serverless/search + """; + + var siteNavFile = SiteNavigationFile.Deserialize(yaml); + var fileSystem = SiteNavigationTestFixture.CreateMultiRepositoryFileSystem(); + + // Create DocumentationSetNavigation instances for the referenced repos + var observabilityContext = SiteNavigationTestFixture.CreateContext(fileSystem, "/checkouts/current/observability", output); + var observabilityDocset = DocumentationSetFile.Deserialize(fileSystem.File.ReadAllText("/checkouts/current/observability/docs/docset.yml")); + var observabilityNav = new DocumentationSetNavigation(observabilityDocset, observabilityContext); + + var searchContext = SiteNavigationTestFixture.CreateContext(fileSystem, "/checkouts/current/serverless-search", output); + var searchDocset = DocumentationSetFile.Deserialize(fileSystem.File.ReadAllText("/checkouts/current/serverless-search/docs/docset.yml")); + var searchNav = new DocumentationSetNavigation(searchDocset, searchContext); + + var documentationSets = new List { observabilityNav, searchNav }; + + var siteContext = SiteNavigationTestFixture.CreateContext(fileSystem, "/checkouts/current/observability", output); + var navigation = new SiteNavigation(siteNavFile, siteContext, documentationSets); + + navigation.Should().NotBeNull(); + navigation.Url.Should().Be("/"); + navigation.NavigationTitle.Should().Be("Site Navigation"); + navigation.NavigationItems.Should().HaveCount(2); + } + + [Fact] + public void SiteNavigationWithNestedChildren() + { + // language=yaml + var yaml = """ + toc: + - toc: platform:// + path_prefix: /platform + children: + - toc: platform://deployment-guide + path_prefix: /platform/deployment + - toc: platform://cloud-guide + path_prefix: /platform/cloud + """; + + var siteNavFile = SiteNavigationFile.Deserialize(yaml); + var fileSystem = SiteNavigationTestFixture.CreateMultiRepositoryFileSystem(); + + // Create DocumentationSetNavigation for platform + var platformContext = SiteNavigationTestFixture.CreateContext(fileSystem, "/checkouts/current/platform", output); + var platformDocset = DocumentationSetFile.Deserialize(fileSystem.File.ReadAllText("/checkouts/current/platform/docs/docset.yml")); + var platformNav = new DocumentationSetNavigation(platformDocset, platformContext); + + var documentationSets = new List { platformNav }; + + var siteContext = SiteNavigationTestFixture.CreateContext(fileSystem, "/checkouts/current/platform", output); + var navigation = new SiteNavigation(siteNavFile, siteContext, documentationSets); + + navigation.NavigationItems.Should().HaveCount(1); + + var platform = navigation.NavigationItems.First(); + platform.Should().NotBeNull(); + } +} diff --git a/tests/Navigation.Tests/Isolation/ConstructorTests.cs b/tests/Navigation.Tests/Isolation/ConstructorTests.cs new file mode 100644 index 000000000..0bf88ce5b --- /dev/null +++ b/tests/Navigation.Tests/Isolation/ConstructorTests.cs @@ -0,0 +1,274 @@ +// 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.TestingHelpers; +using Elastic.Documentation.Configuration.DocSet; +using Elastic.Documentation.Navigation.Isolated; +using FluentAssertions; + +namespace Elastic.Documentation.Navigation.Tests.Isolation; + +public class ConstructorTests(ITestOutputHelper output) : DocumentationSetNavigationTestBase(output) +{ + [Fact] + public void ConstructorInitializesRootProperties() + { + // language=yaml + var yaml = """ + project: 'test-project' + toc: + - file: index.md + """; + + var docSet = DocumentationSetFile.Deserialize(yaml); + var context = CreateContext(); + + var navigation = new DocumentationSetNavigation(docSet, context); + + navigation.NavigationRoot.Should().BeSameAs(navigation); + navigation.Parent.Should().BeNull(); + navigation.Depth.Should().Be(0); + navigation.Hidden.Should().BeFalse(); + navigation.IsCrossLink.Should().BeFalse(); + navigation.Id.Should().NotBeNullOrEmpty(); + navigation.NavigationTitle.Should().Be("test-project"); + navigation.IsUsingNavigationDropdown.Should().BeFalse(); + navigation.Url.Should().Be("/"); + } + + [Fact] + public void ConstructorSetsIsUsingNavigationDropdownFromFeatures() + { + // language=yaml + var yaml = """ + project: 'test-project' + features: + primary-nav: true + toc: + - file: index.md + """; + + var docSet = DocumentationSetFile.Deserialize(yaml); + var context = CreateContext(); + + var navigation = new DocumentationSetNavigation(docSet, context); + + navigation.IsUsingNavigationDropdown.Should().BeTrue(); + } + + [Fact] + public void ConstructorCreatesFileNavigationLeafFromFileRef() + { + // language=yaml + var yaml = """ + project: 'test-project' + toc: + - file: getting-started.md + """; + + var docSet = DocumentationSetFile.Deserialize(yaml); + var context = CreateContext(); + + var navigation = new DocumentationSetNavigation(docSet, context); + + navigation.NavigationItems.Should().HaveCount(1); + var fileNav = navigation.NavigationItems.First().Should().BeOfType().Subject; + fileNav.NavigationTitle.Should().Be("getting-started"); + fileNav.Url.Should().Be("/getting-started"); + fileNav.Hidden.Should().BeFalse(); + fileNav.NavigationRoot.Should().BeSameAs(navigation); + fileNav.Parent.Should().BeNull(); + } + + [Fact] + public void ConstructorCreatesHiddenFileNavigationLeaf() + { + // language=yaml + var yaml = """ + project: 'test-project' + toc: + - hidden: 404.md + """; + + var docSet = DocumentationSetFile.Deserialize(yaml); + var context = CreateContext(); + + var navigation = new DocumentationSetNavigation(docSet, context); + + navigation.NavigationItems.Should().HaveCount(1); + var fileNav = navigation.NavigationItems.First().Should().BeOfType().Subject; + fileNav.Hidden.Should().BeTrue(); + fileNav.Url.Should().Be("/404"); + } + + [Fact] + public void ConstructorCreatesCrossLinkNavigation() + { + // language=yaml + var yaml = """ + project: 'test-project' + toc: + - title: "External Guide" + crosslink: docs-content://guide.md + """; + + var docSet = DocumentationSetFile.Deserialize(yaml); + var context = CreateContext(); + + var navigation = new DocumentationSetNavigation(docSet, context); + + navigation.NavigationItems.Should().HaveCount(1); + var crossLink = navigation.NavigationItems.First().Should().BeOfType().Subject; + crossLink.NavigationTitle.Should().Be("External Guide"); + crossLink.Url.Should().Be("docs-content://guide.md"); + crossLink.IsCrossLink.Should().BeTrue(); + } + + [Fact] + public void ConstructorCreatesFolderNavigationWithChildren() + { + // language=yaml + var yaml = """ + project: 'test-project' + toc: + - folder: setup + children: + - file: index.md + - file: install.md + """; + + var docSet = DocumentationSetFile.Deserialize(yaml); + var context = CreateContext(); + + var navigation = new DocumentationSetNavigation(docSet, context); + + navigation.NavigationItems.Should().HaveCount(1); + var folder = navigation.NavigationItems.First().Should().BeOfType().Subject; + folder.Depth.Should().Be(1); + folder.Url.Should().Be("/setup"); + folder.NavigationItems.Should().HaveCount(2); + + var firstFile = folder.NavigationItems.ElementAt(0).Should().BeOfType().Subject; + firstFile.Url.Should().Be("/setup"); // index.md becomes /setup + firstFile.Parent.Should().BeSameAs(folder); + + var secondFile = folder.NavigationItems.ElementAt(1).Should().BeOfType().Subject; + secondFile.Url.Should().Be("/setup/install"); + } + + [Fact] + public void ConstructorCreatesTableOfContentsNavigationWithChildren() + { + // language=yaml + var yaml = """ + project: 'test-project' + toc: + - toc: api + """; + + // language=yaml + var tocYaml = """ + toc: + - file: index.md + """; + + var fileSystem = new MockFileSystem(); + fileSystem.AddDirectory("/docs/api"); + fileSystem.AddFile("/docs/api/toc.yml", new MockFileData(tocYaml)); + var docSet = DocumentationSetFile.Deserialize(yaml); + var context = CreateContext(fileSystem); + + var navigation = new DocumentationSetNavigation(docSet, context); + + navigation.NavigationItems.Should().HaveCount(1); + var toc = navigation.NavigationItems.First().Should().BeOfType().Subject; + toc.Depth.Should().Be(1); + toc.Url.Should().Be("/api"); + toc.NavigationItems.Should().HaveCount(1); + + var file = toc.NavigationItems.First().Should().BeOfType().Subject; + file.Url.Should().Be("/api"); // index.md becomes /api + file.Parent.Should().BeSameAs(toc); + file.NavigationRoot.Should().BeSameAs(navigation); + } + + [Fact] + public void ConstructorReadsTableOfContentsFromTocYmlFile() + { + // language=yaml + var docSetYaml = """ + project: 'test-project' + toc: + - toc: api + """; + + // language=yaml + var tocYaml = """ + toc: + - file: overview.md + - file: reference.md + """; + + var fileSystem = new MockFileSystem(); + fileSystem.AddDirectory("/docs/api"); + fileSystem.AddFile("/docs/api/toc.yml", new MockFileData(tocYaml)); + + var docSet = DocumentationSetFile.Deserialize(docSetYaml); + var context = CreateContext(fileSystem); + + var navigation = new DocumentationSetNavigation(docSet, context); + + navigation.NavigationItems.Should().HaveCount(1); + var toc = navigation.NavigationItems.First().Should().BeOfType().Subject; + toc.NavigationItems.Should().HaveCount(2); + + var overview = toc.NavigationItems.ElementAt(0).Should().BeOfType().Subject; + overview.Url.Should().Be("/api/overview"); + + var reference = toc.NavigationItems.ElementAt(1).Should().BeOfType().Subject; + reference.Url.Should().Be("/api/reference"); + } + + [Fact] + public void ConstructorProcessesTocYmlItemsBeforeChildrenFromNavigation() + { + // language=yaml + var docSetYaml = """ + project: 'test-project' + toc: + - toc: api + children: + - toc: extra + """; + + // language=yaml + var tocYaml = """ + toc: + - file: from-toc.md + """; + + var fileSystem = new MockFileSystem(); + fileSystem.AddDirectory("/docs/api"); + fileSystem.AddDirectory("/docs/api/extra"); + fileSystem.AddFile("/docs/api/toc.yml", new MockFileData(tocYaml)); + + var docSet = DocumentationSetFile.Deserialize(docSetYaml); + var context = CreateContext(fileSystem); + + var navigation = new DocumentationSetNavigation(docSet, context); + + var toc = navigation.NavigationItems.First().Should().BeOfType().Subject; + toc.NavigationItems.Should().HaveCount(2); + + // First item should be from toc.yml + var fromToc = toc.NavigationItems.ElementAt(0).Should().BeOfType().Subject; + fromToc.NavigationTitle.Should().Be("from-toc"); + fromToc.Url.Should().Be("/api/from-toc"); + + // Second item should be from docset.yml children (a nested TOC) + var fromNav = toc.NavigationItems.ElementAt(1).Should().BeOfType().Subject; + fromNav.NavigationTitle.Should().Be("extra"); + fromNav.Url.Should().Be("/api/extra"); + } +} diff --git a/tests/Navigation.Tests/Isolation/DocumentationSetNavigationTestBase.cs b/tests/Navigation.Tests/Isolation/DocumentationSetNavigationTestBase.cs new file mode 100644 index 000000000..b81d584eb --- /dev/null +++ b/tests/Navigation.Tests/Isolation/DocumentationSetNavigationTestBase.cs @@ -0,0 +1,20 @@ +// 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.TestingHelpers; + +namespace Elastic.Documentation.Navigation.Tests.Isolation; + +public abstract class DocumentationSetNavigationTestBase(ITestOutputHelper output) +{ + protected TestDocumentationSetContext CreateContext(MockFileSystem? fileSystem = null) + { + fileSystem ??= new MockFileSystem(); + var sourceDir = fileSystem.DirectoryInfo.New("/docs"); + var outputDir = fileSystem.DirectoryInfo.New("/output"); + var configPath = fileSystem.FileInfo.New("/docs/docset.yml"); + + return new TestDocumentationSetContext(fileSystem, sourceDir, outputDir, configPath, output, "docs-builder"); + } +} diff --git a/tests/Navigation.Tests/Isolation/DynamicUrlTests.cs b/tests/Navigation.Tests/Isolation/DynamicUrlTests.cs new file mode 100644 index 000000000..edc78c6ea --- /dev/null +++ b/tests/Navigation.Tests/Isolation/DynamicUrlTests.cs @@ -0,0 +1,169 @@ +// 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.TestingHelpers; +using Elastic.Documentation.Configuration.DocSet; +using Elastic.Documentation.Navigation.Isolated; +using FluentAssertions; + +namespace Elastic.Documentation.Navigation.Tests.Isolation; + +public class DynamicUrlTests(ITestOutputHelper output) : DocumentationSetNavigationTestBase(output) +{ + [Fact] + public void DynamicUrlUpdatesWhenRootUrlChanges() + { + // language=yaml + var yaml = """ + project: 'test-project' + toc: + - folder: setup + children: + - file: install.md + """; + + var docSet = DocumentationSetFile.Deserialize(yaml); + var context = CreateContext(); + + var navigation = new DocumentationSetNavigation(docSet, context); + var folder = navigation.NavigationItems.First() as FolderNavigation; + var file = folder!.NavigationItems.First(); + + // Initial URL + file.Url.Should().Be("/setup/install"); + + // Change root URL + navigation.PathPrefixProvider = new PathPrefixProvider("/v8.0"); + + // URLs should update dynamically + // Since folder has no index child, its URL is the first child's URL + folder.Url.Should().Be("/v8.0/setup/install"); + file.Url.Should().Be("/v8.0/setup/install"); + + // Change root URL + navigation.PathPrefixProvider = new PathPrefixProvider("/v9.0"); + + // URLs should update dynamically + // Since folder has no index child, its URL is the first child's URL + folder.Url.Should().Be("/v9.0/setup/install"); + file.Url.Should().Be("/v9.0/setup/install"); + } + + [Fact] + public void UrlRootPropagatesCorrectlyThroughFolders() + { + // language=yaml + var yaml = """ + project: 'test-project' + toc: + - folder: outer + children: + - folder: inner + children: + - file: deep.md + """; + + var docSet = DocumentationSetFile.Deserialize(yaml); + var context = CreateContext(); + + var navigation = new DocumentationSetNavigation(docSet, context); + var outerFolder = navigation.NavigationItems.First() as FolderNavigation; + var innerFolder = outerFolder!.NavigationItems.First() as FolderNavigation; + var file = innerFolder!.NavigationItems.First(); + + file.Url.Should().Be("/outer/inner/deep"); + + // Change root URL + navigation.PathPrefixProvider = new PathPrefixProvider("/base"); + + file.Url.Should().Be("/base/outer/inner/deep"); + } + + [Fact] + public void FolderWithoutIndexUsesFirstChildUrl() + { + // language=yaml + var yaml = """ + project: 'test-project' + toc: + - folder: guides + children: + - file: getting-started.md + - file: advanced.md + """; + + var docSet = DocumentationSetFile.Deserialize(yaml); + var context = CreateContext(); + + var navigation = new DocumentationSetNavigation(docSet, context); + var folder = navigation.NavigationItems.First() as FolderNavigation; + + // Folder has no index.md, so URL should be the first child's URL + folder!.Url.Should().Be("/guides/getting-started"); + } + + [Fact] + public void FolderWithIndexUsesOwnUrl() + { + // language=yaml + var yaml = """ + project: 'test-project' + toc: + - folder: guides + children: + - file: index.md + - file: advanced.md + """; + + var docSet = DocumentationSetFile.Deserialize(yaml); + var context = CreateContext(); + + var navigation = new DocumentationSetNavigation(docSet, context); + var folder = navigation.NavigationItems.First() as FolderNavigation; + + // Folder has index.md, so URL should be the folder path + folder!.Url.Should().Be("/guides"); + } + + [Fact] + public void UrlRootChangesForTableOfContentsNavigation() + { + // language=yaml + var yaml = """ + project: 'test-project' + toc: + - folder: guides + children: + - toc: api + """; + + // language=yaml + var tocYaml = """ + toc: + - file: reference.md + """; + + var fileSystem = new MockFileSystem(); + fileSystem.AddDirectory("/docs/guides/api"); + fileSystem.AddFile("/docs/guides/api/toc.yml", new MockFileData(tocYaml)); + var docSet = DocumentationSetFile.Deserialize(yaml); + var context = CreateContext(fileSystem); + + var navigation = new DocumentationSetNavigation(docSet, context); + var folder = navigation.NavigationItems.First() as FolderNavigation; + var toc = folder!.NavigationItems.First() as TableOfContentsNavigation; + var file = toc!.NavigationItems.First(); + + // The TOC becomes the new URL root, so the file URL is based on TOC's URL + toc.Url.Should().Be("/guides/api"); + file.Url.Should().Be("/guides/api/reference"); + + // Change root URL + navigation.PathPrefixProvider = new PathPrefixProvider("/v2"); + + // Both TOC and file URLs should update + toc.Url.Should().Be("/v2/guides/api"); + file.Url.Should().Be("/v2/guides/api/reference"); + } +} diff --git a/tests/Navigation.Tests/Isolation/FileNavigationTests.cs b/tests/Navigation.Tests/Isolation/FileNavigationTests.cs new file mode 100644 index 000000000..d392dcb0a --- /dev/null +++ b/tests/Navigation.Tests/Isolation/FileNavigationTests.cs @@ -0,0 +1,188 @@ +// 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.Navigation.Isolated; +using FluentAssertions; + +namespace Elastic.Documentation.Navigation.Tests.Isolation; + +public class FileNavigationTests(ITestOutputHelper output) : DocumentationSetNavigationTestBase(output) +{ + [Fact] + public void FileWithNoChildrenCreatesFileNavigationLeaf() + { + // language=yaml + var yaml = """ + project: 'test-project' + toc: + - file: getting-started.md + """; + + var docSet = DocumentationSetFile.Deserialize(yaml); + var context = CreateContext(); + + var navigation = new DocumentationSetNavigation(docSet, context); + + navigation.NavigationItems.Should().HaveCount(1); + var fileNav = navigation.NavigationItems.First().Should().BeOfType().Subject; + fileNav.Url.Should().Be("/getting-started"); + } + + [Fact] + public void FileWithChildrenCreatesFileNavigation() + { + // language=yaml + var yaml = """ + project: 'test-project' + toc: + - file: guide.md + children: + - file: section1.md + - file: section2.md + """; + + var docSet = DocumentationSetFile.Deserialize(yaml); + var context = CreateContext(); + + var navigation = new DocumentationSetNavigation(docSet, context); + + navigation.NavigationItems.Should().HaveCount(1); + var fileNav = navigation.NavigationItems.First().Should().BeOfType().Subject; + fileNav.Url.Should().Be("/guide"); + fileNav.NavigationItems.Should().HaveCount(2); + + var section1 = fileNav.NavigationItems.ElementAt(0).Should().BeOfType().Subject; + section1.Url.Should().Be("/guide/section1"); + section1.Parent.Should().BeSameAs(fileNav); + + var section2 = fileNav.NavigationItems.ElementAt(1).Should().BeOfType().Subject; + section2.Url.Should().Be("/guide/section2"); + section2.Parent.Should().BeSameAs(fileNav); + } + + [Fact] + public void FileWithNestedChildrenBuildsCorrectly() + { + // language=yaml + var yaml = """ + project: 'test-project' + toc: + - file: guide.md + children: + - file: chapter1.md + children: + - file: subsection.md + """; + + var docSet = DocumentationSetFile.Deserialize(yaml); + var context = CreateContext(); + + var navigation = new DocumentationSetNavigation(docSet, context); + + navigation.NavigationItems.Should().HaveCount(1); + var guideFile = navigation.NavigationItems.First().Should().BeOfType().Subject; + guideFile.Url.Should().Be("/guide"); + guideFile.NavigationItems.Should().HaveCount(1); + + var chapter1 = guideFile.NavigationItems.First().Should().BeOfType().Subject; + chapter1.Url.Should().Be("/guide/chapter1"); + chapter1.Parent.Should().BeSameAs(guideFile); + chapter1.NavigationItems.Should().HaveCount(1); + + var subsection = chapter1.NavigationItems.First().Should().BeOfType().Subject; + subsection.Url.Should().Be("/guide/chapter1/subsection"); + subsection.Parent.Should().BeSameAs(chapter1); + } + + [Fact] + public async Task IndexFileWithChildrenEmitsError() + { + // language=yaml + var yaml = """ + project: 'test-project' + toc: + - file: index.md + children: + - file: section1.md + """; + + var docSet = DocumentationSetFile.Deserialize(yaml); + var context = CreateContext(); + _ = context.Collector.StartAsync(TestContext.Current.CancellationToken); + + _ = new DocumentationSetNavigation(docSet, context); + + await context.Collector.StopAsync(TestContext.Current.CancellationToken); + + var diagnostics = context.Diagnostics; + diagnostics.Should().ContainSingle(d => + d.Message.Contains("is an index file and may not have children")); + } + + [Fact] + public void FileNavigationUrlUpdatesWhenRootChanges() + { + // language=yaml + var yaml = """ + project: 'test-project' + toc: + - file: guide.md + children: + - file: section1.md + """; + + var docSet = DocumentationSetFile.Deserialize(yaml); + var context = CreateContext(); + + var navigation = new DocumentationSetNavigation(docSet, context); + var fileNav = navigation.NavigationItems.First() as FileNavigation; + var child = fileNav!.NavigationItems.First(); + + // Initial URLs + fileNav.Url.Should().Be("/guide"); + child.Url.Should().Be("/guide/section1"); + + // Change root URL + navigation.PathPrefixProvider = new PathPrefixProvider("/v2"); + + // URLs should update dynamically + fileNav.Url.Should().Be("/v2/guide"); + child.Url.Should().Be("/v2/guide/section1"); + } + + [Fact] + public void FileNavigationMixedWithFolderChildren() + { + // language=yaml + var yaml = """ + project: 'test-project' + toc: + - file: guide.md + children: + - file: intro.md + - folder: advanced + children: + - file: topics.md + """; + + var docSet = DocumentationSetFile.Deserialize(yaml); + var context = CreateContext(); + + var navigation = new DocumentationSetNavigation(docSet, context); + + var guideFile = navigation.NavigationItems.First().Should().BeOfType().Subject; + guideFile.NavigationItems.Should().HaveCount(2); + + var intro = guideFile.NavigationItems.ElementAt(0).Should().BeOfType().Subject; + intro.Url.Should().Be("/guide/intro"); + + var advancedFolder = guideFile.NavigationItems.ElementAt(1).Should().BeOfType().Subject; + advancedFolder.Url.Should().Be("/guide/advanced/topics"); // No index, uses first child + advancedFolder.NavigationItems.Should().HaveCount(1); + + var topics = advancedFolder.NavigationItems.First().Should().BeOfType().Subject; + topics.Url.Should().Be("/guide/advanced/topics"); + } +} diff --git a/tests/Navigation.Tests/Isolation/NavigationStructureTests.cs b/tests/Navigation.Tests/Isolation/NavigationStructureTests.cs new file mode 100644 index 000000000..fe0e46456 --- /dev/null +++ b/tests/Navigation.Tests/Isolation/NavigationStructureTests.cs @@ -0,0 +1,101 @@ +// 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.TestingHelpers; +using Elastic.Documentation.Configuration.DocSet; +using Elastic.Documentation.Navigation.Isolated; +using FluentAssertions; + +namespace Elastic.Documentation.Navigation.Tests.Isolation; + +public class NavigationStructureTests(ITestOutputHelper output) : DocumentationSetNavigationTestBase(output) +{ + [Fact] + public void NavigationIndexIsSetCorrectly() + { + // language=yaml + var yaml = """ + project: 'test-project' + toc: + - file: first.md + - file: second.md + - file: third.md + """; + + var docSet = DocumentationSetFile.Deserialize(yaml); + var context = CreateContext(); + + var navigation = new DocumentationSetNavigation(docSet, context); + + navigation.NavigationItems.ElementAt(0).NavigationIndex.Should().Be(0); + navigation.NavigationItems.ElementAt(1).NavigationIndex.Should().Be(1); + navigation.NavigationItems.ElementAt(2).NavigationIndex.Should().Be(2); + } + + [Fact] + public async Task ComplexNestedStructureBuildsCorrectly() + { + // language=yaml + var yaml = """ + project: 'docs-builder' + features: + primary-nav: true + toc: + - file: index.md + - folder: setup + children: + - file: index.md + - toc: advanced + children: + - toc: performance + - title: "External" + crosslink: other://link.md + """; + + var fileSystem = new MockFileSystem(); + fileSystem.AddDirectory("/docs/setup/advanced"); + fileSystem.AddDirectory("/docs/setup/advanced/performance"); + fileSystem.AddFile("/docs/setup/advanced/toc.yml", new MockFileData("toc: []")); + fileSystem.AddFile("/docs/setup/advanced/performance/toc.yml", new MockFileData("toc: []")); + var docSet = DocumentationSetFile.Deserialize(yaml); + var context = CreateContext(fileSystem); + _ = context.Collector.StartAsync(TestContext.Current.CancellationToken); + + var navigation = new DocumentationSetNavigation(docSet, context); + + await context.Collector.StopAsync(TestContext.Current.CancellationToken); + + + navigation.NavigationItems.Should().HaveCount(3); + navigation.IsUsingNavigationDropdown.Should().BeTrue(); + + // First item: simple file + var indexFile = navigation.NavigationItems.ElementAt(0).Should().BeOfType().Subject; + indexFile.Url.Should().Be("/"); // index.md becomes / + + // Second item: complex nested structure + var setupFolder = navigation.NavigationItems.ElementAt(1).Should().BeOfType().Subject; + setupFolder.NavigationItems.Should().HaveCount(2); + setupFolder.Url.Should().Be("/setup"); + + var setupIndex = setupFolder.NavigationItems.ElementAt(0).Should().BeOfType().Subject; + setupIndex.Url.Should().Be("/setup"); // index.md becomes /setup + + var advancedToc = setupFolder.NavigationItems.ElementAt(1).Should().BeOfType().Subject; + advancedToc.Url.Should().Be("/setup/advanced"); + advancedToc.NavigationItems.Should().HaveCount(1); + + var performanceToc = advancedToc.NavigationItems.First().Should().BeOfType().Subject; + performanceToc.Url.Should().Be("/setup/advanced/performance"); + // Nested TOC has a placeholder since it has no explicit children + performanceToc.NavigationItems.Should().HaveCount(1); + + // Third item: crosslink + var crosslink = navigation.NavigationItems.ElementAt(2).Should().BeOfType().Subject; + crosslink.IsCrossLink.Should().BeTrue(); + + // Verify no errors were emitted + context.Diagnostics.Should().BeEmpty(); + } +} diff --git a/tests/Navigation.Tests/Isolation/ValidationTests.cs b/tests/Navigation.Tests/Isolation/ValidationTests.cs new file mode 100644 index 000000000..92483871f --- /dev/null +++ b/tests/Navigation.Tests/Isolation/ValidationTests.cs @@ -0,0 +1,168 @@ +// 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.TestingHelpers; +using Elastic.Documentation.Configuration.DocSet; +using Elastic.Documentation.Navigation.Isolated; +using FluentAssertions; + +namespace Elastic.Documentation.Navigation.Tests.Isolation; + +public class ValidationTests(ITestOutputHelper output) : DocumentationSetNavigationTestBase(output) +{ + [Fact] + public async Task ValidationEmitsErrorWhenTableOfContentsHasNonTocChildrenAndNestedTocNotAllowed() + { + // language=yaml + var yaml = """ + project: 'test-project' + toc: + - toc: api + children: + - toc: nested-toc + children: + - file: should-error.md + """; + + var fileSystem = new MockFileSystem(); + fileSystem.AddDirectory("/docs/api"); + fileSystem.AddDirectory("/docs/api/nested-toc"); + var docSet = DocumentationSetFile.Deserialize(yaml); + var context = CreateContext(fileSystem); + _ = context.Collector.StartAsync(TestContext.Current.CancellationToken); + + _ = new DocumentationSetNavigation(docSet, context); + + await context.Collector.StopAsync(TestContext.Current.CancellationToken); + + var diagnostics = context.Diagnostics; + diagnostics.Should().ContainSingle(d => + d.Message.Contains("TableOfContents navigation does not allow nested children")); + } + + [Fact] + public async Task ValidationEmitsErrorWhenTableOfContentsHasNonTocChildren() + { + // language=yaml + var yaml = """ + project: 'test-project' + toc: + - toc: api + children: + - file: should-error.md + """; + + var fileSystem = new MockFileSystem(); + fileSystem.AddDirectory("/docs/api"); + var docSet = DocumentationSetFile.Deserialize(yaml); + var context = CreateContext(fileSystem); + _ = context.Collector.StartAsync(TestContext.Current.CancellationToken); + + _ = new DocumentationSetNavigation(docSet, context); + + await context.Collector.StopAsync(TestContext.Current.CancellationToken); + + // Check using Errors count instead of Diagnostics collection + context.Collector.Errors.Should().BeGreaterThan(0); + var diagnostics = context.Diagnostics; + diagnostics.Should().ContainSingle(d => + d.Message.Contains("TableOfContents navigation does not allow nested children")); + } + + [Fact] + public async Task ValidationEmitsErrorForNestedTocWithFileChildren() + { + // language=yaml + var yaml = """ + project: 'test-project' + toc: + - folder: setup + children: + - toc: advanced + children: + - toc: performance + children: + - file: index.md + """; + + var fileSystem = new MockFileSystem(); + fileSystem.AddDirectory("/docs/setup/advanced"); + fileSystem.AddDirectory("/docs/setup/advanced/performance"); + var docSet = DocumentationSetFile.Deserialize(yaml); + var context = CreateContext(fileSystem); + _ = context.Collector.StartAsync(TestContext.Current.CancellationToken); + + _ = new DocumentationSetNavigation(docSet, context); + + await context.Collector.StopAsync(TestContext.Current.CancellationToken); + + // Nested TOC under a root-level TOC should not allow file children + var diagnostics = context.Diagnostics; + diagnostics.Should().ContainSingle(d => + d.Message.Contains("TableOfContents navigation does not allow nested children")); + } + + [Fact] + public async Task ValidationEmitsErrorForDeeplyNestedFolderWithInvalidTocStructure() + { + // language=yaml + var yaml = """ + project: 'test-project' + toc: + - folder: docs + children: + - folder: guides + children: + - toc: api + children: + - toc: endpoints + children: + - file: users.md + """; + + var fileSystem = new MockFileSystem(); + fileSystem.AddDirectory("/docs/docs/guides/api"); + fileSystem.AddDirectory("/docs/docs/guides/api/endpoints"); + var docSet = DocumentationSetFile.Deserialize(yaml); + var context = CreateContext(fileSystem); + _ = context.Collector.StartAsync(TestContext.Current.CancellationToken); + + _ = new DocumentationSetNavigation(docSet, context); + + await context.Collector.StopAsync(TestContext.Current.CancellationToken); + + // Nested TOC structure under folders should still validate correctly + var diagnostics = context.Diagnostics; + diagnostics.Should().ContainSingle(d => + d.Message.Contains("TableOfContents navigation does not allow nested children")); + } + + [Fact] + public async Task ValidationEmitsErrorWhenTocYmlFileNotFound() + { + // language=yaml + var yaml = """ + project: 'test-project' + toc: + - toc: api + """; + + var fileSystem = new MockFileSystem(); + fileSystem.AddDirectory("/docs/api"); + // Note: not adding /docs/api/toc.yml file + + var docSet = DocumentationSetFile.Deserialize(yaml); + var context = CreateContext(fileSystem); + _ = context.Collector.StartAsync(TestContext.Current.CancellationToken); + + _ = new DocumentationSetNavigation(docSet, context); + + await context.Collector.StopAsync(TestContext.Current.CancellationToken); + + var diagnostics = context.Diagnostics; + diagnostics.Should().ContainSingle(d => + d.Message.Contains("Table of contents file not found") && + d.Message.Contains("api/toc.yml")); + } +} diff --git a/tests/Navigation.Tests/Navigation.Tests.csproj b/tests/Navigation.Tests/Navigation.Tests.csproj new file mode 100644 index 000000000..17a0cc7a1 --- /dev/null +++ b/tests/Navigation.Tests/Navigation.Tests.csproj @@ -0,0 +1,17 @@ + + + + net9.0 + enable + enable + Elastic.Documentation.Navigation.Tests + + + + + + + + + + diff --git a/tests/Navigation.Tests/TestDocumentationSetContext.cs b/tests/Navigation.Tests/TestDocumentationSetContext.cs new file mode 100644 index 000000000..798229745 --- /dev/null +++ b/tests/Navigation.Tests/TestDocumentationSetContext.cs @@ -0,0 +1,70 @@ +// 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; +using Elastic.Documentation.Diagnostics; + +namespace Elastic.Documentation.Navigation.Tests; + +public class TestDiagnosticsOutput(ITestOutputHelper output) : IDiagnosticsOutput +{ + public void Write(Diagnostic diagnostic) + { + if (diagnostic.Severity == Severity.Error) + output.WriteLine($"Error: {diagnostic.Message} ({diagnostic.File}:{diagnostic.Line})"); + else + output.WriteLine($"Warn : {diagnostic.Message} ({diagnostic.File}:{diagnostic.Line})"); + } +} + +public class TestDiagnosticsCollector(ITestOutputHelper output) + : DiagnosticsCollector([new TestDiagnosticsOutput(output)]) +{ + private readonly List _diagnostics = []; + + public IReadOnlyCollection Diagnostics => _diagnostics; + + protected override void HandleItem(Diagnostic diagnostic) => _diagnostics.Add(diagnostic); +} + +public class TestDocumentationSetContext : IDocumentationSetContext +{ + public TestDocumentationSetContext( + IFileSystem fileSystem, + IDirectoryInfo sourceDirectory, + IDirectoryInfo outputDirectory, + IFileInfo configPath, + ITestOutputHelper output, + string? repository = null + ) + { + ReadFileSystem = fileSystem; + WriteFileSystem = fileSystem; + DocumentationSourceDirectory = sourceDirectory; + OutputDirectory = outputDirectory; + ConfigurationPath = configPath; + Collector = new TestDiagnosticsCollector(output); + Git = repository is null ? GitCheckoutInformation.Unavailable : new GitCheckoutInformation + { + Branch = "main", + Remote = $"elastic/{repository}", + Ref = "main", + RepositoryName = repository + }; + + // Start the diagnostics collector to process messages + _ = Collector.StartAsync(CancellationToken.None); + } + + public IDiagnosticsCollector Collector { get; } + public IFileSystem ReadFileSystem { get; } + public IFileSystem WriteFileSystem { get; } + public IDirectoryInfo OutputDirectory { get; } + public IDirectoryInfo DocumentationSourceDirectory { get; } + public GitCheckoutInformation Git { get; } + public IFileInfo ConfigurationPath { get; } + + public IReadOnlyCollection Diagnostics => ((TestDiagnosticsCollector)Collector).Diagnostics; +} diff --git a/tests/docs-assembler.Tests/src/docs-assembler.Tests/GlobalNavigationTests.cs b/tests/docs-assembler.Tests/src/docs-assembler.Tests/GlobalNavigationTests.cs index 64acb2c3b..407c8ac95 100644 --- a/tests/docs-assembler.Tests/src/docs-assembler.Tests/GlobalNavigationTests.cs +++ b/tests/docs-assembler.Tests/src/docs-assembler.Tests/GlobalNavigationTests.cs @@ -11,6 +11,7 @@ using Elastic.Documentation.Configuration.Assembler; using Elastic.Documentation.Configuration.Navigation; using Elastic.Documentation.Diagnostics; +using Elastic.Documentation.Navigation; using Elastic.Documentation.Site.Navigation; using Elastic.Markdown.IO; using Elastic.Markdown.IO.Navigation;