Skip to content

Commit 2b18190

Browse files
robertmclawsclaude
andauthored
Parse Root and Expanded attributes on navigation <Group> elements (#61)
ParseGroupConfig only read Name, Icon, and Tag, so a template <Group> could not set GroupConfig.Root or GroupConfig.Expanded even though both exist on the model and serialize to docs.json. This made it impossible to give identically named groups (e.g. repeated "Episodes"/"Planning" groups across sibling brand sections) a distinct root page, which Mintlify uses to keep their sidebar expand/collapse state independent. - ParseGroupConfig now reads the optional Root (string) and Expanded (bool, via the codebase's IsNullOrWhiteSpace + bool.TryParse convention) attributes. Both remain optional; omitting them leaves the properties null. - Documented the supported attributes on the method with remarks + an example. - Added tests: Root present/absent, Expanded true/false/cased, and missing/invalid Expanded left null. Extended the complete-attributes test. Also pins System.Security.Cryptography.Xml above the transitively-resolved 9.0.0 (high-severity advisories GHSA-37gx-xxp4-5rgx, GHSA-w3x6-4m5h-cxqf) so restore passes with NuGet audit enabled. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 9bce37f commit 2b18190

5 files changed

Lines changed: 137 additions & 5 deletions

File tree

.claude/settings.local.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,8 @@
3030
"WebFetch(domain:www.nuget.org)",
3131
"WebFetch(domain:www.anthropic.com)",
3232
"Bash(gh run:*)",
33-
"Bash(gh workflow:*)"
33+
"Bash(gh workflow:*)",
34+
"Bash(git rev-list *)"
3435
],
3536
"deny": [],
3637
"ask": [],

src/CloudNimble.DotNetDocs.Sdk.Tasks/CloudNimble.DotNetDocs.Sdk.Tasks.csproj

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@
2121
<ItemGroup>
2222
<!-- MSBuild task dependencies -->
2323
<PackageReference Include="EasyAF.MSBuild" Version="4.*" />
24-
<PackageReference Include="System.Collections.Immutable" Version="9.*" PrivateAssets="all" />
2524

2625
<!-- Microsoft.CodeAnalysis.CSharp.Workspaces needs to be available for the task -->
2726
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="4.*" />
@@ -30,14 +29,26 @@
3029
<!-- Framework-specific dependencies -->
3130
<ItemGroup Condition="'$(TargetFramework)' == 'net10.0'">
3231
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.*" />
32+
<PackageReference Include="System.Collections.Immutable" Version="10.*" PrivateAssets="all" />
33+
34+
<!-- Vulnerability fix -->
35+
<PackageReference Include="System.Security.Cryptography.Xml" Version="10.*" />
3336
</ItemGroup>
3437

3538
<ItemGroup Condition="'$(TargetFramework)' == 'net9.0'">
3639
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.*" />
40+
<PackageReference Include="System.Collections.Immutable" Version="9.*" PrivateAssets="all" />
41+
42+
<!-- Vulnerability fix -->
43+
<PackageReference Include="System.Security.Cryptography.Xml" Version="9.*" />
3744
</ItemGroup>
3845

3946
<ItemGroup Condition="'$(TargetFramework)' == 'net8.0'">
4047
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.*" />
48+
<PackageReference Include="System.Collections.Immutable" Version="9.*" PrivateAssets="all" />
49+
50+
<!-- Vulnerability fix -->
51+
<PackageReference Include="System.Security.Cryptography.Xml" Version="9.*" />
4152
</ItemGroup>
4253

4354
<ItemGroup Condition="'$(TargetFramework)' == 'net472'">

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

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -779,6 +779,20 @@ internal NavigationConfig ParseNavigationConfig(XElement navigationElement)
779779
/// </summary>
780780
/// <param name="groupElement">The group XML element.</param>
781781
/// <returns>A GroupConfig instance, or null if parsing fails.</returns>
782+
/// <remarks>
783+
/// Requires a <c>Name</c> attribute. Optional attributes include <c>Icon</c>, <c>Tag</c>,
784+
/// <c>Root</c> (the default page shown when the group is selected), and <c>Expanded</c>
785+
/// (<c>true</c>/<c>false</c>, controlling the group's default expand/collapse state).
786+
/// Child <c>&lt;Pages&gt;</c> and nested <c>&lt;Group&gt;</c> elements (optionally wrapped in
787+
/// <c>&lt;Groups&gt;</c>) are also parsed.
788+
/// </remarks>
789+
/// <example>
790+
/// <code>
791+
/// &lt;Group Name="Episodes" Icon="microphone" Root="Brands/WhatsYourStory/Episodes/index" Expanded="false"&gt;
792+
/// &lt;Pages&gt;Brands/WhatsYourStory/Episodes/index;Brands/WhatsYourStory/Episodes/analysis&lt;/Pages&gt;
793+
/// &lt;/Group&gt;
794+
/// </code>
795+
/// </example>
782796
internal GroupConfig? ParseGroupConfig(XElement groupElement)
783797
{
784798
var groupName = groupElement.Attribute("Name")?.Value;
@@ -793,9 +807,17 @@ internal NavigationConfig ParseNavigationConfig(XElement navigationElement)
793807
Group = groupName,
794808
Icon = groupElement.Attribute("Icon")?.Value,
795809
Tag = groupElement.Attribute("Tag")?.Value,
810+
Root = groupElement.Attribute("Root")?.Value,
796811
Pages = []
797812
};
798813

814+
// Parse the optional Expanded attribute, which controls the group's default expand/collapse state.
815+
var expandedAttr = groupElement.Attribute("Expanded")?.Value;
816+
if (!string.IsNullOrWhiteSpace(expandedAttr) && bool.TryParse(expandedAttr, out var expanded))
817+
{
818+
group.Expanded = expanded;
819+
}
820+
799821
// Parse direct pages (semicolon-separated list)
800822
var pagesElement = groupElement.Element(nameof(GroupConfig.Pages));
801823
if (pagesElement is not null && !string.IsNullOrWhiteSpace(pagesElement.Value))

src/CloudNimble.DotNetDocs.Tests.Sdk.Tasks/GenerateDocumentationTaskTests.cs

Lines changed: 100 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,14 +48,14 @@ public void TestCleanup()
4848
#region MintlifyTemplate XML Parsing Tests
4949

5050
/// <summary>
51-
/// Tests that ParseGroupConfig correctly extracts group name, icon, and tag from XML.
51+
/// Tests that ParseGroupConfig correctly extracts group name, icon, tag, root, and expanded from XML.
5252
/// </summary>
5353
[TestMethod]
5454
public void ParseGroupConfig_WithCompleteAttributes_ParsesAllProperties()
5555
{
5656
// Arrange
5757
var xml = """
58-
<Group Name="Getting Started" Icon="stars" Tag="CORE">
58+
<Group Name="Getting Started" Icon="stars" Tag="CORE" Root="getting-started/index" Expanded="false">
5959
<Pages>index;quickstart</Pages>
6060
</Group>
6161
""";
@@ -69,11 +69,109 @@ public void ParseGroupConfig_WithCompleteAttributes_ParsesAllProperties()
6969
result!.Group.Should().Be("Getting Started");
7070
result.Icon!.Name.Should().Be("stars");
7171
result.Tag.Should().Be("CORE");
72+
result.Root.Should().Be("getting-started/index");
73+
result.Expanded.Should().BeFalse();
7274
result.Pages.Should().HaveCount(2);
7375
result.Pages![0].Should().Be("index");
7476
result.Pages![1].Should().Be("quickstart");
7577
}
7678

79+
/// <summary>
80+
/// Tests that ParseGroupConfig parses the Root attribute, used to disambiguate identically-named
81+
/// groups across sections and to provide a landing page when the group is selected.
82+
/// </summary>
83+
[TestMethod]
84+
public void ParseGroupConfig_WithRootAttribute_ParsesRoot()
85+
{
86+
// Arrange
87+
var xml = """
88+
<Group Name="Episodes" Icon="microphone" Root="Brands/WhatsYourStory/Episodes/index">
89+
<Pages>Brands/WhatsYourStory/Episodes/index</Pages>
90+
</Group>
91+
""";
92+
var groupElement = XElement.Parse(xml);
93+
94+
// Act
95+
var result = _task.ParseGroupConfig(groupElement);
96+
97+
// Assert
98+
result.Should().NotBeNull();
99+
result!.Root.Should().Be("Brands/WhatsYourStory/Episodes/index");
100+
}
101+
102+
/// <summary>
103+
/// Tests that ParseGroupConfig leaves Root null when the attribute is absent.
104+
/// </summary>
105+
[TestMethod]
106+
public void ParseGroupConfig_WithoutRoot_HasNullRoot()
107+
{
108+
// Arrange
109+
var xml = """
110+
<Group Name="Episodes" Icon="microphone">
111+
<Pages>index</Pages>
112+
</Group>
113+
""";
114+
var groupElement = XElement.Parse(xml);
115+
116+
// Act
117+
var result = _task.ParseGroupConfig(groupElement);
118+
119+
// Assert
120+
result.Should().NotBeNull();
121+
result!.Root.Should().BeNull();
122+
}
123+
124+
/// <summary>
125+
/// Tests that ParseGroupConfig parses the Expanded attribute for both true and false values.
126+
/// </summary>
127+
[TestMethod]
128+
[DataRow("true", true)]
129+
[DataRow("false", false)]
130+
[DataRow("True", true)]
131+
public void ParseGroupConfig_WithExpandedAttribute_ParsesExpanded(string value, bool expected)
132+
{
133+
// Arrange
134+
var xml = $"""
135+
<Group Name="Episodes" Icon="microphone" Expanded="{value}">
136+
<Pages>index</Pages>
137+
</Group>
138+
""";
139+
var groupElement = XElement.Parse(xml);
140+
141+
// Act
142+
var result = _task.ParseGroupConfig(groupElement);
143+
144+
// Assert
145+
result.Should().NotBeNull();
146+
result!.Expanded.Should().Be(expected);
147+
}
148+
149+
/// <summary>
150+
/// Tests that ParseGroupConfig leaves Expanded null when the attribute is absent or not a valid boolean.
151+
/// </summary>
152+
[TestMethod]
153+
[DataRow(null)]
154+
[DataRow("")]
155+
[DataRow("yes")]
156+
public void ParseGroupConfig_WithMissingOrInvalidExpanded_LeavesExpandedNull(string? value)
157+
{
158+
// Arrange
159+
var attr = value is null ? "" : $@" Expanded=""{value}""";
160+
var xml = $"""
161+
<Group Name="Episodes" Icon="microphone"{attr}>
162+
<Pages>index</Pages>
163+
</Group>
164+
""";
165+
var groupElement = XElement.Parse(xml);
166+
167+
// Act
168+
var result = _task.ParseGroupConfig(groupElement);
169+
170+
// Assert
171+
result.Should().NotBeNull();
172+
result!.Expanded.Should().BeNull();
173+
}
174+
77175
/// <summary>
78176
/// Tests that ParseGroupConfig preserves the exact order of groups as defined in the template.
79177
/// </summary>

src/global.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"sdk": {
3-
"version": "10.0.100",
3+
"version": "10.0.300",
44
"rollForward": "latestPatch"
55
},
66
"test": {

0 commit comments

Comments
 (0)