Skip to content

Commit 3eac8b0

Browse files
robertmclawsclaude
andcommitted
Add full navigation section type support to ParseNavigationConfig
ParseNavigationConfig previously only handled Pages-based navigation, always initializing an empty Pages list even when unused. This adds support for all four remaining Mintlify navigation section types: Tabs, Anchors, Dropdowns, and Products. Changes: - Rework ParseNavigationConfig to detect and populate Tabs, Anchors, Dropdowns, and Products from their XML wrapper elements; Pages initialization is now deferred and only set when explicitly present - Add ParseTabConfig, ParseAnchorConfig, ParseDropdownConfig, and ParseProductConfig internal methods with full XML attribute and nested-element parsing - Add ParseNavigationSectionPages private helper to share Groups/Page parsing logic across all section types - Add 20 new tests covering all parse methods, HTML entity decoding, nested structures, multi-type coexistence, and backward compatibility Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent d48e16a commit 3eac8b0

2 files changed

Lines changed: 880 additions & 6 deletions

File tree

src/CloudNimble.DotNetDocs.Sdk.Tasks/GenerateDocumentationTask.cs

Lines changed: 320 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -639,14 +639,73 @@ internal DocsNavigationConfig ParseDocsNavigationConfig(XElement root)
639639
/// <returns>A NavigationConfig instance.</returns>
640640
internal NavigationConfig ParseNavigationConfig(XElement navigationElement)
641641
{
642-
var navConfig = new NavigationConfig
642+
var navConfig = new NavigationConfig();
643+
644+
// Parse Tabs
645+
var tabsElement = navigationElement.Element("Tabs");
646+
if (tabsElement is not null)
643647
{
644-
Pages = []
645-
};
648+
navConfig.Tabs = [];
649+
foreach (var tabElement in tabsElement.Elements("Tab"))
650+
{
651+
var tab = ParseTabConfig(tabElement);
652+
if (tab is not null)
653+
{
654+
navConfig.Tabs.Add(tab);
655+
}
656+
}
657+
}
658+
659+
// Parse Anchors
660+
var anchorsElement = navigationElement.Element("Anchors");
661+
if (anchorsElement is not null)
662+
{
663+
navConfig.Anchors = [];
664+
foreach (var anchorElement in anchorsElement.Elements("Anchor"))
665+
{
666+
var anchor = ParseAnchorConfig(anchorElement);
667+
if (anchor is not null)
668+
{
669+
navConfig.Anchors.Add(anchor);
670+
}
671+
}
672+
}
673+
674+
// Parse Dropdowns
675+
var dropdownsElement = navigationElement.Element("Dropdowns");
676+
if (dropdownsElement is not null)
677+
{
678+
navConfig.Dropdowns = [];
679+
foreach (var dropdownElement in dropdownsElement.Elements("Dropdown"))
680+
{
681+
var dropdown = ParseDropdownConfig(dropdownElement);
682+
if (dropdown is not null)
683+
{
684+
navConfig.Dropdowns.Add(dropdown);
685+
}
686+
}
687+
}
688+
689+
// Parse Products
690+
var productsElement = navigationElement.Element("Products");
691+
if (productsElement is not null)
692+
{
693+
navConfig.Products = [];
694+
foreach (var productElement in productsElement.Elements("Product"))
695+
{
696+
var product = ParseProductConfig(productElement);
697+
if (product is not null)
698+
{
699+
navConfig.Products.Add(product);
700+
}
701+
}
702+
}
646703

704+
// Parse Pages (only if explicitly provided alongside or instead of structured section types)
647705
var pagesElement = navigationElement.Element(nameof(NavigationConfig.Pages));
648706
if (pagesElement is not null)
649707
{
708+
navConfig.Pages = [];
650709
var groupsElement = pagesElement.Element("Groups");
651710
if (groupsElement is not null)
652711
{
@@ -660,9 +719,7 @@ internal NavigationConfig ParseNavigationConfig(XElement navigationElement)
660719
}
661720
}
662721

663-
// Also parse any direct page references
664-
var directPages = pagesElement.Elements("Page");
665-
foreach (var pageElement in directPages)
722+
foreach (var pageElement in pagesElement.Elements("Page"))
666723
{
667724
var pageName = pageElement.Value?.Trim();
668725
if (!string.IsNullOrWhiteSpace(pageName))
@@ -741,6 +798,263 @@ internal NavigationConfig ParseNavigationConfig(XElement navigationElement)
741798
return group;
742799
}
743800

801+
/// <summary>
802+
/// Parses an Anchor element from the navigation XML.
803+
/// </summary>
804+
/// <param name="anchorElement">The anchor XML element.</param>
805+
/// <returns>An <see cref="AnchorConfig"/> instance, or <see langword="null"/> if parsing fails.</returns>
806+
/// <remarks>
807+
/// Expects a <c>Name</c> attribute on the element. Optional attributes include <c>Href</c> and <c>Icon</c>.
808+
/// Child <c>&lt;Pages&gt;</c> and <c>&lt;Tabs&gt;</c> elements are also parsed if present.
809+
/// </remarks>
810+
/// <example>
811+
/// <code>
812+
/// &lt;Anchor Name="API Reference" Href="/api" Icon="code"&gt;
813+
/// &lt;Pages&gt;
814+
/// &lt;Groups&gt;
815+
/// &lt;Group Name="Endpoints" Icon="bolt"&gt;
816+
/// &lt;Pages&gt;api/index;api/auth&lt;/Pages&gt;
817+
/// &lt;/Group&gt;
818+
/// &lt;/Groups&gt;
819+
/// &lt;/Pages&gt;
820+
/// &lt;/Anchor&gt;
821+
/// </code>
822+
/// </example>
823+
internal AnchorConfig? ParseAnchorConfig(XElement anchorElement)
824+
{
825+
var anchorName = anchorElement.Attribute("Name")?.Value;
826+
if (string.IsNullOrWhiteSpace(anchorName))
827+
{
828+
Log.LogWarning("Anchor element missing Name attribute, skipping");
829+
return null;
830+
}
831+
832+
var anchor = new AnchorConfig
833+
{
834+
Anchor = anchorName,
835+
Href = anchorElement.Attribute("Href")?.Value,
836+
Icon = anchorElement.Attribute("Icon")?.Value,
837+
Pages = []
838+
};
839+
840+
ParseNavigationSectionPages(anchorElement, anchor.Pages);
841+
842+
// Parse nested Tabs within Anchor
843+
var tabsElement = anchorElement.Element("Tabs");
844+
if (tabsElement is not null)
845+
{
846+
anchor.Tabs = [];
847+
foreach (var tabElement in tabsElement.Elements("Tab"))
848+
{
849+
var tab = ParseTabConfig(tabElement);
850+
if (tab is not null)
851+
{
852+
anchor.Tabs.Add(tab);
853+
}
854+
}
855+
}
856+
857+
return anchor;
858+
}
859+
860+
/// <summary>
861+
/// Parses a Dropdown element from the navigation XML.
862+
/// </summary>
863+
/// <param name="dropdownElement">The dropdown XML element.</param>
864+
/// <returns>A <see cref="DropdownConfig"/> instance, or <see langword="null"/> if parsing fails.</returns>
865+
/// <remarks>
866+
/// Expects a <c>Name</c> attribute on the element. Optional attributes include <c>Href</c> and <c>Icon</c>.
867+
/// Child <c>&lt;Pages&gt;</c>, <c>&lt;Tabs&gt;</c>, and <c>&lt;Anchors&gt;</c> elements are also parsed if present.
868+
/// </remarks>
869+
/// <example>
870+
/// <code>
871+
/// &lt;Dropdown Name="Products" Icon="grid"&gt;
872+
/// &lt;Anchors&gt;
873+
/// &lt;Anchor Name="Core" Icon="star"&gt;
874+
/// &lt;Pages&gt;&lt;Groups&gt;&lt;Group Name="Basics"&gt;&lt;Pages&gt;core/index&lt;/Pages&gt;&lt;/Group&gt;&lt;/Groups&gt;&lt;/Pages&gt;
875+
/// &lt;/Anchor&gt;
876+
/// &lt;/Anchors&gt;
877+
/// &lt;/Dropdown&gt;
878+
/// </code>
879+
/// </example>
880+
internal DropdownConfig? ParseDropdownConfig(XElement dropdownElement)
881+
{
882+
var dropdownName = dropdownElement.Attribute("Name")?.Value;
883+
if (string.IsNullOrWhiteSpace(dropdownName))
884+
{
885+
Log.LogWarning("Dropdown element missing Name attribute, skipping");
886+
return null;
887+
}
888+
889+
var dropdown = new DropdownConfig
890+
{
891+
Dropdown = dropdownName,
892+
Href = dropdownElement.Attribute("Href")?.Value,
893+
Icon = dropdownElement.Attribute("Icon")?.Value,
894+
Pages = []
895+
};
896+
897+
ParseNavigationSectionPages(dropdownElement, dropdown.Pages);
898+
899+
// Parse nested Tabs within Dropdown
900+
var tabsElement = dropdownElement.Element("Tabs");
901+
if (tabsElement is not null)
902+
{
903+
dropdown.Tabs = [];
904+
foreach (var tabElement in tabsElement.Elements("Tab"))
905+
{
906+
var tab = ParseTabConfig(tabElement);
907+
if (tab is not null)
908+
{
909+
dropdown.Tabs.Add(tab);
910+
}
911+
}
912+
}
913+
914+
// Parse nested Anchors within Dropdown
915+
var anchorsElement = dropdownElement.Element("Anchors");
916+
if (anchorsElement is not null)
917+
{
918+
dropdown.Anchors = [];
919+
foreach (var anchorElement in anchorsElement.Elements("Anchor"))
920+
{
921+
var anchor = ParseAnchorConfig(anchorElement);
922+
if (anchor is not null)
923+
{
924+
dropdown.Anchors.Add(anchor);
925+
}
926+
}
927+
}
928+
929+
return dropdown;
930+
}
931+
932+
/// <summary>
933+
/// Parses a Product element from the navigation XML.
934+
/// </summary>
935+
/// <param name="productElement">The product XML element.</param>
936+
/// <returns>A <see cref="ProductConfig"/> instance, or <see langword="null"/> if parsing fails.</returns>
937+
/// <remarks>
938+
/// Expects a <c>Name</c> attribute on the element. Optional attributes include <c>Href</c>, <c>Icon</c>, and <c>Description</c>.
939+
/// Child <c>&lt;Pages&gt;</c> elements are also parsed if present.
940+
/// </remarks>
941+
/// <example>
942+
/// <code>
943+
/// &lt;Product Name="CloudNimble Core" Href="/core" Icon="box" Description="Core platform features"&gt;
944+
/// &lt;Pages&gt;
945+
/// &lt;Groups&gt;
946+
/// &lt;Group Name="Getting Started" Icon="stars"&gt;
947+
/// &lt;Pages&gt;core/index;core/quickstart&lt;/Pages&gt;
948+
/// &lt;/Group&gt;
949+
/// &lt;/Groups&gt;
950+
/// &lt;/Pages&gt;
951+
/// &lt;/Product&gt;
952+
/// </code>
953+
/// </example>
954+
internal ProductConfig? ParseProductConfig(XElement productElement)
955+
{
956+
var productName = productElement.Attribute("Name")?.Value;
957+
if (string.IsNullOrWhiteSpace(productName))
958+
{
959+
Log.LogWarning("Product element missing Name attribute, skipping");
960+
return null;
961+
}
962+
963+
var product = new ProductConfig
964+
{
965+
Product = productName,
966+
Href = productElement.Attribute("Href")?.Value,
967+
Icon = productElement.Attribute("Icon")?.Value,
968+
Description = productElement.Attribute("Description")?.Value,
969+
Pages = []
970+
};
971+
972+
ParseNavigationSectionPages(productElement, product.Pages);
973+
974+
return product;
975+
}
976+
977+
/// <summary>
978+
/// Parses a Tab element from the navigation XML.
979+
/// </summary>
980+
/// <param name="tabElement">The tab XML element.</param>
981+
/// <returns>A <see cref="TabConfig"/> instance, or <see langword="null"/> if parsing fails.</returns>
982+
/// <remarks>
983+
/// Expects a <c>Name</c> attribute on the element. Optional attributes include <c>Href</c> and <c>Icon</c>.
984+
/// Child <c>&lt;Pages&gt;</c> elements are also parsed if present.
985+
/// </remarks>
986+
/// <example>
987+
/// <code>
988+
/// &lt;Tab Name="Guides" Href="/guides" Icon="book"&gt;
989+
/// &lt;Pages&gt;
990+
/// &lt;Groups&gt;
991+
/// &lt;Group Name="Getting Started" Icon="stars"&gt;
992+
/// &lt;Pages&gt;guides/index;guides/quickstart&lt;/Pages&gt;
993+
/// &lt;/Group&gt;
994+
/// &lt;/Groups&gt;
995+
/// &lt;/Pages&gt;
996+
/// &lt;/Tab&gt;
997+
/// </code>
998+
/// </example>
999+
internal TabConfig? ParseTabConfig(XElement tabElement)
1000+
{
1001+
var tabName = tabElement.Attribute("Name")?.Value;
1002+
if (string.IsNullOrWhiteSpace(tabName))
1003+
{
1004+
Log.LogWarning("Tab element missing Name attribute, skipping");
1005+
return null;
1006+
}
1007+
1008+
var tab = new TabConfig
1009+
{
1010+
Tab = tabName,
1011+
Href = tabElement.Attribute("Href")?.Value,
1012+
Icon = tabElement.Attribute("Icon")?.Value,
1013+
Pages = []
1014+
};
1015+
1016+
ParseNavigationSectionPages(tabElement, tab.Pages);
1017+
1018+
return tab;
1019+
}
1020+
1021+
/// <summary>
1022+
/// Parses the <c>&lt;Pages&gt;</c> child element of a navigation section element and populates the provided
1023+
/// <paramref name="pages"/> list with page strings and nested <see cref="GroupConfig"/> objects.
1024+
/// </summary>
1025+
/// <param name="parentElement">The parent XML element that may contain a <c>&lt;Pages&gt;</c> child.</param>
1026+
/// <param name="pages">The list to populate with parsed page entries.</param>
1027+
private void ParseNavigationSectionPages(XElement parentElement, List<object> pages)
1028+
{
1029+
var pagesElement = parentElement.Element("Pages");
1030+
if (pagesElement is null)
1031+
{
1032+
return;
1033+
}
1034+
1035+
var groupsElement = pagesElement.Element("Groups");
1036+
if (groupsElement is not null)
1037+
{
1038+
foreach (var groupElement in groupsElement.Elements("Group"))
1039+
{
1040+
var group = ParseGroupConfig(groupElement);
1041+
if (group is not null)
1042+
{
1043+
pages.Add(group);
1044+
}
1045+
}
1046+
}
1047+
1048+
foreach (var pageElement in pagesElement.Elements("Page"))
1049+
{
1050+
var pageName = pageElement.Value?.Trim();
1051+
if (!string.IsNullOrWhiteSpace(pageName))
1052+
{
1053+
pages.Add(pageName);
1054+
}
1055+
}
1056+
}
1057+
7441058
/// <summary>
7451059
/// Parses the Integrations element from the MintlifyTemplate XML using attributes.
7461060
/// </summary>

0 commit comments

Comments
 (0)