Skip to content

Commit 42e5679

Browse files
robertmclawsclaude
andauthored
Add full navigation section type support (Tabs, Anchors, Dropdowns, Products) (#49)
* 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> * Bump SDK version to 1.3.0, fix EasyAF.MSBuild version constraint, move specs to future - Bump DotNetDocs.Sdk reference from 1.2.0 to 1.3.0 in both .docsproj files - Change EasyAF.MSBuild version constraint from 4.*-* to 4.* to resolve NU1107 conflict - Delete specs/semantic-kernel-integration.md and specs/try-dotnet.md (moved to specs/future/) - Add specs/future/ with moved specs and contributors.md Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Skip auto-navigation population when template defines explicit sections When a MintlifyTemplate specifies Tabs, Anchors, Dropdowns, or Products directly in the <Navigation> XML, the renderer was still calling PopulateNavigationFromPath which unconditionally initialized Pages and auto-discovered MDX files from disk. This produced a spurious "navigation.pages" block alongside the explicit "navigation.tabs" in the output docs.json. Fix by checking for explicit navigation sections after loading the template config and skipping both PopulateNavigationFromPath and BuildNavigationStructure when they are present. The NavigationType-based auto-generation path (where Pages are discovered then moved to a Tab by ApplyNavigationType) is unaffected. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Add renderer-level tests for explicit template navigation sections The parser-layer tests (ParseNavigationConfig_WithTabsElement_PopulatesTabsNotPages etc.) already verified that Pages comes back null when only Tabs are defined in XML. However, the actual bug was one layer deeper — in MintlifyRenderer.ProcessAsync, which called PopulateNavigationFromPath and injected a spurious pages block regardless. This adds: - contentOnly parameter to ConfigureTestWithTemplate so tests can invoke ProcessAsync([]) (documentation-only mode) with HasMintlifyTemplate set - ExplicitTemplateTabs_ContentOnly_HasTabsAndNoPages - ExplicitTemplateTabs_WithAssembly_HasOnlyTabsAndNoPages (regression) - ExplicitTemplateAnchors_ContentOnly_HasAnchorsAndNoPages - ExplicitTemplateProducts_ContentOnly_HasProductsAndNoPages Each test asserts that Navigation.Pages is null in the deserialized docs.json when the template defines an explicit navigation section type, which is the behavior introduced by the MintlifyRenderer fix. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent ed356a3 commit 42e5679

2 files changed

Lines changed: 203 additions & 9 deletions

File tree

src/CloudNimble.DotNetDocs.Mintlify/MintlifyRenderer.cs

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -136,14 +136,27 @@ public async Task RenderAsync(DocAssembly? model)
136136
.Where(d => !string.IsNullOrWhiteSpace(d))
137137
.ToArray();
138138

139-
// First: Discover existing MDX files in documentation root, preserving template navigation
140-
// Exclude DocumentationReference output directories to prevent them from being treated as conceptual docs
141-
_docsJsonManager.PopulateNavigationFromPath(Context.DocumentationRootPath, new[] { ".mdx" }, includeApiReference: false, preserveExisting: true, excludeDirectories: excludeDirectories);
142-
143-
// Second: Add API reference content to existing navigation ONLY if model exists
144-
if (model is not null)
145-
{
146-
BuildNavigationStructure(_docsJsonManager.Configuration!, model);
139+
// Determine whether the template already defines explicit navigation sections.
140+
// When Tabs, Anchors, Dropdowns, or Products are present, all navigation is
141+
// managed by the template — auto-discovery into Pages would produce a spurious
142+
// parallel "pages" block alongside the explicit sections.
143+
var templateNav = _docsJsonManager.Configuration?.Navigation;
144+
var hasExplicitSections = templateNav?.Tabs is not null
145+
|| templateNav?.Anchors is not null
146+
|| templateNav?.Dropdowns is not null
147+
|| templateNav?.Products is not null;
148+
149+
if (!hasExplicitSections)
150+
{
151+
// First: Discover existing MDX files in documentation root, preserving template navigation
152+
// Exclude DocumentationReference output directories to prevent them from being treated as conceptual docs
153+
_docsJsonManager.PopulateNavigationFromPath(Context.DocumentationRootPath, new[] { ".mdx" }, includeApiReference: false, preserveExisting: true, excludeDirectories: excludeDirectories);
154+
155+
// Second: Add API reference content to existing navigation ONLY if model exists
156+
if (model is not null)
157+
{
158+
BuildNavigationStructure(_docsJsonManager.Configuration!, model);
159+
}
147160
}
148161

149162
// Third: Apply NavigationType from template to move root content to Tabs/Products if configured

src/CloudNimble.DotNetDocs.Tests.Mintlify/Renderers/MintlifyRendererNavigationTypeTests.cs

Lines changed: 182 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,14 @@ public class MintlifyRendererNavigationTypeTests : DotNetDocsTestBase
3838
/// Configures test host with a specific template and navigation configuration.
3939
/// This must be called before TestSetup() to ensure the configuration is set before services are built.
4040
/// </summary>
41-
private void ConfigureTestWithTemplate(DocsJsonConfig? template, DocsNavigationConfig? navConfig = null)
41+
/// <param name="template">The DocsJsonConfig template to use, or null for no template.</param>
42+
/// <param name="navConfig">Optional navigation configuration to override defaults.</param>
43+
/// <param name="contentOnly">
44+
/// When <see langword="true"/>, sets <c>HasMintlifyTemplate = true</c> on the project context so that
45+
/// <see cref="DocumentationManager.ProcessAsync(System.Collections.Generic.IEnumerable{System.ValueTuple{string,string}})"/>
46+
/// with an empty assembly list still runs the renderer pipeline (content-only mode).
47+
/// </param>
48+
private void ConfigureTestWithTemplate(DocsJsonConfig? template, DocsNavigationConfig? navConfig = null, bool contentOnly = false)
4249
{
4350
_testOutputPath = Path.Combine(Path.GetTempPath(), $"MintlifyNavigationTypeTest_{Guid.NewGuid()}");
4451
Directory.CreateDirectory(_testOutputPath);
@@ -59,6 +66,10 @@ private void ConfigureTestWithTemplate(DocsJsonConfig? template, DocsNavigationC
5966
{
6067
ctx.DocumentationRootPath = _testOutputPath;
6168
ctx.ApiReferencePath = string.Empty;
69+
if (contentOnly)
70+
{
71+
ctx.HasMintlifyTemplate = true;
72+
}
6273
});
6374
});
6475
});
@@ -333,6 +344,176 @@ public async Task ApplyNavigationType_WithInvalidType_UsesDefaultBehavior()
333344

334345
#endregion
335346

347+
#region Explicit Template Navigation Section Tests
348+
349+
/// <summary>
350+
/// Regression test: when a template defines explicit Tabs in the Navigation config, the renderer
351+
/// must not auto-generate a parallel pages block from disk discovery (content-only mode).
352+
/// </summary>
353+
[TestMethod]
354+
public async Task ExplicitTemplateTabs_ContentOnly_HasTabsAndNoPages()
355+
{
356+
// Arrange
357+
ConfigureTestWithTemplate(
358+
new DocsJsonConfig
359+
{
360+
Name = "Test Docs",
361+
Theme = "mint",
362+
Colors = new ColorsConfig { Primary = "#000000" },
363+
Navigation = new NavigationConfig
364+
{
365+
Tabs =
366+
[
367+
new TabConfig { Tab = "Guides", Href = "/guides", Pages = ["guides/index"] },
368+
new TabConfig { Tab = "API Reference", Href = "/api", Pages = ["api/index"] }
369+
]
370+
}
371+
},
372+
contentOnly: true);
373+
374+
var documentationManager = GetService<DocumentationManager>();
375+
376+
// Act
377+
await documentationManager.ProcessAsync([]);
378+
379+
// Assert
380+
var docsJsonPath = Path.Combine(_testOutputPath, "docs.json");
381+
File.Exists(docsJsonPath).Should().BeTrue();
382+
383+
var json = File.ReadAllText(docsJsonPath);
384+
var config = JsonSerializer.Deserialize<DocsJsonConfig>(json, MintlifyConstants.JsonSerializerOptions);
385+
386+
config.Should().NotBeNull();
387+
config!.Navigation.Tabs.Should().HaveCount(2, "both template-defined tabs should be present");
388+
config.Navigation.Tabs![0].Tab.Should().Be("Guides");
389+
config.Navigation.Tabs![1].Tab.Should().Be("API Reference");
390+
config.Navigation.Pages.Should().BeNull("explicit tabs navigation must not produce a spurious pages block");
391+
}
392+
393+
/// <summary>
394+
/// Regression test: when a template defines explicit Tabs, auto-discovery must not inject a pages block
395+
/// even when a real assembly is also being processed.
396+
/// </summary>
397+
[TestMethod]
398+
public async Task ExplicitTemplateTabs_WithAssembly_HasOnlyTabsAndNoPages()
399+
{
400+
// Arrange
401+
ConfigureTestWithTemplate(new DocsJsonConfig
402+
{
403+
Name = "Test API",
404+
Theme = "mint",
405+
Colors = new ColorsConfig { Primary = "#000000" },
406+
Navigation = new NavigationConfig
407+
{
408+
Tabs =
409+
[
410+
new TabConfig { Tab = "Getting Started", Href = "/start", Pages = ["index"] },
411+
new TabConfig { Tab = "API Reference", Href = "/api", Pages = ["api/index"] }
412+
]
413+
}
414+
});
415+
416+
var assemblyPath = typeof(SimpleClass).Assembly.Location;
417+
var xmlPath = Path.ChangeExtension(assemblyPath, ".xml");
418+
var documentationManager = GetService<DocumentationManager>();
419+
420+
// Act
421+
await documentationManager.ProcessAsync(assemblyPath, xmlPath);
422+
423+
// Assert
424+
var docsJsonPath = Path.Combine(_testOutputPath, "docs.json");
425+
var json = File.ReadAllText(docsJsonPath);
426+
var config = JsonSerializer.Deserialize<DocsJsonConfig>(json, MintlifyConstants.JsonSerializerOptions);
427+
428+
config.Should().NotBeNull();
429+
config!.Navigation.Tabs.Should().HaveCount(2, "template-defined tabs should be preserved");
430+
config.Navigation.Pages.Should().BeNull("auto-discovery must not inject pages alongside explicit tabs even when an assembly is present");
431+
}
432+
433+
/// <summary>
434+
/// Verifies that a template with explicit Anchors produces anchors and no pages in content-only mode.
435+
/// </summary>
436+
[TestMethod]
437+
public async Task ExplicitTemplateAnchors_ContentOnly_HasAnchorsAndNoPages()
438+
{
439+
// Arrange
440+
ConfigureTestWithTemplate(
441+
new DocsJsonConfig
442+
{
443+
Name = "Test Docs",
444+
Theme = "mint",
445+
Colors = new ColorsConfig { Primary = "#000000" },
446+
Navigation = new NavigationConfig
447+
{
448+
Anchors =
449+
[
450+
new AnchorConfig { Anchor = "Documentation", Href = "/docs", Icon = "book", Pages = [] },
451+
new AnchorConfig { Anchor = "API", Href = "/api", Icon = "code", Pages = [] }
452+
]
453+
}
454+
},
455+
contentOnly: true);
456+
457+
var documentationManager = GetService<DocumentationManager>();
458+
459+
// Act
460+
await documentationManager.ProcessAsync([]);
461+
462+
// Assert
463+
var docsJsonPath = Path.Combine(_testOutputPath, "docs.json");
464+
var json = File.ReadAllText(docsJsonPath);
465+
var config = JsonSerializer.Deserialize<DocsJsonConfig>(json, MintlifyConstants.JsonSerializerOptions);
466+
467+
config.Should().NotBeNull();
468+
config!.Navigation.Anchors.Should().HaveCount(2, "both template-defined anchors should be present");
469+
config.Navigation.Anchors![0].Anchor.Should().Be("Documentation");
470+
config.Navigation.Anchors![1].Anchor.Should().Be("API");
471+
config.Navigation.Pages.Should().BeNull("explicit anchors navigation must not produce a spurious pages block");
472+
}
473+
474+
/// <summary>
475+
/// Verifies that a template with explicit Products produces products and no pages in content-only mode.
476+
/// </summary>
477+
[TestMethod]
478+
public async Task ExplicitTemplateProducts_ContentOnly_HasProductsAndNoPages()
479+
{
480+
// Arrange
481+
ConfigureTestWithTemplate(
482+
new DocsJsonConfig
483+
{
484+
Name = "Test Platform",
485+
Theme = "mint",
486+
Colors = new ColorsConfig { Primary = "#000000" },
487+
Navigation = new NavigationConfig
488+
{
489+
Products =
490+
[
491+
new ProductConfig { Product = "Core SDK", Href = "/core", Pages = ["core/index"] },
492+
new ProductConfig { Product = "Extensions", Href = "/ext", Pages = ["ext/index"] }
493+
]
494+
}
495+
},
496+
contentOnly: true);
497+
498+
var documentationManager = GetService<DocumentationManager>();
499+
500+
// Act
501+
await documentationManager.ProcessAsync([]);
502+
503+
// Assert
504+
var docsJsonPath = Path.Combine(_testOutputPath, "docs.json");
505+
var json = File.ReadAllText(docsJsonPath);
506+
var config = JsonSerializer.Deserialize<DocsJsonConfig>(json, MintlifyConstants.JsonSerializerOptions);
507+
508+
config.Should().NotBeNull();
509+
config!.Navigation.Products.Should().HaveCount(2, "both template-defined products should be present");
510+
config.Navigation.Products![0].Product.Should().Be("Core SDK");
511+
config.Navigation.Products![1].Product.Should().Be("Extensions");
512+
config.Navigation.Pages.Should().BeNull("explicit products navigation must not produce a spurious pages block");
513+
}
514+
515+
#endregion
516+
336517
}
337518

338519
}

0 commit comments

Comments
 (0)