diff --git a/docs/syntax/dropdowns.md b/docs/syntax/dropdowns.md index a8b4f9a60..850eb33f7 100644 --- a/docs/syntax/dropdowns.md +++ b/docs/syntax/dropdowns.md @@ -9,7 +9,7 @@ Dropdowns allow you to hide and reveal content on user interaction. By default, ::::{tab-item} Output -:::{dropdown} Dropdown Title +:::{dropdown} Dropdown Title 1 Dropdown content ::: @@ -17,7 +17,7 @@ Dropdown content ::::{tab-item} Markdown ```markdown -:::{dropdown} Dropdown Title +:::{dropdown} Dropdown Title 1 Dropdown content ::: ``` @@ -33,7 +33,7 @@ You can specify that the dropdown content should be visible by default. Do this ::::{tab-item} Output -:::{dropdown} Dropdown Title +:::{dropdown} Dropdown Title 2 :open: Dropdown content ::: @@ -102,3 +102,39 @@ Dropdown content for ECE and ECH :::: ::::: + +## Anchors and deep linking + +Dropdowns automatically generate anchors from their titles, allowing you to link directly to them. The anchor is created by converting the title to lowercase and replacing spaces with hyphens (slugify). + +For example, a dropdown with title "Installation Guide" will have the anchor `#installation-guide`. + +### Custom anchors + +You can specify a custom anchor using the `:name:` option: + +:::::{tab-set} + +::::{tab-item} Output + +:::{dropdown} My Dropdown +:name: custom-anchor +Content that can be linked to via #custom-anchor +::: + +:::: + +::::{tab-item} Markdown +```markdown +:::{dropdown} My Dropdown +:name: custom-anchor +Content that can be linked to via #custom-anchor +::: +``` +:::: + +::::: + +### Duplicate anchors + +If multiple elements (dropdowns or headings) in the same document have the same anchor, the build will emit a hint warning. While this doesn't fail the build, it may cause linking issues. Ensure each dropdown has a unique title or use the `:name:` option to specify unique anchors. diff --git a/docs/testing/nested/index.md b/docs/testing/nested/index.md index c65382bcd..dda6d6db9 100644 --- a/docs/testing/nested/index.md +++ b/docs/testing/nested/index.md @@ -11,3 +11,10 @@ The files in this directory are used for testing purposes. Do not edit these fil ## Injecting a {{x}} is supported in headers. This should show up in the file's table of contents too. + +:::{dropdown} Dropdown Title +:name: dropdown-title + +Text. + +::: \ No newline at end of file diff --git a/src/Elastic.Documentation.Site/Assets/main.ts b/src/Elastic.Documentation.Site/Assets/main.ts index ec0f22c6f..6c724f1a2 100644 --- a/src/Elastic.Documentation.Site/Assets/main.ts +++ b/src/Elastic.Documentation.Site/Assets/main.ts @@ -3,7 +3,7 @@ import { initCopyButton } from './copybutton' import { initHighlight } from './hljs' import { initImageCarousel } from './image-carousel' import './markdown/applies-to' -import { openDetailsWithAnchor } from './open-details-with-anchor' +import { initOpenDetailsWithAnchor } from './open-details-with-anchor' import { initNav } from './pages-nav' import { initSmoothScroll } from './smooth-scroll' import { initTabs } from './tabs' @@ -33,7 +33,7 @@ document.addEventListener('htmx:load', function (event) { initNav() } initSmoothScroll() - openDetailsWithAnchor() + initOpenDetailsWithAnchor() initImageCarousel() const urlParams = new URLSearchParams(window.location.search) diff --git a/src/Elastic.Documentation.Site/Assets/open-details-with-anchor.ts b/src/Elastic.Documentation.Site/Assets/open-details-with-anchor.ts index dd715904e..3aa5b933b 100644 --- a/src/Elastic.Documentation.Site/Assets/open-details-with-anchor.ts +++ b/src/Elastic.Documentation.Site/Assets/open-details-with-anchor.ts @@ -2,21 +2,72 @@ import { UAParser } from 'ua-parser-js' const { browser } = UAParser() -// This is a fix for anchors in details elements in non-Chrome browsers. +// Opens details elements (dropdowns) when navigating to an anchor link within them +// This enables deeplinking to collapsed dropdown content export function openDetailsWithAnchor() { if (window.location.hash) { const target = document.querySelector(window.location.hash) if (target) { const closestDetails = target.closest('details') - if (closestDetails) { - if (browser.name !== 'Chrome') { + if (closestDetails && !closestDetails.open) { + // Only open if it's not already open by default + if (!closestDetails.dataset.openDefault) { closestDetails.open = true - target.scrollIntoView({ - behavior: 'instant', - block: 'start', - }) } } + + // Chrome automatically ensures parent content is visible, scroll immediately + // Other browsers need manual scroll handling + if (browser.name !== 'Chrome') { + target.scrollIntoView({ + behavior: 'instant', + block: 'start', + }) + } } } } + +// Initialize the anchor handling functionality +export function initOpenDetailsWithAnchor() { + // Handle initial page load + openDetailsWithAnchor() + + // Handle hash changes within the same page (e.g., clicking anchor links) + window.addEventListener('hashchange', openDetailsWithAnchor) + + // Handle dropdown URL updates + document.addEventListener( + 'click', + (event) => { + const target = event.target as HTMLElement + const dropdown = target.closest( + 'details.dropdown' + ) as HTMLDetailsElement + if (dropdown) { + const initialState = dropdown.open + + // Check state after toggle completes + setTimeout(() => { + const finalState = dropdown.open + const stateChanged = initialState !== finalState + + // If dropdown opened and doesn't have open-default flag, push URL + if ( + stateChanged && + finalState && + !dropdown.dataset.openDefault + ) { + window.history.pushState(null, '', `#${dropdown.id}`) + } + + // Remove open-default flag after first interaction + if (dropdown.dataset.openDefault === 'true') { + delete dropdown.dataset.openDefault + } + }, 10) + } + }, + true + ) +} diff --git a/src/Elastic.Markdown/IO/MarkdownFile.cs b/src/Elastic.Markdown/IO/MarkdownFile.cs index d9dfb8358..59ebd4539 100644 --- a/src/Elastic.Markdown/IO/MarkdownFile.cs +++ b/src/Elastic.Markdown/IO/MarkdownFile.cs @@ -12,6 +12,7 @@ using Elastic.Markdown.Helpers; using Elastic.Markdown.Myst; using Elastic.Markdown.Myst.Directives; +using Elastic.Markdown.Myst.Directives.Admonition; using Elastic.Markdown.Myst.Directives.Include; using Elastic.Markdown.Myst.Directives.Stepper; using Elastic.Markdown.Myst.FrontMatter; @@ -178,6 +179,7 @@ public async Task ParseFullAsync(Cancel ctx) _ = await MinimalParseAsync(ctx); var document = await GetParseDocumentAsync(ctx); + ValidateDuplicateAnchors(document); return document; } @@ -194,6 +196,68 @@ private IReadOnlyDictionary GetSubstitutions() return allProperties; } + private void ValidateDuplicateAnchors(MarkdownDocument document) + { + // Collect all anchors with their source blocks + var anchorSources = new List<(string Anchor, int Line, int Column, int Length, string Type)>(); + + // Collect dropdown anchors + foreach (var dropdown in document.Descendants()) + { + if (!string.IsNullOrEmpty(dropdown.CrossReferenceName)) + { + anchorSources.Add(( + dropdown.CrossReferenceName, + dropdown.Line + 1, + dropdown.Column + 1, // Column is 0-indexed, add 1 + dropdown.OpeningLength, + "dropdown" + )); + } + } + + // Collect heading anchors + foreach (var heading in document.Descendants()) + { + var header = heading.GetData("header") as string; + var anchor = heading.GetData("anchor") as string; + var slugTarget = (anchor ?? header) ?? string.Empty; + if (!string.IsNullOrEmpty(slugTarget)) + { + var slug = slugTarget.Slugify(); + anchorSources.Add(( + slug, + heading.Line + 1, + heading.Column + 1, // Column is 0-indexed, add 1 + 1, // heading length + "heading" + )); + } + } + + // Group by anchor and find duplicates + var duplicateGroups = anchorSources + .GroupBy(a => a.Anchor, StringComparer.OrdinalIgnoreCase) + .Where(g => g.Count() > 1); + + foreach (var group in duplicateGroups) + { + var anchor = group.Key; + foreach (var (_, line, column, length, type) in group) + { + Collector.Write(new Diagnostic + { + Severity = Severity.Hint, + File = SourceFile.FullName, + Line = line, + Column = column, + Length = length, + Message = $"Duplicate anchor '{anchor}' found in {type}. Multiple elements with the same anchor may cause linking issues." + }); + } + } + } + protected void ReadDocumentInstructions(MarkdownDocument document) { Title ??= document diff --git a/src/Elastic.Markdown/Myst/Directives/Admonition/AdmonitionBlock.cs b/src/Elastic.Markdown/Myst/Directives/Admonition/AdmonitionBlock.cs index 6446429dc..84080d1f7 100644 --- a/src/Elastic.Markdown/Myst/Directives/Admonition/AdmonitionBlock.cs +++ b/src/Elastic.Markdown/Myst/Directives/Admonition/AdmonitionBlock.cs @@ -53,6 +53,10 @@ public override void FinalizeAndValidate(ParserContext context) else if (!string.IsNullOrEmpty(Arguments)) Title += $" {Arguments}"; Title = Title.ReplaceSubstitutions(context); + + // Auto-generate CrossReferenceName for dropdowns without explicit name, same as headings + if (string.IsNullOrEmpty(CrossReferenceName) && (Admonition == "dropdown" || Classes == "dropdown")) + CrossReferenceName = Title.Slugify(); } private ApplicableTo? ParseApplicableTo(string yaml) diff --git a/src/Elastic.Markdown/Myst/Directives/Dropdown/DropdownView.cshtml b/src/Elastic.Markdown/Myst/Directives/Dropdown/DropdownView.cshtml index f120e352d..e4c0a57b0 100644 --- a/src/Elastic.Markdown/Myst/Directives/Dropdown/DropdownView.cshtml +++ b/src/Elastic.Markdown/Myst/Directives/Dropdown/DropdownView.cshtml @@ -1,5 +1,27 @@ @using Elastic.Markdown.Myst.Components @inherits RazorSlice +@if (!string.IsNullOrEmpty(Model.Open)) +{ + +} +else +{ +} diff --git a/src/Elastic.Markdown/Myst/ParserContext.cs b/src/Elastic.Markdown/Myst/ParserContext.cs index 5288ce886..824a27584 100644 --- a/src/Elastic.Markdown/Myst/ParserContext.cs +++ b/src/Elastic.Markdown/Myst/ParserContext.cs @@ -7,6 +7,7 @@ using Elastic.Documentation.Configuration.Builder; using Elastic.Documentation.Links.CrossLinks; using Elastic.Markdown.Diagnostics; +using Elastic.Markdown.Helpers; using Elastic.Markdown.IO; using Elastic.Markdown.Myst.FrontMatter; using Markdig; diff --git a/src/tooling/docs-builder/Diagnostics/LiveMode/LiveModeDiagnosticsCollector.cs b/src/tooling/docs-builder/Diagnostics/LiveMode/LiveModeDiagnosticsCollector.cs index 20efc3d49..4826dfd0f 100644 --- a/src/tooling/docs-builder/Diagnostics/LiveMode/LiveModeDiagnosticsCollector.cs +++ b/src/tooling/docs-builder/Diagnostics/LiveMode/LiveModeDiagnosticsCollector.cs @@ -12,7 +12,27 @@ namespace Documentation.Builder.Diagnostics.LiveMode; public class LiveModeDiagnosticsCollector(ILoggerFactory logFactory) : DiagnosticsCollector([new Log(logFactory.CreateLogger())]) { - protected override void HandleItem(Diagnostic diagnostic) { } + private readonly List _errors = []; + private readonly List _warnings = []; + private readonly List _hints = []; - public override async Task StopAsync(Cancel cancellationToken) => await Task.CompletedTask; + protected override void HandleItem(Diagnostic diagnostic) + { + if (diagnostic.Severity == Severity.Error) + _errors.Add(diagnostic); + else if (diagnostic.Severity == Severity.Warning) + _warnings.Add(diagnostic); + else + _hints.Add(diagnostic); + } + + public override async Task StopAsync(Cancel cancellationToken) + { + if (_errors.Count > 0 || _warnings.Count > 0 || _hints.Count > 0) + { + var repository = new Elastic.Documentation.Tooling.Diagnostics.Console.ErrataFileSourceRepository(); + repository.WriteDiagnosticsToConsole(_errors, _warnings, _hints); + } + await Task.CompletedTask; + } } diff --git a/tests/Elastic.Markdown.Tests/Directives/AdmonitionTests.cs b/tests/Elastic.Markdown.Tests/Directives/AdmonitionTests.cs index 49d427b2b..47620c75c 100644 --- a/tests/Elastic.Markdown.Tests/Directives/AdmonitionTests.cs +++ b/tests/Elastic.Markdown.Tests/Directives/AdmonitionTests.cs @@ -2,8 +2,11 @@ // 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.Diagnostics; using Elastic.Markdown.Myst.Directives.Admonition; +using Elastic.Markdown.Myst.Directives.Dropdown; using FluentAssertions; +using Markdig.Syntax; namespace Elastic.Markdown.Tests.Directives; @@ -100,126 +103,35 @@ A regular paragraph. public void SetsDropdownOpen() => Block!.DropdownOpen.Should().BeTrue(); } -public class DropdownAppliesToTests(ITestOutputHelper output) : DirectiveTest(output, -""" -:::{dropdown} This is my custom dropdown -:applies_to: stack: ga 9.0 -This is an attention block -::: -A regular paragraph. -""" -) -{ - [Fact] - public void SetsCorrectAdmonitionType() => Block!.Admonition.Should().Be("dropdown"); - - [Fact] - public void SetsCustomTitle() => Block!.Title.Should().Be("This is my custom dropdown"); - - [Fact] - public void SetsAppliesToDefinition() => Block!.AppliesToDefinition.Should().Be("stack: ga 9.0"); - - [Fact] - public void ParsesAppliesTo() => Block!.AppliesTo.Should().NotBeNull(); -} - -public class DropdownPropertyParsingTests(ITestOutputHelper output) : DirectiveTest(output, +public class DropdownWithNameTests(ITestOutputHelper output) : DirectiveTest(output, """ -:::{dropdown} Test Dropdown -:open: +:::{dropdown} Dropdown with name :name: test-dropdown -This is test content -::: -A regular paragraph. -""" -) -{ - [Fact] - public void SetsCorrectAdmonitionType() => Block!.Admonition.Should().Be("dropdown"); - - [Fact] - public void SetsCustomTitle() => Block!.Title.Should().Be("Test Dropdown"); - - [Fact] - public void SetsDropdownOpen() => Block!.DropdownOpen.Should().BeTrue(); - - [Fact] - public void SetsCrossReferenceName() => Block!.CrossReferenceName.Should().Be("test-dropdown"); -} - -public class DropdownNestedContentTests(ITestOutputHelper output) : DirectiveTest(output, -""" -::::{dropdown} Nested Content Test :open: -This dropdown contains nested content with colons: - -- Time: 10:30 AM -- URL: https://example.com:8080/path -- Configuration: key:value pairs -- Code: `function test() { return "hello:world"; }` - -And even nested directives: - -:::{note} Nested Note -This is a nested note with colons: 10:30 AM +This is a dropdown with a name ::: - -More content after nested directive. -:::: A regular paragraph. """ ) { - private readonly ITestOutputHelper _output = output; - [Fact] public void SetsCorrectAdmonitionType() => Block!.Admonition.Should().Be("dropdown"); [Fact] - public void SetsCustomTitle() => Block!.Title.Should().Be("Nested Content Test"); + public void SetsCustomTitle() => Block!.Title.Should().Be("Dropdown with name"); [Fact] public void SetsDropdownOpen() => Block!.DropdownOpen.Should().BeTrue(); [Fact] - public void ContainsContentWithColons() - { - var html = Html; - html.Should().Contain("Time: 10:30 AM"); - html.Should().Contain("URL: https://example.com:8080/path"); - html.Should().Contain("Configuration: key:value pairs"); - html.Should().Contain("function test() { return "hello:world"; }"); - } - - [Fact] - public void ContainsNestedDirective() - { - var html = Html; - // Output the full HTML for inspection - _output.WriteLine("Generated HTML:"); - _output.WriteLine(html); - - html.Should().Contain("Nested Note"); - html.Should().Contain("This is a nested note with colons: 10:30 AM"); - // Verify the nested note was actually parsed as a directive, not just plain text - html.Should().Contain("class=\"admonition note\""); - html.Should().Contain("admonition-title"); - html.Should().Contain("admonition-content"); - } - - [Fact] - public void ContainsContentAfterNestedDirective() - { - var html = Html; - html.Should().Contain("More content after nested directive"); - } + public void SetsCrossReferenceName() => Block!.CrossReferenceName.Should().Be("test-dropdown"); } -public class DropdownComplexPropertyTests(ITestOutputHelper output) : DirectiveTest(output, +public class DropdownAppliesToTests(ITestOutputHelper output) : DirectiveTest(output, """ -:::{dropdown} Complex Properties Test +:::{dropdown} This is my custom dropdown :applies_to: stack: ga 9.0 -This is content with applies_to property +This is an attention block ::: A regular paragraph. """ @@ -229,172 +141,63 @@ A regular paragraph. public void SetsCorrectAdmonitionType() => Block!.Admonition.Should().Be("dropdown"); [Fact] - public void SetsCustomTitle() => Block!.Title.Should().Be("Complex Properties Test"); - - [Fact] - public void ParsesAppliesToWithComplexValue() - { - Block!.AppliesToDefinition.Should().Be("stack: ga 9.0"); - Block!.AppliesTo.Should().NotBeNull(); - } -} - -public class NoteAppliesToTests(ITestOutputHelper output) : DirectiveTest(output, -""" -:::{note} -:applies_to: stack: ga -This is a note with applies_to information -::: -A regular paragraph. -""" -) -{ - [Fact] - public void SetsCorrectAdmonitionType() => Block!.Admonition.Should().Be("note"); - - [Fact] - public void SetsTitle() => Block!.Title.Should().Be("Note"); + public void SetsCustomTitle() => Block!.Title.Should().Be("This is my custom dropdown"); [Fact] - public void SetsAppliesToDefinition() => Block!.AppliesToDefinition.Should().Be("stack: ga"); + public void SetsAppliesToDefinition() => Block!.AppliesToDefinition.Should().Be("stack: ga 9.0"); [Fact] public void ParsesAppliesTo() => Block!.AppliesTo.Should().NotBeNull(); - - [Fact] - public void RendersAppliesToInHtml() - { - var html = Html; - html.Should().Contain("applies applies-admonition"); - html.Should().Contain("admonition-title__separator"); - html.Should().Contain("applicable-info"); - } } -public class WarningAppliesToTests(ITestOutputHelper output) : DirectiveTest(output, +public class DuplicateDropdownAnchorTests(ITestOutputHelper output) : DirectiveTest(output, """ -:::{warning} -:applies_to: stack: ga -This is a warning with applies_to information +:::{dropdown} Same title +First dropdown content ::: -A regular paragraph. -""" -) -{ - [Fact] - public void SetsCorrectAdmonitionType() => Block!.Admonition.Should().Be("warning"); - - [Fact] - public void SetsTitle() => Block!.Title.Should().Be("Warning"); - - [Fact] - public void SetsAppliesToDefinition() => Block!.AppliesToDefinition.Should().Be("stack: ga"); - - [Fact] - public void ParsesAppliesTo() => Block!.AppliesTo.Should().NotBeNull(); - - [Fact] - public void RendersAppliesToInHtml() - { - var html = Html; - html.Should().Contain("applies applies-admonition"); - html.Should().Contain("admonition-title__separator"); - html.Should().Contain("applicable-info"); - } -} -public class TipAppliesToTests(ITestOutputHelper output) : DirectiveTest(output, -""" -:::{tip} -:applies_to: stack: ga -This is a tip with applies_to information +:::{dropdown} Same title +Second dropdown content ::: -A regular paragraph. -""" -) +""") { [Fact] - public void SetsCorrectAdmonitionType() => Block!.Admonition.Should().Be("tip"); - - [Fact] - public void SetsTitle() => Block!.Title.Should().Be("Tip"); - - [Fact] - public void SetsAppliesToDefinition() => Block!.AppliesToDefinition.Should().Be("stack: ga"); - - [Fact] - public void ParsesAppliesTo() => Block!.AppliesTo.Should().NotBeNull(); - - [Fact] - public void RendersAppliesToInHtml() + public void ReportsHintForDuplicateAnchors() { - var html = Html; - html.Should().Contain("applies applies-admonition"); - html.Should().Contain("admonition-title__separator"); - html.Should().Contain("applicable-info"); + Collector.Diagnostics.Should().Contain(m => + m.Severity == Severity.Hint && + m.Message.Contains("Duplicate anchor") && + m.Message.Contains("'same-title'")); + + // Should report hint for both duplicate dropdowns + Collector.Diagnostics.Where(m => + m.Severity == Severity.Hint && + m.Message.Contains("Duplicate anchor") && + m.Message.Contains("'same-title'")).Should().HaveCount(2); } } -public class ImportantAppliesToTests(ITestOutputHelper output) : DirectiveTest(output, -""" -:::{important} -:applies_to: stack: ga -This is an important notice with applies_to information -::: -A regular paragraph. +public class DuplicateDropdownAndHeadingAnchorTests(ITestOutputHelper output) : DirectiveTest(output, """ -) -{ - [Fact] - public void SetsCorrectAdmonitionType() => Block!.Admonition.Should().Be("important"); +## Test Heading - [Fact] - public void SetsTitle() => Block!.Title.Should().Be("Important"); - - [Fact] - public void SetsAppliesToDefinition() => Block!.AppliesToDefinition.Should().Be("stack: ga"); - - [Fact] - public void ParsesAppliesTo() => Block!.AppliesTo.Should().NotBeNull(); - - [Fact] - public void RendersAppliesToInHtml() - { - var html = Html; - html.Should().Contain("applies applies-admonition"); - html.Should().Contain("admonition-title__separator"); - html.Should().Contain("applicable-info"); - } -} - -public class AdmonitionAppliesToTests(ITestOutputHelper output) : DirectiveTest(output, -""" -:::{admonition} Custom Admonition -:applies_to: stack: ga -This is a custom admonition with applies_to information +:::{dropdown} Test Heading +Dropdown content with same anchor as heading ::: -A regular paragraph. -""" -) +""") { [Fact] - public void SetsCorrectAdmonitionType() => Block!.Admonition.Should().Be("admonition"); - - [Fact] - public void SetsCustomTitle() => Block!.Title.Should().Be("Custom Admonition"); - - [Fact] - public void SetsAppliesToDefinition() => Block!.AppliesToDefinition.Should().Be("stack: ga"); - - [Fact] - public void ParsesAppliesTo() => Block!.AppliesTo.Should().NotBeNull(); - - [Fact] - public void RendersAppliesToInHtml() + public void ReportsHintForDuplicateAnchorsAcrossTypes() { - var html = Html; - html.Should().Contain("applies applies-admonition"); - html.Should().Contain("admonition-title__separator"); - html.Should().Contain("applicable-info"); + Collector.Diagnostics.Should().Contain(m => + m.Severity == Severity.Hint && + m.Message.Contains("Duplicate anchor") && + m.Message.Contains("'test-heading'")); + + // Should report hint for both the heading and dropdown + Collector.Diagnostics.Where(m => + m.Severity == Severity.Hint && + m.Message.Contains("Duplicate anchor") && + m.Message.Contains("'test-heading'")).Should().HaveCount(2); } } diff --git a/tests/authoring/Framework/MarkdownDocumentAssertions.fs b/tests/authoring/Framework/MarkdownDocumentAssertions.fs index 81617d3c4..16d5a264b 100644 --- a/tests/authoring/Framework/MarkdownDocumentAssertions.fs +++ b/tests/authoring/Framework/MarkdownDocumentAssertions.fs @@ -36,7 +36,7 @@ module MarkdownDocumentAssertions = match (expectedAvailability, m.AppliesTo) with | NonNull a, NonNull applies -> applies.Diagnostics <- a.Diagnostics | _ -> () - + let apply = m.AppliesTo test <@ apply = expectedAvailability @> | _ -> failwithf $"%s{result.File.RelativePath} has no yamlfront matter" diff --git a/tests/authoring/FrontMatter/ProductsFrontMatter.fs b/tests/authoring/FrontMatter/ProductsFrontMatter.fs index e22e73355..3e49b7b34 100644 --- a/tests/authoring/FrontMatter/ProductsFrontMatter.fs +++ b/tests/authoring/FrontMatter/ProductsFrontMatter.fs @@ -66,7 +66,7 @@ This is a test page without products frontmatter. // Test that the products frontmatter is correctly processed by checking the file let results = markdownWithProducts.Value let defaultFile = results.MarkdownResults |> Seq.find (fun r -> r.File.RelativePath = "index.md") - + // Test that the file has the correct products test <@ defaultFile.File.YamlFrontMatter <> null @> match defaultFile.File.YamlFrontMatter with @@ -81,19 +81,18 @@ This is a test page without products frontmatter. test <@ productIds.Contains("ecctl") @> | _ -> () | _ -> () - [] let ``does not include products in frontmatter when no products are specified`` () = // Test that pages without products frontmatter don't have products let results = markdownWithoutProducts.Value let defaultFile = results.MarkdownResults |> Seq.find (fun r -> r.File.RelativePath = "index.md") - + // Test that the file has no products match defaultFile.File.YamlFrontMatter with | NonNull frontMatter -> match frontMatter.Products with - | NonNull products -> + | NonNull products -> test <@ products.Count = 0 @> | _ -> () | _ -> ()