diff --git a/Directory.Packages.props b/Directory.Packages.props index d68e0a42a..fa35cb23a 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -15,7 +15,7 @@ - + @@ -32,7 +32,7 @@ - + @@ -48,6 +48,7 @@ + @@ -63,4 +64,4 @@ - + \ No newline at end of file diff --git a/docs/syntax/frontmatter.md b/docs/syntax/frontmatter.md index 7c7b5d52e..4e30c4d7e 100644 --- a/docs/syntax/frontmatter.md +++ b/docs/syntax/frontmatter.md @@ -12,23 +12,111 @@ navigation_title: This is the navigation title <1> description: This is a description of the page <2> applies_to: <3> serverless: all +products: <4> + - apm-java-agent + - edot-java --- ``` + 1. [`navigation_title`](#navigation-title) 2. [`description`](#description) 3. [`applies_to`](#applies-to) +4. [`products`](#products) ## Navigation Title + See [](./titles.md) ## Description -Use the `description` frontmatter to set the description meta tag for a page. +Use the `description` frontmatter to set the description meta tag for a page. This helps search engines and social media. It also sets the `og:description` and `twitter:description` meta tags. -The `description` frontmatter is a string, recommended to be around 150 characters. If you don't set a `description`, +The `description` frontmatter is a string, recommended to be around 150 characters. If you don't set a `description`, it will be generated from the first few paragraphs of the page until it reaches 150 characters. ## Applies to + See [](./applies.md) + +## Products + +The products frontmatter is a list of products that the page relates to. +This is used for the "Products" filter in the Search UI. + +The products frontmatter is a list of strings, each string is the id of a product. + +| Product ID | Product Name | +|---------------------------------------------|-----------------------------------------------| +| `apm` | APM | +| `apm-android-agent` | APM Android Agent | +| `apm-attacher` | APM Attacher | +| `apm-aws-lambda-extension` | APM AWS Lambda extension | +| `apm-dotnet-agent` | APM .NET Agent | +| `apm-go-agent` | APM Go Agent | +| `apm-ios-agent` | APM iOS Agent | +| `apm-java-agent` | APM Java Agent | +| `apm-node-agent` | APM Node.js Agent | +| `apm-php-agent` | APM PHP Agent | +| `apm-python-agent` | APM Python Agent | +| `apm-ruby-agent` | APM Ruby Agent | +| `apm-rum-agent` | APM RUM Agent | +| `beats-logging-plugin` | Beats Logging plugin | +| `cloud-control-ecctl` | Cloud Control ECCTL | +| `cloud-enterprise` | Cloud Enterprise | +| `cloud-hosted` | Cloud Hosted | +| `cloud-kubernetes` | Cloud Kubernetes | +| `cloud-native-ingest` | Cloud Native Ingest | +| `cloud-serverless` | Cloud Serverless | +| `cloud-terraform` | Cloud Terraform | +| `ecs` | Elastic Common Schema (ECS) | +| `ecs-logging-dotnet` | ECS Logging .NET | +| `ecs-logging-go-logrus` | ECS Logging Go Logrus | +| `ecs-logging-go-zap` | ECS Logging Go Zap | +| `ecs-logging-go-zerolog` | ECS Logging Go Zerolog | +| `ecs-logging-java` | ECS Logging Java | +| `ecs-logging-node` | ECS Logging Node.js | +| `ecs-logging-php` | ECS Logging PHP | +| `ecs-logging-python` | ECS Logging Python | +| `ecs-logging-ruby` | ECS Logging Ruby | +| `edot-android` | Elastic Distribution of OpenTelemetry Android | +| `edot-collector` | Elastic Distribution of OpenTelemetry Collector | +| `edot-dotnet` | Elastic Distribution of OpenTelemetry .NET | +| `edot-ios` | Elastic Distribution of OpenTelemetry iOS | +| `edot-java` | Elastic Distribution of OpenTelemetry Java | +| `edot-nodejs` | Elastic Distribution of OpenTelemetry Node.js | +| `edot-php` | Elastic Distribution of OpenTelemetry PHP | +| `edot-python` | Elastic Distribution of OpenTelemetry Python | +| `elastic-agent` | Elastic Agent | +| `elastic-products-platform` | Elastic Products platform | +| `elastic-stack` | Elastic Stack | +| `elasticsearch` | Elasticsearch | +| `elasticsearch-apache-hadoop` | Elasticsearch Apache Hadoop | +| `elasticsearch-cloud-hosted-heroku` | Elasticsearch Cloud Hosted Heroku | +| `elasticsearch-community-clients` | Elasticsearch community clients | +| `elasticsearch-curator` | Elasticsearch Curator | +| `elasticsearch-dotnet-client` | Elasticsearch .NET Client | +| `elasticsearch-eland-python-client` | Elasticsearch Eland Python Client | +| `elasticsearch-go-client` | Elasticsearch Go Client | +| `elasticsearch-groovy-client` | Elasticsearch Groovy Client | +| `elasticsearch-java-client` | Elasticsearch Java Client | +| `elasticsearch-java-script-client` | Elasticsearch JavaScript Client | +| `elasticsearch-painless-scripting-language` | Elasticsearch Painless scripting language | +| `elasticsearch-perl-client` | Elasticsearch Perl Client | +| `elasticsearch-php-client` | Elasticsearch PHP Client | +| `elasticsearch-plugins` | Elasticsearch plugins | +| `elasticsearch-python-client` | Elasticsearch Python Client | +| `elasticsearch-resiliency-status` | Elasticsearch Resiliency Status | +| `elasticsearch-ruby-client` | Elasticsearch Ruby Client | +| `elasticsearch-rust-client` | Elasticsearch Rust Client | +| `fleet` | Fleet | +| `ingest` | Ingest | +| `integrations` | Integrations | +| `kibana` | Kibana | +| `logstash` | Logstash | +| `machine-learning` | Machine Learning | +| `observability` | Observability | +| `reference-architectures` | Reference Architectures | +| `search-ui` | Search UI | +| `security` | Security | diff --git a/src/Elastic.Markdown/Elastic.Markdown.csproj b/src/Elastic.Markdown/Elastic.Markdown.csproj index f4e330173..2ba55d0ef 100644 --- a/src/Elastic.Markdown/Elastic.Markdown.csproj +++ b/src/Elastic.Markdown/Elastic.Markdown.csproj @@ -56,6 +56,7 @@ + diff --git a/src/Elastic.Markdown/IO/MarkdownFile.cs b/src/Elastic.Markdown/IO/MarkdownFile.cs index f11e1d6f4..d6823cf06 100644 --- a/src/Elastic.Markdown/IO/MarkdownFile.cs +++ b/src/Elastic.Markdown/IO/MarkdownFile.cs @@ -330,6 +330,11 @@ private YamlFrontMatter ReadYamlFrontMatter(string raw) { return YamlSerialization.Deserialize(raw); } + catch (InvalidProductException e) + { + Collector.EmitError(FilePath, "Invalid product in yaml front matter.", e); + return new YamlFrontMatter(); + } catch (Exception e) { Collector.EmitError(FilePath, "Failed to parse yaml front matter block.", e); diff --git a/src/Elastic.Markdown/Myst/FrontMatter/FrontMatterParser.cs b/src/Elastic.Markdown/Myst/FrontMatter/FrontMatterParser.cs index fad211c79..70f65f1f0 100644 --- a/src/Elastic.Markdown/Myst/FrontMatter/FrontMatterParser.cs +++ b/src/Elastic.Markdown/Myst/FrontMatter/FrontMatterParser.cs @@ -2,18 +2,10 @@ // 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.Runtime.Serialization; using YamlDotNet.Serialization; namespace Elastic.Markdown.Myst.FrontMatter; -public enum LayoutName -{ - [EnumMember(Value = "landing-page")] LandingPage, - [EnumMember(Value = "not-found")] NotFound, - [EnumMember(Value = "archive")] Archive -} - [YamlSerializable] public class YamlFrontMatter { @@ -38,4 +30,7 @@ public class YamlFrontMatter [YamlMember(Alias = "mapped_pages")] public IReadOnlyCollection? MappedPages { get; set; } + + [YamlMember(Alias = "products")] + public IReadOnlyCollection? Products { get; set; } } diff --git a/src/Elastic.Markdown/Myst/FrontMatter/Layout.cs b/src/Elastic.Markdown/Myst/FrontMatter/Layout.cs new file mode 100644 index 000000000..c62efbade --- /dev/null +++ b/src/Elastic.Markdown/Myst/FrontMatter/Layout.cs @@ -0,0 +1,14 @@ +// 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.Runtime.Serialization; + +namespace Elastic.Markdown.Myst.FrontMatter; + +public enum LayoutName +{ + [EnumMember(Value = "landing-page")] LandingPage, + [EnumMember(Value = "not-found")] NotFound, + [EnumMember(Value = "archive")] Archive +} diff --git a/src/Elastic.Markdown/Myst/FrontMatter/Products.cs b/src/Elastic.Markdown/Myst/FrontMatter/Products.cs new file mode 100644 index 000000000..d7cc8ea66 --- /dev/null +++ b/src/Elastic.Markdown/Myst/FrontMatter/Products.cs @@ -0,0 +1,268 @@ +// 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.Collections.Frozen; +using System.ComponentModel.DataAnnotations; +using Elastic.Markdown.Suggestions; +using EnumFastToStringGenerated; +using YamlDotNet.Core; +using YamlDotNet.Core.Events; +using YamlDotNet.Serialization; + +namespace Elastic.Markdown.Myst.FrontMatter; + +[EnumGenerator] +public enum Product +{ + [Display(Name = "apm", Description = "APM")] + Apm, + + [Display(Name = "apm-dotnet-agent", Description = "APM .NET Agent")] + ApmDotnetAgent, + + [Display(Name = "apm-android-agent", Description = "APM Android Agent")] + ApmAndroidAgent, + + [Display(Name = "apm-attacher", Description = "APM Attacher")] + ApmAttacher, + + [Display(Name = "apm-aws-lambda-extension", Description = "APM AWS Lambda extension")] + ApmAwsLambdaExtension, + + [Display(Name = "apm-go-agent", Description = "APM Go Agent")] + ApmGoAgent, + + [Display(Name = "apm-ios-agent", Description = "APM iOS Agent")] + ApmIosAgent, + + [Display(Name = "apm-java-agent", Description = "APM Java Agent")] + ApmJavaAgent, + + [Display(Name = "apm-node-agent", Description = "APM Node.js Agent")] + ApmNodeAgent, + + [Display(Name = "apm-php-agent", Description = "APM PHP Agent")] + ApmPhpAgent, + + [Display(Name = "apm-python-agent", Description = "APM Python Agent")] + ApmPythonAgent, + + [Display(Name = "apm-ruby-agent", Description = "APM Ruby Agent")] + ApmRubyAgent, + + [Display(Name = "apm-rum-agent", Description = "APM RUM Agent")] + ApmRumAgent, + + [Display(Name = "beats-logging-plugin", Description = "Beats Logging plugin")] + BeatsLoggingPlugin, + + [Display(Name = "cloud-control-ecctl", Description = "Cloud Control ECCTL")] + CloudControlEcctl, + + [Display(Name = "cloud-enterprise", Description = "Cloud Enterprise")] + CloudEnterprise, + + [Display(Name = "cloud-hosted", Description = "Cloud Hosted")] + CloudHosted, + + [Display(Name = "cloud-kubernetes", Description = "Cloud Kubernetes")] + CloudKubernetes, + + [Display(Name = "cloud-native-ingest", Description = "Cloud Native Ingest")] + CloudNativeIngest, + + [Display(Name = "cloud-serverless", Description = "Cloud Serverless")] + CloudServerless, + + [Display(Name = "cloud-terraform", Description = "Cloud Terraform")] + CloudTerraform, + + [Display(Name = "ecs-logging", Description = "ECS Logging")] + EcsLogging, + + [Display(Name = "ecs-logging-dotnet", Description = "ECS Logging .NET")] + EcsLoggingDotnet, + + [Display(Name = "ecs-logging-go-logrus", Description = "ECS Logging Go Logrus")] + EcsLoggingGoLogrus, + + [Display(Name = "ecs-logging-go-zap", Description = "ECS Logging Go Zap")] + EcsLoggingGoZap, + + [Display(Name = "ecs-logging-go-zerolog", Description = "ECS Logging Go Zerolog")] + EcsLoggingGoZerolog, + + [Display(Name = "ecs-logging-java", Description = "ECS Logging Java")] + EcsLoggingJava, + + [Display(Name = "ecs-logging-node", Description = "ECS Logging Node.js")] + EcsLoggingNode, + + [Display(Name = "ecs-logging-php", Description = "ECS Logging PHP")] + EcsLoggingPhp, + + [Display(Name = "ecs-logging-python", Description = "ECS Logging Python")] + EcsLoggingPython, + + [Display(Name = "ecs-logging-ruby", Description = "ECS Logging Ruby")] + EcsLoggingRuby, + + [Display(Name = "elastic-agent", Description = "Elastic Agent")] + ElasticAgent, + + [Display(Name = "ecs", Description = "Elastic Common Schema (ECS)")] + Ecs, + + [Display(Name = "elastic-products-platform", Description = "Elastic Products platform")] + ElasticProductsPlatform, + + [Display(Name = "elastic-stack", Description = "Elastic Stack")] + ElasticStack, + + [Display(Name = "elasticsearch", Description = "Elasticsearch")] + Elasticsearch, + + [Display(Name = "elasticsearch-dotnet-client", Description = "Elasticsearch .NET Client")] + ElasticsearchDotnetClient, + + [Display(Name = "elasticsearch-apache-hadoop", Description = "Elasticsearch Apache Hadoop")] + ElasticsearchApacheHadoop, + + [Display(Name = "elasticsearch-cloud-hosted-heroku", Description = "Elasticsearch Cloud Hosted Heroku")] + ElasticsearchCloudHostedHeroku, + + [Display(Name = "elasticsearch-community-clients", Description = "Elasticsearch community clients")] + ElasticsearchCommunityClients, + + [Display(Name = "elasticsearch-curator", Description = "Elasticsearch Curator")] + ElasticsearchCurator, + + [Display(Name = "elasticsearch-eland-python-client", Description = "Elasticsearch Eland Python Client")] + ElasticsearchElandPythonClient, + + [Display(Name = "elasticsearch-go-client", Description = "Elasticsearch Go Client")] + ElasticsearchGoClient, + + [Display(Name = "elasticsearch-groovy-client", Description = "Elasticsearch Groovy Client")] + ElasticsearchGroovyClient, + + [Display(Name = "elasticsearch-java-client", Description = "Elasticsearch Java Client")] + ElasticsearchJavaClient, + + [Display(Name = "elasticsearch-java-script-client", Description = "Elasticsearch JavaScript Client")] + ElasticsearchJavaScriptClient, + + [Display(Name = "elasticsearch-painless-scripting-language", Description = "Elasticsearch Painless scripting language")] + ElasticsearchPainlessScriptingLanguage, + + [Display(Name = "elasticsearch-perl-client", Description = "Elasticsearch Perl Client")] + ElasticsearchPerlClient, + + [Display(Name = "elasticsearch-php-client", Description = "Elasticsearch PHP Client")] + ElasticsearchPhpClient, + + [Display(Name = "elasticsearch-plugins", Description = "Elasticsearch plugins")] + ElasticsearchPlugins, + + [Display(Name = "elasticsearch-python-client", Description = "Elasticsearch Python Client")] + ElasticsearchPythonClient, + + [Display(Name = "elasticsearch-resiliency-status", Description = "Elasticsearch Resiliency Status")] + ElasticsearchResiliencyStatus, + + [Display(Name = "elasticsearch-ruby-client", Description = "Elasticsearch Ruby Client")] + ElasticsearchRubyClient, + + [Display(Name = "elasticsearch-rust-client", Description = "Elasticsearch Rust Client")] + ElasticsearchRustClient, + + [Display(Name = "fleet", Description = "Fleet")] + Fleet, + + [Display(Name = "ingest", Description = "Ingest")] + Ingest, + + [Display(Name = "integrations", Description = "Integrations")] + Integrations, + + [Display(Name = "kibana", Description = "Kibana")] + Kibana, + + [Display(Name = "logstash", Description = "Logstash")] + Logstash, + + [Display(Name = "machine-learning", Description = "Machine Learning")] + MachineLearning, + + [Display(Name = "observability", Description = "Observability")] + Observability, + + [Display(Name = "reference-architectures", Description = "Reference Architectures")] + ReferenceArchitectures, + + [Display(Name = "search-ui", Description = "Search UI")] + SearchUi, + + [Display(Name = "security", Description = "Security")] + Security, + + [Display(Name = "edot-collector", Description = "Elastic Distribution of OpenTelemetry Collector")] + EdotCollector, + + [Display(Name = "edot-java", Description = "Elastic Distribution of OpenTelemetry Java")] + EdotJava, + + [Display(Name = "edot-dotnet", Description = "Elastic Distribution of OpenTelemetry .NET")] + EdotDotnet, + + [Display(Name = "edot-nodejs", Description = "Elastic Distribution of OpenTelemetry Node.js")] + EdotNodeJs, + + [Display(Name = "edot-php", Description = "Elastic Distribution of OpenTelemetry PHP")] + EdotPhp, + + [Display(Name = "edot-python", Description = "Elastic Distribution of OpenTelemetry Python")] + EdotPython, + + [Display(Name = "edot-android", Description = "Elastic Distribution of OpenTelemetry Android")] + EdotAndroid, + + [Display(Name = "edot-ios", Description = "Elastic Distribution of OpenTelemetry iOS")] + EdotIos, +} + +public class ProductConverter : IYamlTypeConverter +{ + public bool Accepts(Type type) => type == typeof(Product); + + public object ReadYaml(IParser parser, Type type, ObjectDeserializer rootDeserializer) + { + var value = parser.Consume(); + if (string.IsNullOrWhiteSpace(value.Value)) + throw new InvalidProductException(""); + + var product = Enum.GetValues() + .FirstOrDefault(p => p.ToDisplayFast()?.Equals(value.Value, StringComparison.Ordinal) ?? false); + + if (ProductEnumExtensions.IsDefinedFast(product) && product.ToDisplayFast()?.Equals(value.Value) == true) + return product; + + throw new InvalidProductException(value.Value); + } + + public void WriteYaml(IEmitter emitter, object? value, Type type, ObjectSerializer serializer) => serializer.Invoke(value, type); +} + +public class InvalidProductException(string invalidValue) + : Exception( + $"Invalid products frontmatter value: \"{invalidValue}\"." + + (!string.IsNullOrWhiteSpace(invalidValue) ? " " + new Suggestion(ProductExtensions.GetProductIds(), invalidValue).GetSuggestionQuestion() : "") + + "\nYou can find the full list at https://docs-v3-preview.elastic.dev/elastic/docs-builder/tree/main/syntax/frontmatter#products."); + +public static class ProductExtensions +{ + public static IReadOnlySet GetProductIds() => + ProductEnumExtensions.GetValuesFast() + .Select(p => p.ToDisplayFast()).ToFrozenSet(); +} diff --git a/src/Elastic.Markdown/Myst/YamlSerialization.cs b/src/Elastic.Markdown/Myst/YamlSerialization.cs index 9cdf134db..179903435 100644 --- a/src/Elastic.Markdown/Myst/YamlSerialization.cs +++ b/src/Elastic.Markdown/Myst/YamlSerialization.cs @@ -19,6 +19,7 @@ public static T Deserialize(string yaml) .IgnoreUnmatchedProperties() .WithEnumNamingConvention(HyphenatedNamingConvention.Instance) .WithTypeConverter(new SemVersionConverter()) + .WithTypeConverter(new ProductConverter()) #pragma warning disable CS0618 // Type or member is obsolete .WithTypeConverter(new DeploymentConverter()) .WithTypeConverter(new ApplicableToConverter()) diff --git a/src/Elastic.Markdown/Slices/Index.cshtml b/src/Elastic.Markdown/Slices/Index.cshtml index ec7f4fb07..0507fcf20 100644 --- a/src/Elastic.Markdown/Slices/Index.cshtml +++ b/src/Elastic.Markdown/Slices/Index.cshtml @@ -1,4 +1,6 @@ +@using Elastic.Markdown.Myst.FrontMatter @using Elastic.Markdown.Slices.Components +@using EnumFastToStringGenerated @using Markdig @inherits RazorSliceHttpResult @implements IUsesLayout @@ -22,7 +24,8 @@ Features = Model.Features, StaticFileContentHashProvider = Model.StaticFileContentHashProvider, ReportIssueUrl = Model.ReportIssueUrl, - LegacyPage = Model.LegacyPage + LegacyPage = Model.LegacyPage, + Products = Model.CurrentDocument.YamlFrontMatter?.Products is { Count: > 0} products ? string.Join(",", products.Select(p => p.ToDescriptionFast()).ToList()) : null, }; }
diff --git a/src/Elastic.Markdown/Slices/Layout/_Head.cshtml b/src/Elastic.Markdown/Slices/Layout/_Head.cshtml index 2f897dcbd..432699c38 100644 --- a/src/Elastic.Markdown/Slices/Layout/_Head.cshtml +++ b/src/Elastic.Markdown/Slices/Layout/_Head.cshtml @@ -35,4 +35,9 @@ { } + @if (!string.IsNullOrEmpty(Model.Products)) + { + + + } diff --git a/src/Elastic.Markdown/Slices/_ViewModels.cs b/src/Elastic.Markdown/Slices/_ViewModels.cs index 740da3d4b..36a9a745a 100644 --- a/src/Elastic.Markdown/Slices/_ViewModels.cs +++ b/src/Elastic.Markdown/Slices/_ViewModels.cs @@ -68,6 +68,8 @@ public class LayoutViewModel public required MarkdownFile[] Parents { get; init; } + public required string? Products { get; init; } + public string Static(string path) { var staticPath = $"_static/{path.TrimStart('/')}"; diff --git a/src/Elastic.Markdown/Suggestions/Suggestions.cs b/src/Elastic.Markdown/Suggestions/Suggestions.cs new file mode 100644 index 000000000..9674407a9 --- /dev/null +++ b/src/Elastic.Markdown/Suggestions/Suggestions.cs @@ -0,0 +1,65 @@ +// 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 + +namespace Elastic.Markdown.Suggestions; + +public class Suggestion(IReadOnlySet candidates, string input) +{ + private IReadOnlyCollection GetSuggestions() => + candidates + .Select(source => (source, Distance: LevenshteinDistance(input, source))) + .OrderBy(suggestion => suggestion.Distance) + .Where(suggestion => suggestion.Distance <= 2) + .Select(suggestion => suggestion.source) + .Take(3) + .ToList(); + + public string GetSuggestionQuestion() + { + var suggestions = GetSuggestions(); + if (suggestions.Count == 0) + return string.Empty; + + return "Did you mean " + string.Join(", ", suggestions.SkipLast(1).Select(s => $"\"{s}\"")) + (suggestions.Count > 1 ? " or " : "") + (suggestions.LastOrDefault() != null ? $"\"{suggestions.LastOrDefault()}\"" : "") + "?"; + } + + private static int LevenshteinDistance(string source, string target) + { + if (string.IsNullOrEmpty(target)) + return int.MaxValue; + + var sourceLength = source.Length; + var targetLength = target.Length; + + if (sourceLength == 0) + return targetLength; + + if (targetLength == 0) + return sourceLength; + + var distance = new int[sourceLength + 1, targetLength + 1]; + + for (var i = 0; i <= sourceLength; i++) + distance[i, 0] = i; + + for (var j = 0; j <= targetLength; j++) + distance[0, j] = j; + + for (var i = 1; i <= sourceLength; i++) + { + for (var j = 1; j <= targetLength; j++) + { + var cost = (source[i - 1] == target[j - 1]) ? 0 : 1; + + distance[i, j] = Math.Min( + Math.Min( + distance[i - 1, j] + 1, + distance[i, j - 1] + 1), + distance[i - 1, j - 1] + cost); + } + } + + return distance[sourceLength, targetLength]; + } +} diff --git a/tests/Elastic.Markdown.Tests/FrontMatter/YamlFrontMatterTests.cs b/tests/Elastic.Markdown.Tests/FrontMatter/YamlFrontMatterTests.cs index 86f653dd4..f24aabccd 100644 --- a/tests/Elastic.Markdown.Tests/FrontMatter/YamlFrontMatterTests.cs +++ b/tests/Elastic.Markdown.Tests/FrontMatter/YamlFrontMatterTests.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.Markdown.Myst.FrontMatter; using Elastic.Markdown.Tests.Directives; using FluentAssertions; @@ -63,3 +64,123 @@ public class NavigationTitleSupportReplacements(ITestOutputHelper output) : Dire [Fact] public void ReadsNavigationTitle() => File.NavigationTitle.Should().Be("Documentation Guide: value"); } + +public class ProductsSingle(ITestOutputHelper output) : DirectiveTest(output, + """ + --- + products: + - "apm" + --- + + # APM + """ +) +{ + [Fact] + public void ReadsProducts() + { + File.YamlFrontMatter.Should().NotBeNull(); + File.YamlFrontMatter!.Products.Should().NotBeNull() + .And.HaveCount(1) + .And.Contain(Product.Apm); + } +} + +public class ProductsMultiple(ITestOutputHelper output) : DirectiveTest(output, + """ + --- + products: + - "apm" + - "elasticsearch" + --- + + # APM + """ +) +{ + [Fact] + public void ReadsProducts() + { + File.YamlFrontMatter.Should().NotBeNull(); + File.YamlFrontMatter!.Products.Should().NotBeNull() + .And.HaveCount(2) + .And.Contain(Product.Apm) + .And.Contain(Product.Elasticsearch); + } +} + +public class ProductsSuggestionWhenMispelled(ITestOutputHelper output) : DirectiveTest(output, + """ + --- + products: + - aapm + --- + + # APM + """ +) +{ + [Fact] + public void HasErrors() + { + Collector.Diagnostics.Should().HaveCount(1); + Collector.Diagnostics.Should().Contain(d => d.Message.Contains("Invalid products frontmatter value: \"aapm\". Did you mean \"apm\"?")); + } +} + +public class ProductsSuggestionWhenMispelled2(ITestOutputHelper output) : DirectiveTest(output, + """ + --- + products: + - apm-javaagent + --- + + # APM + """ +) +{ + [Fact] + public void HasErrors() + { + Collector.Diagnostics.Should().HaveCount(1); + Collector.Diagnostics.Should().Contain(d => d.Message.Contains("Invalid products frontmatter value: \"apm-javaagent\". Did you mean \"apm-java-agent\"?")); + } +} + +public class ProductsSuggestionWhenCasingError(ITestOutputHelper output) : DirectiveTest(output, + """ + --- + products: + - Apm + --- + + # APM + """ +) +{ + [Fact] + public void HasErrors() + { + Collector.Diagnostics.Should().HaveCount(1); + Collector.Diagnostics.Should().Contain(d => d.Message.Contains("Invalid products frontmatter value: \"Apm\". Did you mean \"apm\"?")); + } +} + +public class ProductsSuggestionWhenEmpty(ITestOutputHelper output) : DirectiveTest(output, + """ + --- + products: + - "" + --- + + # APM + """ +) +{ + [Fact] + public void HasErrors() + { + Collector.Diagnostics.Should().HaveCount(1); + Collector.Diagnostics.Should().Contain(d => d.Message.Contains("Invalid products frontmatter value: \"\".\nYou can find the full list at https://docs-v3-preview.elastic.dev/elastic/docs-builder/tree/main/syntax/frontmatter#products.")); + } +}