From 5017d39dc93fa27bdc8b4e27429304107a14f069 Mon Sep 17 00:00:00 2001 From: Jan Calanog Date: Fri, 9 May 2025 15:14:04 +0200 Subject: [PATCH] Add docset `products` attribute --- docs/_docset.yml | 3 + docs/syntax/code.md | 5 ++ .../Builder/ConfigurationFile.cs | 17 ++++ .../Builder/Products.cs | 89 +++++++++++++++++++ .../Suggestions/Suggestions.cs | 2 +- .../Myst/FrontMatter/FrontMatterParser.cs | 1 + .../Myst/FrontMatter/Products.cs | 86 +----------------- src/Elastic.Markdown/Slices/HtmlWriter.cs | 19 +++- src/Elastic.Markdown/Slices/Index.cshtml | 2 +- src/Elastic.Markdown/Slices/_ViewModels.cs | 2 + 10 files changed, 139 insertions(+), 87 deletions(-) create mode 100644 src/Elastic.Documentation.Configuration/Builder/Products.cs rename src/{Elastic.Markdown => Elastic.Documentation.Configuration}/Suggestions/Suggestions.cs (96%) diff --git a/docs/_docset.yml b/docs/_docset.yml index a31228d0b..16559027b 100644 --- a/docs/_docset.yml +++ b/docs/_docset.yml @@ -13,6 +13,9 @@ subs: serverless-short: Serverless ece: "Elastic Cloud Enterprise" eck: "Elastic Cloud on Kubernetes" + +products: + - elasticsearch features: primary-nav: false diff --git a/docs/syntax/code.md b/docs/syntax/code.md index 15524985a..5e0633b16 100644 --- a/docs/syntax/code.md +++ b/docs/syntax/code.md @@ -1,3 +1,8 @@ +--- +products: + - apm +--- + # Code Code blocks can be used to display multiple lines of code. They preserve formatting and provide syntax highlighting when possible. diff --git a/src/Elastic.Documentation.Configuration/Builder/ConfigurationFile.cs b/src/Elastic.Documentation.Configuration/Builder/ConfigurationFile.cs index cf42f94de..8287237a7 100644 --- a/src/Elastic.Documentation.Configuration/Builder/ConfigurationFile.cs +++ b/src/Elastic.Documentation.Configuration/Builder/ConfigurationFile.cs @@ -4,6 +4,7 @@ using System.IO.Abstractions; using DotNet.Globbing; +using Elastic.Documentation.Configuration.Suggestions; using Elastic.Documentation.Configuration.TableOfContents; using Elastic.Documentation.Links; using Elastic.Documentation.Navigation; @@ -33,6 +34,8 @@ public record ConfigurationFile : ITableOfContentsScope public Dictionary? Redirects { get; } + public HashSet Products { get; } = new(StringComparer.Ordinal); + public HashSet ImplicitFolders { get; } = new(StringComparer.OrdinalIgnoreCase); public Glob[] Globs { get; } = []; @@ -104,6 +107,20 @@ public ConfigurationFile(IDocumentationContext context) case "toc": // read this later break; + case "products": + var productIds = YamlStreamReader.ReadStringArray(entry.Entry); + foreach (var productId in productIds) + { + if (!Builder.Products.AllById.ContainsKey(productId)) + { + var message = + $"Product \"{productId}\" not found in the product list. {new Suggestion(Builder.Products.All.Select(p => p.Id).ToHashSet(), productId).GetSuggestionQuestion()}"; + reader.EmitError(message, entry.Entry.Value); + } + else + _ = Products.Add(productId); + } + break; case "features": _features = reader.ReadDictionary(entry.Entry).ToDictionary(k => k.Key, v => bool.Parse(v.Value), StringComparer.OrdinalIgnoreCase); break; diff --git a/src/Elastic.Documentation.Configuration/Builder/Products.cs b/src/Elastic.Documentation.Configuration/Builder/Products.cs new file mode 100644 index 000000000..25457f66e --- /dev/null +++ b/src/Elastic.Documentation.Configuration/Builder/Products.cs @@ -0,0 +1,89 @@ +// 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; + +namespace Elastic.Documentation.Configuration.Builder; + +public record Product(string Id, string DisplayName); + +public static class Products +{ + public static FrozenSet All { get; } = [ + new("apm", "APM"), + new("apm-dotnet-agent", "APM .NET Agent"), + new("apm-android-agent", "APM Android Agent"), + new("apm-attacher", "APM Attacher"), + new("apm-aws-lambda-extension", "APM AWS Lambda extension"), + new("apm-go-agent", "APM Go Agent"), + new("apm-ios-agent", "APM iOS Agent"), + new("apm-java-agent", "APM Java Agent"), + new("apm-node-agent", "APM Node.js Agent"), + new("apm-php-agent", "APM PHP Agent"), + new("apm-python-agent", "APM Python Agent"), + new("apm-ruby-agent", "APM Ruby Agent"), + new("apm-rum-agent", "APM RUM Agent"), + new("beats-logging-plugin", "Beats Logging plugin"), + new("cloud-control-ecctl", "Cloud Control ECCTL"), + new("cloud-enterprise", "Cloud Enterprise"), + new("cloud-hosted", "Cloud Hosted"), + new("cloud-kubernetes", "Cloud Kubernetes"), + new("cloud-native-ingest", "Cloud Native Ingest"), + new("cloud-serverless", "Cloud Serverless"), + new("cloud-terraform", "Cloud Terraform"), + new("ecs-logging", "ECS Logging"), + new("ecs-logging-dotnet", "ECS Logging .NET"), + new("ecs-logging-go-logrus", "ECS Logging Go Logrus"), + new("ecs-logging-go-zap", "ECS Logging Go Zap"), + new("ecs-logging-go-zerolog", "ECS Logging Go Zerolog"), + new("ecs-logging-java", "ECS Logging Java"), + new("ecs-logging-node", "ECS Logging Node.js"), + new("ecs-logging-php", "ECS Logging PHP"), + new("ecs-logging-python", "ECS Logging Python"), + new("ecs-logging-ruby", "ECS Logging Ruby"), + new("elastic-agent", "Elastic Agent"), + new("ecs", "Elastic Common Schema (ECS)"), + new("elastic-products-platform", "Elastic Products platform"), + new("elastic-stack", "Elastic Stack"), + new("elasticsearch", "Elasticsearch"), + new("elasticsearch-dotnet-client", "Elasticsearch .NET Client"), + new("elasticsearch-apache-hadoop", "Elasticsearch Apache Hadoop"), + new("elasticsearch-cloud-hosted-heroku", "Elasticsearch Cloud Hosted Heroku"), + new("elasticsearch-community-clients", "Elasticsearch community clients"), + new("elasticsearch-curator", "Elasticsearch Curator"), + new("elasticsearch-eland-python-client", "Elasticsearch Eland Python Client"), + new("elasticsearch-go-client", "Elasticsearch Go Client"), + new("elasticsearch-groovy-client", "Elasticsearch Groovy Client"), + new("elasticsearch-java-client", "Elasticsearch Java Client"), + new("elasticsearch-java-script-client", "Elasticsearch JavaScript Client"), + new("elasticsearch-painless-scripting-language", "Elasticsearch Painless scripting language"), + new("elasticsearch-perl-client", "Elasticsearch Perl Client"), + new("elasticsearch-php-client", "Elasticsearch PHP Client"), + new("elasticsearch-plugins", "Elasticsearch plugins"), + new("elasticsearch-python-client", "Elasticsearch Python Client"), + new("elasticsearch-resiliency-status", "Elasticsearch Resiliency Status"), + new("elasticsearch-ruby-client", "Elasticsearch Ruby Client"), + new("elasticsearch-rust-client", "Elasticsearch Rust Client"), + new("fleet", "Fleet"), + new("ingest", "Ingest"), + new("integrations", "Integrations"), + new("kibana", "Kibana"), + new("logstash", "Logstash"), + new("machine-learning", "Machine Learning"), + new("observability", "Observability"), + new("reference-architectures", "Reference Architectures"), + new("search-ui", "Search UI"), + new("security", "Security"), + new("edot-collector", "Elastic Distribution of OpenTelemetry Collector"), + new("edot-java", "Elastic Distribution of OpenTelemetry Java"), + new("edot-dotnet", "Elastic Distribution of OpenTelemetry .NET"), + new("edot-nodejs", "Elastic Distribution of OpenTelemetry Node.js"), + new("edot-php", "Elastic Distribution of OpenTelemetry PHP"), + new("edot-python", "Elastic Distribution of OpenTelemetry Python"), + new("edot-android", "Elastic Distribution of OpenTelemetry Android"), + new("edot-ios", "Elastic Distribution of OpenTelemetry iOS") + ]; + + public static FrozenDictionary AllById { get; } = All.ToDictionary(p => p.Id, StringComparer.Ordinal).ToFrozenDictionary(); +} diff --git a/src/Elastic.Markdown/Suggestions/Suggestions.cs b/src/Elastic.Documentation.Configuration/Suggestions/Suggestions.cs similarity index 96% rename from src/Elastic.Markdown/Suggestions/Suggestions.cs rename to src/Elastic.Documentation.Configuration/Suggestions/Suggestions.cs index 9674407a9..ac194fd31 100644 --- a/src/Elastic.Markdown/Suggestions/Suggestions.cs +++ b/src/Elastic.Documentation.Configuration/Suggestions/Suggestions.cs @@ -2,7 +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 -namespace Elastic.Markdown.Suggestions; +namespace Elastic.Documentation.Configuration.Suggestions; public class Suggestion(IReadOnlySet candidates, string input) { diff --git a/src/Elastic.Markdown/Myst/FrontMatter/FrontMatterParser.cs b/src/Elastic.Markdown/Myst/FrontMatter/FrontMatterParser.cs index 70f65f1f0..9b4ce9831 100644 --- a/src/Elastic.Markdown/Myst/FrontMatter/FrontMatterParser.cs +++ b/src/Elastic.Markdown/Myst/FrontMatter/FrontMatterParser.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.Documentation.Configuration.Builder; using YamlDotNet.Serialization; namespace Elastic.Markdown.Myst.FrontMatter; diff --git a/src/Elastic.Markdown/Myst/FrontMatter/Products.cs b/src/Elastic.Markdown/Myst/FrontMatter/Products.cs index e3eb66102..88c3e2b32 100644 --- a/src/Elastic.Markdown/Myst/FrontMatter/Products.cs +++ b/src/Elastic.Markdown/Myst/FrontMatter/Products.cs @@ -3,96 +3,14 @@ // See the LICENSE file in the project root for more information using System.Collections.Frozen; -using System.ComponentModel.DataAnnotations; -using Elastic.Markdown.Suggestions; +using Elastic.Documentation.Configuration.Builder; +using Elastic.Documentation.Configuration.Suggestions; using YamlDotNet.Core; using YamlDotNet.Core.Events; using YamlDotNet.Serialization; namespace Elastic.Markdown.Myst.FrontMatter; -public record Product(string Id, string DisplayName); - -public static class Products -{ - public static FrozenSet All { get; } = [ - new("apm", "APM"), - new("apm-dotnet-agent", "APM .NET Agent"), - new("apm-android-agent", "APM Android Agent"), - new("apm-attacher", "APM Attacher"), - new("apm-aws-lambda-extension", "APM AWS Lambda extension"), - new("apm-go-agent", "APM Go Agent"), - new("apm-ios-agent", "APM iOS Agent"), - new("apm-java-agent", "APM Java Agent"), - new("apm-node-agent", "APM Node.js Agent"), - new("apm-php-agent", "APM PHP Agent"), - new("apm-python-agent", "APM Python Agent"), - new("apm-ruby-agent", "APM Ruby Agent"), - new("apm-rum-agent", "APM RUM Agent"), - new("beats-logging-plugin", "Beats Logging plugin"), - new("cloud-control-ecctl", "Cloud Control ECCTL"), - new("cloud-enterprise", "Cloud Enterprise"), - new("cloud-hosted", "Cloud Hosted"), - new("cloud-kubernetes", "Cloud Kubernetes"), - new("cloud-native-ingest", "Cloud Native Ingest"), - new("cloud-serverless", "Cloud Serverless"), - new("cloud-terraform", "Cloud Terraform"), - new("ecs-logging", "ECS Logging"), - new("ecs-logging-dotnet", "ECS Logging .NET"), - new("ecs-logging-go-logrus", "ECS Logging Go Logrus"), - new("ecs-logging-go-zap", "ECS Logging Go Zap"), - new("ecs-logging-go-zerolog", "ECS Logging Go Zerolog"), - new("ecs-logging-java", "ECS Logging Java"), - new("ecs-logging-node", "ECS Logging Node.js"), - new("ecs-logging-php", "ECS Logging PHP"), - new("ecs-logging-python", "ECS Logging Python"), - new("ecs-logging-ruby", "ECS Logging Ruby"), - new("elastic-agent", "Elastic Agent"), - new("ecs", "Elastic Common Schema (ECS)"), - new("elastic-products-platform", "Elastic Products platform"), - new("elastic-stack", "Elastic Stack"), - new("elasticsearch", "Elasticsearch"), - new("elasticsearch-dotnet-client", "Elasticsearch .NET Client"), - new("elasticsearch-apache-hadoop", "Elasticsearch Apache Hadoop"), - new("elasticsearch-cloud-hosted-heroku", "Elasticsearch Cloud Hosted Heroku"), - new("elasticsearch-community-clients", "Elasticsearch community clients"), - new("elasticsearch-curator", "Elasticsearch Curator"), - new("elasticsearch-eland-python-client", "Elasticsearch Eland Python Client"), - new("elasticsearch-go-client", "Elasticsearch Go Client"), - new("elasticsearch-groovy-client", "Elasticsearch Groovy Client"), - new("elasticsearch-java-client", "Elasticsearch Java Client"), - new("elasticsearch-java-script-client", "Elasticsearch JavaScript Client"), - new("elasticsearch-painless-scripting-language", "Elasticsearch Painless scripting language"), - new("elasticsearch-perl-client", "Elasticsearch Perl Client"), - new("elasticsearch-php-client", "Elasticsearch PHP Client"), - new("elasticsearch-plugins", "Elasticsearch plugins"), - new("elasticsearch-python-client", "Elasticsearch Python Client"), - new("elasticsearch-resiliency-status", "Elasticsearch Resiliency Status"), - new("elasticsearch-ruby-client", "Elasticsearch Ruby Client"), - new("elasticsearch-rust-client", "Elasticsearch Rust Client"), - new("fleet", "Fleet"), - new("ingest", "Ingest"), - new("integrations", "Integrations"), - new("kibana", "Kibana"), - new("logstash", "Logstash"), - new("machine-learning", "Machine Learning"), - new("observability", "Observability"), - new("reference-architectures", "Reference Architectures"), - new("search-ui", "Search UI"), - new("security", "Security"), - new("edot-collector", "Elastic Distribution of OpenTelemetry Collector"), - new("edot-java", "Elastic Distribution of OpenTelemetry Java"), - new("edot-dotnet", "Elastic Distribution of OpenTelemetry .NET"), - new("edot-nodejs", "Elastic Distribution of OpenTelemetry Node.js"), - new("edot-php", "Elastic Distribution of OpenTelemetry PHP"), - new("edot-python", "Elastic Distribution of OpenTelemetry Python"), - new("edot-android", "Elastic Distribution of OpenTelemetry Android"), - new("edot-ios", "Elastic Distribution of OpenTelemetry iOS") - ]; - - public static FrozenDictionary AllById { get; } = All.ToDictionary(p => p.Id, StringComparer.Ordinal).ToFrozenDictionary(); -} - public class ProductConverter : IYamlTypeConverter { public bool Accepts(Type type) => type == typeof(Product); diff --git a/src/Elastic.Markdown/Slices/HtmlWriter.cs b/src/Elastic.Markdown/Slices/HtmlWriter.cs index 02d024a9a..4e85fb1bc 100644 --- a/src/Elastic.Markdown/Slices/HtmlWriter.cs +++ b/src/Elastic.Markdown/Slices/HtmlWriter.cs @@ -5,10 +5,12 @@ using System.Collections.Concurrent; using System.IO.Abstractions; using Elastic.Documentation; +using Elastic.Documentation.Configuration.Builder; using Elastic.Documentation.Legacy; using Elastic.Markdown.Extensions.DetectionRules; using Elastic.Markdown.IO; using Elastic.Markdown.IO.Navigation; +using Elastic.Markdown.Myst.FrontMatter; using Markdig.Syntax; using RazorSlices; using IFileInfo = System.IO.Abstractions.IFileInfo; @@ -115,6 +117,20 @@ private async Task RenderLayout(MarkdownFile markdown, MarkdownDocument var legacyPage = LegacyUrlMapper.MapLegacyUrl(markdown.YamlFrontMatter?.MappedPages); + var configProducts = DocumentationSet.Configuration.Products.Select(p => + { + if (Products.AllById.TryGetValue(p, out var product)) + return product; + throw new ArgumentException($"Invalid product id: {p}"); + }); + + var frontMatterProducts = markdown.YamlFrontMatter?.Products ?? []; + + var allProducts = frontMatterProducts + .Union(configProducts) + .Distinct() + .ToHashSet(); + var slice = Index.Create(new IndexViewModel { SiteName = siteName, @@ -139,7 +155,8 @@ private async Task RenderLayout(MarkdownFile markdown, MarkdownDocument Features = DocumentationSet.Configuration.Features, StaticFileContentHashProvider = StaticFileContentHashProvider, ReportIssueUrl = reportUrl, - LegacyPage = legacyPage + LegacyPage = legacyPage, + Products = allProducts }); return await slice.RenderAsync(cancellationToken: ctx); } diff --git a/src/Elastic.Markdown/Slices/Index.cshtml b/src/Elastic.Markdown/Slices/Index.cshtml index e100a5c98..7ba7b353c 100644 --- a/src/Elastic.Markdown/Slices/Index.cshtml +++ b/src/Elastic.Markdown/Slices/Index.cshtml @@ -23,7 +23,7 @@ StaticFileContentHashProvider = Model.StaticFileContentHashProvider, ReportIssueUrl = Model.ReportIssueUrl, LegacyPage = Model.LegacyPage, - Products = Model.CurrentDocument.YamlFrontMatter?.Products is { Count: > 0} products ? string.Join(",", products.Select(p => p.DisplayName)) : null, + Products = Model.Products is { Count: > 0} products ? string.Join(",", products.Select(p => p.DisplayName)) : null, }; }
diff --git a/src/Elastic.Markdown/Slices/_ViewModels.cs b/src/Elastic.Markdown/Slices/_ViewModels.cs index 36a9a745a..1eea990c1 100644 --- a/src/Elastic.Markdown/Slices/_ViewModels.cs +++ b/src/Elastic.Markdown/Slices/_ViewModels.cs @@ -39,6 +39,8 @@ public class IndexViewModel public required FeatureFlags Features { get; init; } public required StaticFileContentHashProvider StaticFileContentHashProvider { get; init; } + + public required HashSet Products { get; init; } } public class LayoutViewModel