From 723a8a945ca03b12361c98d29f0b56e252091cb3 Mon Sep 17 00:00:00 2001 From: Martijn Laarman Date: Thu, 12 Dec 2024 12:32:21 +0100 Subject: [PATCH 1/6] Add initial support for page level annotations for product support --- src/Elastic.Markdown/Helpers/SemVersion.cs | 8 +++ .../Myst/FrontMatterParser.cs | 60 +++++++++++++++++++ .../FrontMatter/ProductConstraintTests.cs | 43 +++++++++++++ .../YamlFrontMatterTests.cs | 4 +- 4 files changed, 114 insertions(+), 1 deletion(-) create mode 100644 tests/Elastic.Markdown.Tests/FrontMatter/ProductConstraintTests.cs rename tests/Elastic.Markdown.Tests/{Directives => FrontMatter}/YamlFrontMatterTests.cs (93%) diff --git a/src/Elastic.Markdown/Helpers/SemVersion.cs b/src/Elastic.Markdown/Helpers/SemVersion.cs index d59f7caa4..cf089b214 100644 --- a/src/Elastic.Markdown/Helpers/SemVersion.cs +++ b/src/Elastic.Markdown/Helpers/SemVersion.cs @@ -92,6 +92,14 @@ public SemVersion(int major, int minor, int patch, string? prerelease, string? m Metadata = metadata ?? string.Empty; } + public static explicit operator SemVersion(string b) + { + var semVersion = TryParse(b, out var version) ? version : TryParse(b + ".0", out version) ? version : null; + return semVersion ?? throw new ArgumentException($"'{b}' is not a valid semver2 version string."); + } + + public static implicit operator string(SemVersion d) => d.ToString(); + /// /// /// diff --git a/src/Elastic.Markdown/Myst/FrontMatterParser.cs b/src/Elastic.Markdown/Myst/FrontMatterParser.cs index 71efe322f..7b250420b 100644 --- a/src/Elastic.Markdown/Myst/FrontMatterParser.cs +++ b/src/Elastic.Markdown/Myst/FrontMatterParser.cs @@ -1,6 +1,10 @@ // 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 Elastic.Markdown.Helpers; +using YamlDotNet.Core; +using YamlDotNet.Core.Events; using YamlDotNet.Serialization; using YamlDotNet.Serialization.NamingConventions; @@ -20,6 +24,43 @@ public class YamlFrontMatter [YamlMember(Alias = "sub")] public Dictionary? Properties { get; set; } + + + [YamlMember(Alias = "applies")] + public DeploymentType? AppliesTo { get; set; } +} + +[YamlSerializable] +public class DeploymentType +{ + [YamlMember(Alias = "self")] + public SelfManagedDeployment? SelfManaged { get; set; } + + [YamlMember(Alias = "cloud")] + public CloudManagedDeployment? Cloud { get; set; } +} + +[YamlSerializable] +public class SelfManagedDeployment +{ + [YamlMember(Alias = "stack")] + public SemVersion? Stack { get; set; } + + [YamlMember(Alias = "ece")] + public SemVersion? Ece { get; set; } + + [YamlMember(Alias = "eck")] + public SemVersion? Eck { get; set; } +} + +[YamlSerializable] +public class CloudManagedDeployment +{ + [YamlMember(Alias = "ess")] + public SemVersion? Ess { get; set; } + + [YamlMember(Alias = "serverless")] + public SemVersion? Serverless { get; set; } } public static class FrontMatterParser @@ -30,6 +71,7 @@ public static YamlFrontMatter Deserialize(string yaml) var deserializer = new StaticDeserializerBuilder(new YamlFrontMatterStaticContext()) .IgnoreUnmatchedProperties() + .WithTypeConverter(new SemVersionConverter()) .Build(); var frontMatter = deserializer.Deserialize(input); @@ -37,3 +79,21 @@ public static YamlFrontMatter Deserialize(string yaml) } } + +public class SemVersionConverter : IYamlTypeConverter +{ + public bool Accepts(Type type) => type == typeof(SemVersion); + + public object? ReadYaml(IParser parser, Type type, ObjectDeserializer rootDeserializer) + { + var value = parser.Consume(); + return (SemVersion)value.Value; + } + + public void WriteYaml(IEmitter emitter, object? value, Type type, ObjectSerializer serializer) + { + if (value == null) + return; + emitter.Emit(new Scalar(value.ToString()!)); + } +} diff --git a/tests/Elastic.Markdown.Tests/FrontMatter/ProductConstraintTests.cs b/tests/Elastic.Markdown.Tests/FrontMatter/ProductConstraintTests.cs new file mode 100644 index 000000000..7deabf49a --- /dev/null +++ b/tests/Elastic.Markdown.Tests/FrontMatter/ProductConstraintTests.cs @@ -0,0 +1,43 @@ +// 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 Elastic.Markdown.Helpers; +using Elastic.Markdown.Tests.Directives; +using FluentAssertions; +using Xunit.Abstractions; + +namespace Elastic.Markdown.Tests.FrontMatter; + +public class ProductConstraintTests(ITestOutputHelper output) : DirectiveTest(output, +""" +--- +title: Elastic Docs v3 +navigation_title: "Documentation Guide" +applies: + self: + stack: 7.7 + cloud: + serverless: 1.0.0 +--- +""" +) +{ + [Fact] + public void ReadsTitle() => File.Title.Should().Be("Elastic Docs v3"); + + [Fact] + public void ReadsNavigationTitle() => File.NavigationTitle.Should().Be("Documentation Guide"); + + [Fact] + public void ReadsSubstitutions() + { + File.YamlFrontMatter.Should().NotBeNull(); + var appliesTo = File.YamlFrontMatter!.AppliesTo; + appliesTo.Should().NotBeNull(); + appliesTo!.SelfManaged.Should().NotBeNull(); + appliesTo.Cloud.Should().NotBeNull(); + appliesTo.Cloud!.Serverless.Should().BeEquivalentTo(new SemVersion(1,0,0)); + } +} + diff --git a/tests/Elastic.Markdown.Tests/Directives/YamlFrontMatterTests.cs b/tests/Elastic.Markdown.Tests/FrontMatter/YamlFrontMatterTests.cs similarity index 93% rename from tests/Elastic.Markdown.Tests/Directives/YamlFrontMatterTests.cs rename to tests/Elastic.Markdown.Tests/FrontMatter/YamlFrontMatterTests.cs index c478f37c3..178773ddc 100644 --- a/tests/Elastic.Markdown.Tests/Directives/YamlFrontMatterTests.cs +++ b/tests/Elastic.Markdown.Tests/FrontMatter/YamlFrontMatterTests.cs @@ -1,10 +1,12 @@ // 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 Elastic.Markdown.Tests.Directives; using FluentAssertions; using Xunit.Abstractions; -namespace Elastic.Markdown.Tests.Directives; +namespace Elastic.Markdown.Tests.FrontMatter; public class YamlFrontMatterTests(ITestOutputHelper output) : DirectiveTest(output, """ From 1fb3b1c9e079a01c8fef20118a44a943daba087d Mon Sep 17 00:00:00 2001 From: Martijn Laarman Date: Thu, 12 Dec 2024 16:49:30 +0100 Subject: [PATCH 2/6] Updated parsers --- src/Elastic.Markdown/Helpers/SemVersion.cs | 2 +- .../Myst/FrontMatterParser.cs | 91 ++++++++++++++++++- .../FrontMatter/ProductConstraintTests.cs | 50 ++++++++++ 3 files changed, 140 insertions(+), 3 deletions(-) diff --git a/src/Elastic.Markdown/Helpers/SemVersion.cs b/src/Elastic.Markdown/Helpers/SemVersion.cs index cf089b214..e365257e7 100644 --- a/src/Elastic.Markdown/Helpers/SemVersion.cs +++ b/src/Elastic.Markdown/Helpers/SemVersion.cs @@ -11,7 +11,7 @@ namespace Elastic.Markdown.Helpers; /// /// A semver2 compatible version. /// -public sealed class SemVersion : +public class SemVersion : IEquatable, IComparable, IComparable diff --git a/src/Elastic.Markdown/Myst/FrontMatterParser.cs b/src/Elastic.Markdown/Myst/FrontMatterParser.cs index 7b250420b..34da7c830 100644 --- a/src/Elastic.Markdown/Myst/FrontMatterParser.cs +++ b/src/Elastic.Markdown/Myst/FrontMatterParser.cs @@ -27,11 +27,11 @@ public class YamlFrontMatter [YamlMember(Alias = "applies")] - public DeploymentType? AppliesTo { get; set; } + public Deployment? AppliesTo { get; set; } } [YamlSerializable] -public class DeploymentType +public class Deployment { [YamlMember(Alias = "self")] public SelfManagedDeployment? SelfManaged { get; set; } @@ -40,6 +40,11 @@ public class DeploymentType public CloudManagedDeployment? Cloud { get; set; } } +public class AllVersions() : SemVersion(9999, 9999, 9999) +{ + public static AllVersions Instance { get; } = new (); +} + [YamlSerializable] public class SelfManagedDeployment { @@ -51,6 +56,13 @@ public class SelfManagedDeployment [YamlMember(Alias = "eck")] public SemVersion? Eck { get; set; } + + public static SelfManagedDeployment All { get; } = new() + { + Stack = AllVersions.Instance, + Ece = AllVersions.Instance, + Eck = AllVersions.Instance + }; } [YamlSerializable] @@ -61,6 +73,13 @@ public class CloudManagedDeployment [YamlMember(Alias = "serverless")] public SemVersion? Serverless { get; set; } + + public static CloudManagedDeployment All { get; } = new() + { + Ess = AllVersions.Instance, + Serverless = AllVersions.Instance + }; + } public static class FrontMatterParser @@ -72,6 +91,8 @@ public static YamlFrontMatter Deserialize(string yaml) var deserializer = new StaticDeserializerBuilder(new YamlFrontMatterStaticContext()) .IgnoreUnmatchedProperties() .WithTypeConverter(new SemVersionConverter()) + .WithTypeConverter(new CloudManagedSerializer()) + .WithTypeConverter(new SelfManagedSerializer()) .Build(); var frontMatter = deserializer.Deserialize(input); @@ -87,6 +108,10 @@ public class SemVersionConverter : IYamlTypeConverter public object? ReadYaml(IParser parser, Type type, ObjectDeserializer rootDeserializer) { var value = parser.Consume(); + if (string.IsNullOrWhiteSpace(value.Value)) + return AllVersions.Instance; + if (string.Equals(value.Value, "all", StringComparison.InvariantCultureIgnoreCase)) + return AllVersions.Instance; return (SemVersion)value.Value; } @@ -97,3 +122,65 @@ public void WriteYaml(IEmitter emitter, object? value, Type type, ObjectSerializ emitter.Emit(new Scalar(value.ToString()!)); } } + +public class CloudManagedSerializer : IYamlTypeConverter +{ + public bool Accepts(Type type) => type == typeof(CloudManagedDeployment); + + public object? ReadYaml(IParser parser, Type type, ObjectDeserializer rootDeserializer) + { + if (parser.TryConsume(out var value)) + { + if (string.IsNullOrWhiteSpace(value.Value)) + return CloudManagedDeployment.All; + if (string.Equals(value.Value, "all", StringComparison.InvariantCultureIgnoreCase)) + return CloudManagedDeployment.All; + } + var x = rootDeserializer.Invoke(typeof(Dictionary)); + if (x is not Dictionary { Count: > 0 } dictionary) + return null; + + var cloudManaged = new CloudManagedDeployment(); + if (dictionary.TryGetValue("ess", out var v)) + cloudManaged.Ess = (SemVersion)v; + if (dictionary.TryGetValue("serverless", out v)) + cloudManaged.Serverless = (SemVersion)v; + return cloudManaged; + + } + + public void WriteYaml(IEmitter emitter, object? value, Type type, ObjectSerializer serializer) => + serializer.Invoke(value, type); +} + +public class SelfManagedSerializer : IYamlTypeConverter +{ + public bool Accepts(Type type) => type == typeof(SelfManagedDeployment); + + public object? ReadYaml(IParser parser, Type type, ObjectDeserializer rootDeserializer) + { + if (parser.TryConsume(out var value)) + { + if (string.IsNullOrWhiteSpace(value.Value)) + return SelfManagedDeployment.All; + if (string.Equals(value.Value, "all", StringComparison.InvariantCultureIgnoreCase)) + return SelfManagedDeployment.All; + } + var x = rootDeserializer.Invoke(typeof(Dictionary)); + if (x is not Dictionary { Count: > 0 } dictionary) + return null; + + var deployment = new SelfManagedDeployment(); + if (dictionary.TryGetValue("stack", out var v)) + deployment.Stack = (SemVersion)v; + if (dictionary.TryGetValue("ece", out v)) + deployment.Ece = (SemVersion)v; + if (dictionary.TryGetValue("eck", out v)) + deployment.Eck = (SemVersion)v; + return deployment; + + } + + public void WriteYaml(IEmitter emitter, object? value, Type type, ObjectSerializer serializer) => + serializer.Invoke(value, type); +} diff --git a/tests/Elastic.Markdown.Tests/FrontMatter/ProductConstraintTests.cs b/tests/Elastic.Markdown.Tests/FrontMatter/ProductConstraintTests.cs index 7deabf49a..a119eb54d 100644 --- a/tests/Elastic.Markdown.Tests/FrontMatter/ProductConstraintTests.cs +++ b/tests/Elastic.Markdown.Tests/FrontMatter/ProductConstraintTests.cs @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information using Elastic.Markdown.Helpers; +using Elastic.Markdown.Myst; using Elastic.Markdown.Tests.Directives; using FluentAssertions; using Xunit.Abstractions; @@ -41,3 +42,52 @@ public void ReadsSubstitutions() } } +public class CanSpecifyAllForProductVersions(ITestOutputHelper output) : DirectiveTest(output, +""" +--- +title: Elastic Docs v3 +navigation_title: "Documentation Guide" +applies: + self: + stack: all +--- +""" +) +{ + [Fact] + public void Assert() => + File.YamlFrontMatter!.AppliesTo!.SelfManaged!.Stack.Should().BeEquivalentTo(AllVersions.Instance); +} + +public class EmptyProductVersionMeansAll(ITestOutputHelper output) : DirectiveTest(output, +""" +--- +title: Elastic Docs v3 +navigation_title: "Documentation Guide" +applies: + self: + stack: +--- +""" +) +{ + [Fact] + public void Assert() => + File.YamlFrontMatter!.AppliesTo!.SelfManaged!.Stack.Should().BeEquivalentTo(AllVersions.Instance); +} + +public class EmptyCloudSetsAllProductsToAll(ITestOutputHelper output) : DirectiveTest(output, +""" +--- +title: Elastic Docs v3 +navigation_title: "Documentation Guide" +applies: + cloud: +--- +""" +) +{ + [Fact] + public void Assert() => + File.YamlFrontMatter!.AppliesTo!.Cloud!.Ess.Should().BeEquivalentTo(AllVersions.Instance); +} From 3acc7ee717d2d9937a801817346972de3adda1b9 Mon Sep 17 00:00:00 2001 From: Martijn Laarman Date: Thu, 12 Dec 2024 17:37:53 +0100 Subject: [PATCH 3/6] rename ess to hosted --- .../Myst/FrontMatterParser.cs | 27 ++++++++++++------- .../FrontMatter/ProductConstraintTests.cs | 2 +- 2 files changed, 19 insertions(+), 10 deletions(-) diff --git a/src/Elastic.Markdown/Myst/FrontMatterParser.cs b/src/Elastic.Markdown/Myst/FrontMatterParser.cs index 34da7c830..3aa5fb1b9 100644 --- a/src/Elastic.Markdown/Myst/FrontMatterParser.cs +++ b/src/Elastic.Markdown/Myst/FrontMatterParser.cs @@ -68,15 +68,15 @@ public class SelfManagedDeployment [YamlSerializable] public class CloudManagedDeployment { - [YamlMember(Alias = "ess")] - public SemVersion? Ess { get; set; } + [YamlMember(Alias = "hosted")] + public SemVersion? Hosted { get; set; } [YamlMember(Alias = "serverless")] public SemVersion? Serverless { get; set; } public static CloudManagedDeployment All { get; } = new() { - Ess = AllVersions.Instance, + Hosted = AllVersions.Instance, Serverless = AllVersions.Instance }; @@ -121,6 +121,15 @@ public void WriteYaml(IEmitter emitter, object? value, Type type, ObjectSerializ return; emitter.Emit(new Scalar(value.ToString()!)); } + + public static SemVersion? Parse(string? value) => + value?.Trim().ToLowerInvariant() switch + { + null => AllVersions.Instance, + "all" => AllVersions.Instance, + "" => AllVersions.Instance, + _ => SemVersion.TryParse(value, out var v) ? v : null + }; } public class CloudManagedSerializer : IYamlTypeConverter @@ -141,10 +150,10 @@ public class CloudManagedSerializer : IYamlTypeConverter return null; var cloudManaged = new CloudManagedDeployment(); - if (dictionary.TryGetValue("ess", out var v)) - cloudManaged.Ess = (SemVersion)v; + if (dictionary.TryGetValue("hosted", out var v)) + cloudManaged.Hosted = SemVersionConverter.Parse(v); if (dictionary.TryGetValue("serverless", out v)) - cloudManaged.Serverless = (SemVersion)v; + cloudManaged.Serverless = SemVersionConverter.Parse(v); return cloudManaged; } @@ -172,11 +181,11 @@ public class SelfManagedSerializer : IYamlTypeConverter var deployment = new SelfManagedDeployment(); if (dictionary.TryGetValue("stack", out var v)) - deployment.Stack = (SemVersion)v; + deployment.Stack = SemVersionConverter.Parse(v); if (dictionary.TryGetValue("ece", out v)) - deployment.Ece = (SemVersion)v; + deployment.Ece = SemVersionConverter.Parse(v); if (dictionary.TryGetValue("eck", out v)) - deployment.Eck = (SemVersion)v; + deployment.Eck = SemVersionConverter.Parse(v); return deployment; } diff --git a/tests/Elastic.Markdown.Tests/FrontMatter/ProductConstraintTests.cs b/tests/Elastic.Markdown.Tests/FrontMatter/ProductConstraintTests.cs index a119eb54d..9ece827d2 100644 --- a/tests/Elastic.Markdown.Tests/FrontMatter/ProductConstraintTests.cs +++ b/tests/Elastic.Markdown.Tests/FrontMatter/ProductConstraintTests.cs @@ -89,5 +89,5 @@ public class EmptyCloudSetsAllProductsToAll(ITestOutputHelper output) : Directiv { [Fact] public void Assert() => - File.YamlFrontMatter!.AppliesTo!.Cloud!.Ess.Should().BeEquivalentTo(AllVersions.Instance); + File.YamlFrontMatter!.AppliesTo!.Cloud!.Hosted.Should().BeEquivalentTo(AllVersions.Instance); } From 6c5b38756f68faad1d12c20d30811b54bd276621 Mon Sep 17 00:00:00 2001 From: Martijn Laarman Date: Fri, 13 Dec 2024 12:39:48 +0100 Subject: [PATCH 4/6] update applies to syntax to include product lifecycle --- src/Elastic.Markdown/IO/MarkdownFile.cs | 1 + .../Myst/Directives/IncludeBlock.cs | 1 + .../Myst/FrontMatter/FrontMatterParser.cs | 46 +++++ .../Myst/FrontMatterParser.cs | 195 ------------------ src/Elastic.Markdown/Myst/MarkdownParser.cs | 1 + src/Elastic.Markdown/Myst/ParserContext.cs | 1 + .../FrontMatter/ProductConstraintTests.cs | 79 +++++-- 7 files changed, 107 insertions(+), 217 deletions(-) create mode 100644 src/Elastic.Markdown/Myst/FrontMatter/FrontMatterParser.cs delete mode 100644 src/Elastic.Markdown/Myst/FrontMatterParser.cs diff --git a/src/Elastic.Markdown/IO/MarkdownFile.cs b/src/Elastic.Markdown/IO/MarkdownFile.cs index ece680f78..477b61943 100644 --- a/src/Elastic.Markdown/IO/MarkdownFile.cs +++ b/src/Elastic.Markdown/IO/MarkdownFile.cs @@ -5,6 +5,7 @@ using Elastic.Markdown.Diagnostics; using Elastic.Markdown.Myst; using Elastic.Markdown.Myst.Directives; +using Elastic.Markdown.Myst.FrontMatter; using Elastic.Markdown.Slices; using Markdig; using Markdig.Extensions.Yaml; diff --git a/src/Elastic.Markdown/Myst/Directives/IncludeBlock.cs b/src/Elastic.Markdown/Myst/Directives/IncludeBlock.cs index 974fb323d..28dd4c586 100644 --- a/src/Elastic.Markdown/Myst/Directives/IncludeBlock.cs +++ b/src/Elastic.Markdown/Myst/Directives/IncludeBlock.cs @@ -4,6 +4,7 @@ using System.IO.Abstractions; using Elastic.Markdown.Diagnostics; using Elastic.Markdown.IO; +using Elastic.Markdown.Myst.FrontMatter; namespace Elastic.Markdown.Myst.Directives; diff --git a/src/Elastic.Markdown/Myst/FrontMatter/FrontMatterParser.cs b/src/Elastic.Markdown/Myst/FrontMatter/FrontMatterParser.cs new file mode 100644 index 000000000..694045a1c --- /dev/null +++ b/src/Elastic.Markdown/Myst/FrontMatter/FrontMatterParser.cs @@ -0,0 +1,46 @@ +// 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 YamlDotNet.Serialization; + +namespace Elastic.Markdown.Myst.FrontMatter; + +[YamlStaticContext] +public partial class YamlFrontMatterStaticContext; + +[YamlSerializable] +public class YamlFrontMatter +{ + [YamlMember(Alias = "title")] + public string? Title { get; set; } + + [YamlMember(Alias = "navigation_title")] + public string? NavigationTitle { get; set; } + + [YamlMember(Alias = "sub")] + public Dictionary? Properties { get; set; } + + + [YamlMember(Alias = "applies")] + public Deployment? AppliesTo { get; set; } +} + +public static class FrontMatterParser +{ + public static YamlFrontMatter Deserialize(string yaml) + { + var input = new StringReader(yaml); + + var deserializer = new StaticDeserializerBuilder(new YamlFrontMatterStaticContext()) + .IgnoreUnmatchedProperties() + .WithTypeConverter(new SemVersionConverter()) + .WithTypeConverter(new DeploymentConverter()) + .Build(); + + var frontMatter = deserializer.Deserialize(input); + return frontMatter; + + } +} + diff --git a/src/Elastic.Markdown/Myst/FrontMatterParser.cs b/src/Elastic.Markdown/Myst/FrontMatterParser.cs deleted file mode 100644 index 3aa5fb1b9..000000000 --- a/src/Elastic.Markdown/Myst/FrontMatterParser.cs +++ /dev/null @@ -1,195 +0,0 @@ -// 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 Elastic.Markdown.Helpers; -using YamlDotNet.Core; -using YamlDotNet.Core.Events; -using YamlDotNet.Serialization; -using YamlDotNet.Serialization.NamingConventions; - -namespace Elastic.Markdown.Myst; - -[YamlStaticContext] -public partial class YamlFrontMatterStaticContext; - -[YamlSerializable] -public class YamlFrontMatter -{ - [YamlMember(Alias = "title")] - public string? Title { get; set; } - - [YamlMember(Alias = "navigation_title")] - public string? NavigationTitle { get; set; } - - [YamlMember(Alias = "sub")] - public Dictionary? Properties { get; set; } - - - [YamlMember(Alias = "applies")] - public Deployment? AppliesTo { get; set; } -} - -[YamlSerializable] -public class Deployment -{ - [YamlMember(Alias = "self")] - public SelfManagedDeployment? SelfManaged { get; set; } - - [YamlMember(Alias = "cloud")] - public CloudManagedDeployment? Cloud { get; set; } -} - -public class AllVersions() : SemVersion(9999, 9999, 9999) -{ - public static AllVersions Instance { get; } = new (); -} - -[YamlSerializable] -public class SelfManagedDeployment -{ - [YamlMember(Alias = "stack")] - public SemVersion? Stack { get; set; } - - [YamlMember(Alias = "ece")] - public SemVersion? Ece { get; set; } - - [YamlMember(Alias = "eck")] - public SemVersion? Eck { get; set; } - - public static SelfManagedDeployment All { get; } = new() - { - Stack = AllVersions.Instance, - Ece = AllVersions.Instance, - Eck = AllVersions.Instance - }; -} - -[YamlSerializable] -public class CloudManagedDeployment -{ - [YamlMember(Alias = "hosted")] - public SemVersion? Hosted { get; set; } - - [YamlMember(Alias = "serverless")] - public SemVersion? Serverless { get; set; } - - public static CloudManagedDeployment All { get; } = new() - { - Hosted = AllVersions.Instance, - Serverless = AllVersions.Instance - }; - -} - -public static class FrontMatterParser -{ - public static YamlFrontMatter Deserialize(string yaml) - { - var input = new StringReader(yaml); - - var deserializer = new StaticDeserializerBuilder(new YamlFrontMatterStaticContext()) - .IgnoreUnmatchedProperties() - .WithTypeConverter(new SemVersionConverter()) - .WithTypeConverter(new CloudManagedSerializer()) - .WithTypeConverter(new SelfManagedSerializer()) - .Build(); - - var frontMatter = deserializer.Deserialize(input); - return frontMatter; - - } -} - -public class SemVersionConverter : IYamlTypeConverter -{ - public bool Accepts(Type type) => type == typeof(SemVersion); - - public object? ReadYaml(IParser parser, Type type, ObjectDeserializer rootDeserializer) - { - var value = parser.Consume(); - if (string.IsNullOrWhiteSpace(value.Value)) - return AllVersions.Instance; - if (string.Equals(value.Value, "all", StringComparison.InvariantCultureIgnoreCase)) - return AllVersions.Instance; - return (SemVersion)value.Value; - } - - public void WriteYaml(IEmitter emitter, object? value, Type type, ObjectSerializer serializer) - { - if (value == null) - return; - emitter.Emit(new Scalar(value.ToString()!)); - } - - public static SemVersion? Parse(string? value) => - value?.Trim().ToLowerInvariant() switch - { - null => AllVersions.Instance, - "all" => AllVersions.Instance, - "" => AllVersions.Instance, - _ => SemVersion.TryParse(value, out var v) ? v : null - }; -} - -public class CloudManagedSerializer : IYamlTypeConverter -{ - public bool Accepts(Type type) => type == typeof(CloudManagedDeployment); - - public object? ReadYaml(IParser parser, Type type, ObjectDeserializer rootDeserializer) - { - if (parser.TryConsume(out var value)) - { - if (string.IsNullOrWhiteSpace(value.Value)) - return CloudManagedDeployment.All; - if (string.Equals(value.Value, "all", StringComparison.InvariantCultureIgnoreCase)) - return CloudManagedDeployment.All; - } - var x = rootDeserializer.Invoke(typeof(Dictionary)); - if (x is not Dictionary { Count: > 0 } dictionary) - return null; - - var cloudManaged = new CloudManagedDeployment(); - if (dictionary.TryGetValue("hosted", out var v)) - cloudManaged.Hosted = SemVersionConverter.Parse(v); - if (dictionary.TryGetValue("serverless", out v)) - cloudManaged.Serverless = SemVersionConverter.Parse(v); - return cloudManaged; - - } - - public void WriteYaml(IEmitter emitter, object? value, Type type, ObjectSerializer serializer) => - serializer.Invoke(value, type); -} - -public class SelfManagedSerializer : IYamlTypeConverter -{ - public bool Accepts(Type type) => type == typeof(SelfManagedDeployment); - - public object? ReadYaml(IParser parser, Type type, ObjectDeserializer rootDeserializer) - { - if (parser.TryConsume(out var value)) - { - if (string.IsNullOrWhiteSpace(value.Value)) - return SelfManagedDeployment.All; - if (string.Equals(value.Value, "all", StringComparison.InvariantCultureIgnoreCase)) - return SelfManagedDeployment.All; - } - var x = rootDeserializer.Invoke(typeof(Dictionary)); - if (x is not Dictionary { Count: > 0 } dictionary) - return null; - - var deployment = new SelfManagedDeployment(); - if (dictionary.TryGetValue("stack", out var v)) - deployment.Stack = SemVersionConverter.Parse(v); - if (dictionary.TryGetValue("ece", out v)) - deployment.Ece = SemVersionConverter.Parse(v); - if (dictionary.TryGetValue("eck", out v)) - deployment.Eck = SemVersionConverter.Parse(v); - return deployment; - - } - - public void WriteYaml(IEmitter emitter, object? value, Type type, ObjectSerializer serializer) => - serializer.Invoke(value, type); -} diff --git a/src/Elastic.Markdown/Myst/MarkdownParser.cs b/src/Elastic.Markdown/Myst/MarkdownParser.cs index 925af0d13..9fccbc7c8 100644 --- a/src/Elastic.Markdown/Myst/MarkdownParser.cs +++ b/src/Elastic.Markdown/Myst/MarkdownParser.cs @@ -7,6 +7,7 @@ using Elastic.Markdown.IO; using Elastic.Markdown.Myst.Comments; using Elastic.Markdown.Myst.Directives; +using Elastic.Markdown.Myst.FrontMatter; using Elastic.Markdown.Myst.InlineParsers; using Elastic.Markdown.Myst.Substitution; using Markdig; diff --git a/src/Elastic.Markdown/Myst/ParserContext.cs b/src/Elastic.Markdown/Myst/ParserContext.cs index d4a0067e0..9f07cabfb 100644 --- a/src/Elastic.Markdown/Myst/ParserContext.cs +++ b/src/Elastic.Markdown/Myst/ParserContext.cs @@ -4,6 +4,7 @@ using System.IO.Abstractions; using Elastic.Markdown.IO; +using Elastic.Markdown.Myst.FrontMatter; using Markdig; using Markdig.Parsers; diff --git a/tests/Elastic.Markdown.Tests/FrontMatter/ProductConstraintTests.cs b/tests/Elastic.Markdown.Tests/FrontMatter/ProductConstraintTests.cs index 9ece827d2..63bbc2a3c 100644 --- a/tests/Elastic.Markdown.Tests/FrontMatter/ProductConstraintTests.cs +++ b/tests/Elastic.Markdown.Tests/FrontMatter/ProductConstraintTests.cs @@ -2,11 +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.Markdown.Helpers; -using Elastic.Markdown.Myst; +using Elastic.Markdown.Myst.FrontMatter; using Elastic.Markdown.Tests.Directives; using FluentAssertions; using Xunit.Abstractions; +using static Elastic.Markdown.Myst.FrontMatter.ProductLifecycle; namespace Elastic.Markdown.Tests.FrontMatter; @@ -16,47 +16,83 @@ public class ProductConstraintTests(ITestOutputHelper output) : DirectiveTest(ou title: Elastic Docs v3 navigation_title: "Documentation Guide" applies: - self: - stack: 7.7 - cloud: - serverless: 1.0.0 + stack: ga 8.1 + serverless: tech-preview + hosted: beta 8.1.1 + eck: beta 3.0.2 + ece: unavailable --- """ ) { [Fact] - public void ReadsTitle() => File.Title.Should().Be("Elastic Docs v3"); - - [Fact] - public void ReadsNavigationTitle() => File.NavigationTitle.Should().Be("Documentation Guide"); + public void Assert() + { + File.YamlFrontMatter.Should().NotBeNull(); + var appliesTo = File.YamlFrontMatter!.AppliesTo; + appliesTo.Should().NotBeNull(); + appliesTo!.Cloud.Should().NotBeNull(); + appliesTo.Cloud!.Serverless.Should().BeEquivalentTo(new ProductAvailability { Lifecycle = TechnicalPreview }); + appliesTo.Cloud!.Hosted.Should().BeEquivalentTo(new ProductAvailability { Lifecycle = Beta, Version = new (8,1,1)}); + appliesTo.SelfManaged.Should().NotBeNull(); + appliesTo.SelfManaged!.Eck.Should().BeEquivalentTo(new ProductAvailability { Lifecycle = Beta, Version = new (3,0,2)}); + appliesTo.SelfManaged!.Ece.Should().BeEquivalentTo(new ProductAvailability { Lifecycle = Unavailable }); + appliesTo.SelfManaged!.Stack.Should().BeEquivalentTo(new ProductAvailability { Lifecycle = GenerallyAvailable, Version = new (8,1,0)}); + } +} +public abstract class ParsingTests(ITestOutputHelper output, string moniker, ProductAvailability? expected) + : DirectiveTest(output, +$""" +--- +title: Elastic Docs v3 +navigation_title: "Documentation Guide" +applies: + serverless: {moniker} +--- +""" +) +{ [Fact] - public void ReadsSubstitutions() + public void Assert() { File.YamlFrontMatter.Should().NotBeNull(); var appliesTo = File.YamlFrontMatter!.AppliesTo; appliesTo.Should().NotBeNull(); - appliesTo!.SelfManaged.Should().NotBeNull(); - appliesTo.Cloud.Should().NotBeNull(); - appliesTo.Cloud!.Serverless.Should().BeEquivalentTo(new SemVersion(1,0,0)); + appliesTo!.Cloud.Should().NotBeNull(); + appliesTo.Cloud!.Serverless.Should().BeEquivalentTo(expected); } } +public class ParsesGa(ITestOutputHelper output) : ParsingTests(output, "ga", new () { Lifecycle = GenerallyAvailable }) ; +public class ParsesDev(ITestOutputHelper output) : ParsingTests(output, "dev", new () { Lifecycle = Development }) ; +public class ParsesDevelopment(ITestOutputHelper output) : ParsingTests(output, "development", new () { Lifecycle = Development }) ; +public class ParsesBeta(ITestOutputHelper output) : ParsingTests(output, "beta", new () { Lifecycle = Beta }) ; +public class ParsesComing(ITestOutputHelper output) : ParsingTests(output, "coming", new () { Lifecycle = Coming }) ; +public class ParsesDeprecated(ITestOutputHelper output) : ParsingTests(output, "deprecated", new () { Lifecycle = Deprecated }) ; +public class ParsesDiscontinued(ITestOutputHelper output) : ParsingTests(output, "discontinued", new () { Lifecycle = Discontinued }) ; +public class ParsesUnavailable(ITestOutputHelper output) : ParsingTests(output, "unavailable", new () { Lifecycle = Unavailable }) ; +public class ParsesTechnicalPreview(ITestOutputHelper output) : ParsingTests(output, "tech-preview", new () { Lifecycle = TechnicalPreview }) ; +public class ParsesPreview(ITestOutputHelper output) : ParsingTests(output, "preview", new () { Lifecycle = TechnicalPreview }) ; +public class ParsesEmpty(ITestOutputHelper output) : ParsingTests(output, "", ProductAvailability.GenerallyAvailable) ; +public class ParsesAll(ITestOutputHelper output) : ParsingTests(output, "all", ProductAvailability.GenerallyAvailable) ; +public class ParsesWithVersion(ITestOutputHelper output) : ParsingTests(output, "ga 7.7.0", new () { Lifecycle = GenerallyAvailable, Version = new (7,7,0) }); +public class ParsesWithAllVersion(ITestOutputHelper output) : ParsingTests(output, "ga all", new () { Lifecycle = GenerallyAvailable, Version = AllVersions.Instance }); + public class CanSpecifyAllForProductVersions(ITestOutputHelper output) : DirectiveTest(output, """ --- title: Elastic Docs v3 navigation_title: "Documentation Guide" applies: - self: - stack: all + stack: all --- """ ) { [Fact] public void Assert() => - File.YamlFrontMatter!.AppliesTo!.SelfManaged!.Stack.Should().BeEquivalentTo(AllVersions.Instance); + File.YamlFrontMatter!.AppliesTo!.SelfManaged!.Stack.Should().BeEquivalentTo(ProductAvailability.GenerallyAvailable); } public class EmptyProductVersionMeansAll(ITestOutputHelper output) : DirectiveTest(output, @@ -65,15 +101,14 @@ public class EmptyProductVersionMeansAll(ITestOutputHelper output) : DirectiveTe title: Elastic Docs v3 navigation_title: "Documentation Guide" applies: - self: - stack: + stack: --- """ ) { [Fact] public void Assert() => - File.YamlFrontMatter!.AppliesTo!.SelfManaged!.Stack.Should().BeEquivalentTo(AllVersions.Instance); + File.YamlFrontMatter!.AppliesTo!.SelfManaged!.Stack.Should().BeEquivalentTo(ProductAvailability.GenerallyAvailable); } public class EmptyCloudSetsAllProductsToAll(ITestOutputHelper output) : DirectiveTest(output, @@ -82,12 +117,12 @@ public class EmptyCloudSetsAllProductsToAll(ITestOutputHelper output) : Directiv title: Elastic Docs v3 navigation_title: "Documentation Guide" applies: - cloud: + hosted: --- """ ) { [Fact] public void Assert() => - File.YamlFrontMatter!.AppliesTo!.Cloud!.Hosted.Should().BeEquivalentTo(AllVersions.Instance); + File.YamlFrontMatter!.AppliesTo!.Cloud!.Hosted.Should().BeEquivalentTo(ProductAvailability.GenerallyAvailable); } From c1d3bc3c94ebc277583362d8d703a19033884a7c Mon Sep 17 00:00:00 2001 From: Martijn Laarman Date: Fri, 13 Dec 2024 12:47:20 +0100 Subject: [PATCH 5/6] add missing new files --- .../Myst/FrontMatter/AllVersions.cs | 50 ++++++++ .../Myst/FrontMatter/Deployment.cs | 120 ++++++++++++++++++ .../Myst/FrontMatter/ProductAvailability.cs | 61 +++++++++ .../Myst/FrontMatter/ProductLifecycle.cs | 36 ++++++ 4 files changed, 267 insertions(+) create mode 100644 src/Elastic.Markdown/Myst/FrontMatter/AllVersions.cs create mode 100644 src/Elastic.Markdown/Myst/FrontMatter/Deployment.cs create mode 100644 src/Elastic.Markdown/Myst/FrontMatter/ProductAvailability.cs create mode 100644 src/Elastic.Markdown/Myst/FrontMatter/ProductLifecycle.cs diff --git a/src/Elastic.Markdown/Myst/FrontMatter/AllVersions.cs b/src/Elastic.Markdown/Myst/FrontMatter/AllVersions.cs new file mode 100644 index 000000000..c61a59344 --- /dev/null +++ b/src/Elastic.Markdown/Myst/FrontMatter/AllVersions.cs @@ -0,0 +1,50 @@ +// 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 Elastic.Markdown.Helpers; +using YamlDotNet.Core; +using YamlDotNet.Core.Events; +using YamlDotNet.Serialization; + +namespace Elastic.Markdown.Myst.FrontMatter; + +public class AllVersions() : SemVersion(9999, 9999, 9999) +{ + public static AllVersions Instance { get; } = new (); +} + +public class SemVersionConverter : IYamlTypeConverter +{ + public bool Accepts(Type type) => type == typeof(SemVersion); + + public object? ReadYaml(IParser parser, Type type, ObjectDeserializer rootDeserializer) + { + var value = parser.Consume(); + if (string.IsNullOrWhiteSpace(value.Value)) + return AllVersions.Instance; + if (string.Equals(value.Value.Trim(), "all", StringComparison.InvariantCultureIgnoreCase)) + return AllVersions.Instance; + return (SemVersion)value.Value; + } + + public void WriteYaml(IEmitter emitter, object? value, Type type, ObjectSerializer serializer) + { + if (value == null) + return; + emitter.Emit(new Scalar(value.ToString()!)); + } + + public static bool TryParse(string? value, out SemVersion? version) + { + version = value?.Trim().ToLowerInvariant() switch + { + null => AllVersions.Instance, + "all" => AllVersions.Instance, + "" => AllVersions.Instance, + _ => SemVersion.TryParse(value, out var v) ? v : SemVersion.TryParse(value + ".0", out v) ? v : null + }; + return version is not null; + } +} + diff --git a/src/Elastic.Markdown/Myst/FrontMatter/Deployment.cs b/src/Elastic.Markdown/Myst/FrontMatter/Deployment.cs new file mode 100644 index 000000000..e4916666a --- /dev/null +++ b/src/Elastic.Markdown/Myst/FrontMatter/Deployment.cs @@ -0,0 +1,120 @@ +// 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 YamlDotNet.Core; +using YamlDotNet.Core.Events; +using YamlDotNet.Serialization; + +namespace Elastic.Markdown.Myst.FrontMatter; + +[YamlSerializable] +public record Deployment +{ + [YamlMember(Alias = "self")] + public SelfManagedDeployment? SelfManaged { get; set; } + + [YamlMember(Alias = "cloud")] + public CloudManagedDeployment? Cloud { get; set; } + + public static Deployment All { get; } = new() + { + Cloud = CloudManagedDeployment.All, + SelfManaged = SelfManagedDeployment.All + }; +} + +[YamlSerializable] +public record SelfManagedDeployment +{ + [YamlMember(Alias = "stack")] + public ProductAvailability? Stack { get; set; } + + [YamlMember(Alias = "ece")] + public ProductAvailability? Ece { get; set; } + + [YamlMember(Alias = "eck")] + public ProductAvailability? Eck { get; set; } + + public static SelfManagedDeployment All { get; } = new() + { + Stack = ProductAvailability.GenerallyAvailable, + Ece = ProductAvailability.GenerallyAvailable, + Eck = ProductAvailability.GenerallyAvailable + }; +} + +[YamlSerializable] +public record CloudManagedDeployment +{ + [YamlMember(Alias = "hosted")] + public ProductAvailability? Hosted { get; set; } + + [YamlMember(Alias = "serverless")] + public ProductAvailability? Serverless { get; set; } + + public static CloudManagedDeployment All { get; } = new() + { + Hosted = ProductAvailability.GenerallyAvailable, + Serverless = ProductAvailability.GenerallyAvailable + }; + +} + +public class DeploymentConverter : IYamlTypeConverter +{ + public bool Accepts(Type type) => type == typeof(Deployment); + + public object? ReadYaml(IParser parser, Type type, ObjectDeserializer rootDeserializer) + { + if (parser.TryConsume(out var value)) + { + if (string.IsNullOrWhiteSpace(value.Value)) + return Deployment.All; + if (string.Equals(value.Value, "all", StringComparison.InvariantCultureIgnoreCase)) + return Deployment.All; + } + var x = rootDeserializer.Invoke(typeof(Dictionary)); + if (x is not Dictionary { Count: > 0 } dictionary) + return null; + + var deployment = new Deployment(); + + if (TryGetVersion("stack", out var version)) + { + deployment.SelfManaged ??= new SelfManagedDeployment(); + deployment.SelfManaged.Stack = version; + } + if (TryGetVersion("ece", out version)) + { + deployment.SelfManaged ??= new SelfManagedDeployment(); + deployment.SelfManaged.Ece = version; + } + if (TryGetVersion("eck", out version)) + { + deployment.SelfManaged ??= new SelfManagedDeployment(); + deployment.SelfManaged.Eck = version; + } + if (TryGetVersion("hosted", out version)) + { + deployment.Cloud ??= new CloudManagedDeployment(); + deployment.Cloud.Hosted = version; + } + if (TryGetVersion("serverless", out version)) + { + deployment.Cloud ??= new CloudManagedDeployment(); + deployment.Cloud.Serverless = version; + } + return deployment; + + bool TryGetVersion(string key, out ProductAvailability? semVersion) + { + semVersion = null; + return dictionary.TryGetValue(key, out var v) && ProductAvailability.TryParse(v, out semVersion); + } + + } + + public void WriteYaml(IEmitter emitter, object? value, Type type, ObjectSerializer serializer) => + serializer.Invoke(value, type); +} diff --git a/src/Elastic.Markdown/Myst/FrontMatter/ProductAvailability.cs b/src/Elastic.Markdown/Myst/FrontMatter/ProductAvailability.cs new file mode 100644 index 000000000..88336bc20 --- /dev/null +++ b/src/Elastic.Markdown/Myst/FrontMatter/ProductAvailability.cs @@ -0,0 +1,61 @@ +// 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 Elastic.Markdown.Helpers; +using YamlDotNet.Serialization; + +namespace Elastic.Markdown.Myst.FrontMatter; + +[YamlSerializable] +public record ProductAvailability +{ + public ProductLifecycle Lifecycle { get; init; } + public SemVersion? Version { get; init; } + + public static ProductAvailability GenerallyAvailable { get; } = new() + { + Lifecycle = ProductLifecycle.GenerallyAvailable, Version = AllVersions.Instance + }; + + // [version] + public static bool TryParse(string? value, out ProductAvailability? availability) + { + if (string.IsNullOrWhiteSpace(value) || string.Equals(value.Trim(), "all", StringComparison.InvariantCultureIgnoreCase)) + { + availability = GenerallyAvailable; + return true; + } + + var tokens = value.Split(" ", StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries); + if (tokens.Length < 1) + { + availability = null; + return false; + } + var lifecycle = tokens[0].ToLowerInvariant() switch + { + "preview" => ProductLifecycle.TechnicalPreview, + "tech-preview" => ProductLifecycle.TechnicalPreview, + "beta" => ProductLifecycle.Beta, + "dev" => ProductLifecycle.Development, + "development" => ProductLifecycle.Development, + "deprecated" => ProductLifecycle.Deprecated, + "coming" => ProductLifecycle.Coming, + "discontinued" => ProductLifecycle.Discontinued, + "unavailable" => ProductLifecycle.Unavailable, + "ga" => ProductLifecycle.GenerallyAvailable, + _ => throw new ArgumentOutOfRangeException(nameof(tokens), tokens, $"Unknown product lifecycle: {tokens[0]}") + }; + + var version = tokens.Length < 2 ? null : tokens[1] switch + { + null => AllVersions.Instance, + "all" => AllVersions.Instance, + "" => AllVersions.Instance, + var t => SemVersionConverter.TryParse(t, out var v) ? v : null + }; + availability = new ProductAvailability { Version = version, Lifecycle = lifecycle }; + return true; + } +} diff --git a/src/Elastic.Markdown/Myst/FrontMatter/ProductLifecycle.cs b/src/Elastic.Markdown/Myst/FrontMatter/ProductLifecycle.cs new file mode 100644 index 000000000..8ce224fd5 --- /dev/null +++ b/src/Elastic.Markdown/Myst/FrontMatter/ProductLifecycle.cs @@ -0,0 +1,36 @@ +// 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 YamlDotNet.Serialization; + +namespace Elastic.Markdown.Myst.FrontMatter; + +[YamlSerializable] +public enum ProductLifecycle +{ + // technical preview (exists in current docs system per https://github.com/elastic/docs?tab=readme-ov-file#beta-dev-and-preview-experimental) + [YamlMember(Alias = "preview")] + TechnicalPreview, + // beta (ditto) + [YamlMember(Alias = "beta")] + Beta, + // dev (ditto, though it's uncertain whether it's ever used or still needed) + [YamlMember(Alias = "development")] + Development, + // deprecated (exists in current docs system per https://github.com/elastic/docs?tab=readme-ov-file#additions-and-deprecations) + [YamlMember(Alias = "deprecated")] + Deprecated, + // coming (ditto) + [YamlMember(Alias = "coming")] + Coming, + // discontinued (historically we've immediately removed content when the feature ceases to be supported, but this might not be the case with pages that contain information that spans versions) + [YamlMember(Alias = "discontinued")] + Discontinued, + // unavailable (for content that doesn't exist in a specific context and is never coming or not coming anytime soon) + [YamlMember(Alias = "unavailable")] + Unavailable, + // ga (replaces "added" in the current docs system since it was not entirely clear how/if that overlapped with beta/preview states) + [YamlMember(Alias = "ga")] + GenerallyAvailable +} From 988039f1368c6e021a9ec2f5a09af7193b1f2864 Mon Sep 17 00:00:00 2001 From: Martijn Laarman Date: Fri, 13 Dec 2024 15:09:12 +0100 Subject: [PATCH 6/6] first stab at html for applies to yaml frontmatter --- docs/source/markup/applies.md | 48 ++++++++++ .../Slices/Directives/Applies.cshtml | 96 +++++++++++++++++++ src/Elastic.Markdown/Slices/HtmlWriter.cs | 3 +- src/Elastic.Markdown/Slices/Index.cshtml | 4 + src/Elastic.Markdown/Slices/_ViewModels.cs | 2 + src/Elastic.Markdown/_static/custom.css | 33 +++++++ 6 files changed, 185 insertions(+), 1 deletion(-) create mode 100644 docs/source/markup/applies.md create mode 100644 src/Elastic.Markdown/Slices/Directives/Applies.cshtml diff --git a/docs/source/markup/applies.md b/docs/source/markup/applies.md new file mode 100644 index 000000000..e8818e2ab --- /dev/null +++ b/docs/source/markup/applies.md @@ -0,0 +1,48 @@ +--- +title: Product Availability +applies: + stack: ga 8.1 + serverless: tech-preview + hosted: beta 8.1.1 + eck: beta 3.0.2 + ece: unavailable +--- + + +Using yaml frontmatter pages can explicitly indicate to each deployment targets availability and lifecycle status + + +```yaml +applies: + stack: ga 8.1 + serverless: tech-preview + hosted: beta 8.1.1 + eck: beta 3.0.2 + ece: unavailable +``` + +Its syntax is + +``` + : [version] +``` + +Where version is optional. + +`all` and empty string mean generally available for all active versions + +```yaml +applies: + stack: + serverless: all +``` + +`all` and empty string can also be specified at a version level + +```yaml +applies: + stack: beta all + serverless: beta +``` + +Are equivalent, note `all` just means we won't be rendering the version portion in the html. \ No newline at end of file diff --git a/src/Elastic.Markdown/Slices/Directives/Applies.cshtml b/src/Elastic.Markdown/Slices/Directives/Applies.cshtml new file mode 100644 index 000000000..f34525eb8 --- /dev/null +++ b/src/Elastic.Markdown/Slices/Directives/Applies.cshtml @@ -0,0 +1,96 @@ +@using Elastic.Markdown.Myst.FrontMatter +@inherits RazorSlice +

+ + Applies To: + + @if (Model.SelfManaged is not null) + { + if (Model.SelfManaged.Stack is not null) + { + @RenderProduct("Elastic Stack", Model.SelfManaged.Stack) + } + if (Model.SelfManaged.Ece is not null) + { + @RenderProduct("Elastic Cloud Enterprise", Model.SelfManaged.Ece) + } + if (Model.SelfManaged.Eck is not null) + { + @RenderProduct("Elastic Cloud Kubernetes", Model.SelfManaged.Eck) + } + } + @if (Model.Cloud is not null) + { + if (Model.Cloud.Hosted is not null) + { + @RenderProduct("Elastic Cloud Hosted", Model.Cloud.Hosted) + } + if (Model.Cloud.Serverless is not null) + { + @RenderProduct("Serverless", Model.Cloud.Serverless) + } + } +

+ +@functions { + + private string GetLifeCycleClass(ProductLifecycle cycle) + { + switch (cycle) + { + case ProductLifecycle.Deprecated: + case ProductLifecycle.Coming: + case ProductLifecycle.Discontinued: + case ProductLifecycle.Unavailable: + return "muted"; + case ProductLifecycle.GenerallyAvailable: + case ProductLifecycle.TechnicalPreview: + case ProductLifecycle.Beta: + case ProductLifecycle.Development: + return "primary"; + default: + throw new ArgumentOutOfRangeException(nameof(cycle), cycle, null); + } + } + private string GetLifeCycleName(ProductLifecycle cycle) + { + switch (cycle) + { + case ProductLifecycle.TechnicalPreview: + return "Technical Preview"; + case ProductLifecycle.Beta: + return "Beta"; + case ProductLifecycle.Development: + return "Development"; + case ProductLifecycle.Deprecated: + return "Deprecated"; + case ProductLifecycle.Coming: + return "Coming"; + case ProductLifecycle.Discontinued: + return "Discontinued"; + case ProductLifecycle.Unavailable: + return "Unavailable"; + case ProductLifecycle.GenerallyAvailable: + return "GA"; + default: + throw new ArgumentOutOfRangeException(nameof(cycle), cycle, null); + } + } + + private IHtmlContent RenderProduct(string name, ProductAvailability product) + { + var c = GetLifeCycleClass(product.Lifecycle); + + @name + @if (product.Lifecycle != ProductLifecycle.GenerallyAvailable) + { + @GetLifeCycleName(product.Lifecycle) + } + @if (product.Version is not null and not AllVersions) + { + (@product.Version) + } + + return HtmlString.Empty; + } +} \ No newline at end of file diff --git a/src/Elastic.Markdown/Slices/HtmlWriter.cs b/src/Elastic.Markdown/Slices/HtmlWriter.cs index 9706dd4a5..812fbe0d8 100644 --- a/src/Elastic.Markdown/Slices/HtmlWriter.cs +++ b/src/Elastic.Markdown/Slices/HtmlWriter.cs @@ -52,7 +52,8 @@ public async Task RenderLayout(MarkdownFile markdown, Cancel ctx = defau Tree = DocumentationSet.Tree, CurrentDocument = markdown, NavigationHtml = navigationHtml, - UrlPathPrefix = markdown.UrlPathPrefix + UrlPathPrefix = markdown.UrlPathPrefix, + Applies = markdown.YamlFrontMatter?.AppliesTo }); return await slice.RenderAsync(cancellationToken: ctx); } diff --git a/src/Elastic.Markdown/Slices/Index.cshtml b/src/Elastic.Markdown/Slices/Index.cshtml index f136c1d5c..c818ba8ed 100644 --- a/src/Elastic.Markdown/Slices/Index.cshtml +++ b/src/Elastic.Markdown/Slices/Index.cshtml @@ -13,5 +13,9 @@ }

@(Model.Title)

+ @if (Model.Applies is not null) + { + await RenderPartialAsync(Applies.Create(Model.Applies)); + } @(new HtmlString(Model.MarkdownHtml))
diff --git a/src/Elastic.Markdown/Slices/_ViewModels.cs b/src/Elastic.Markdown/Slices/_ViewModels.cs index 59fb58bb3..684396eac 100644 --- a/src/Elastic.Markdown/Slices/_ViewModels.cs +++ b/src/Elastic.Markdown/Slices/_ViewModels.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.IO; +using Elastic.Markdown.Myst.FrontMatter; namespace Elastic.Markdown.Slices; @@ -14,6 +15,7 @@ public class IndexViewModel public required MarkdownFile CurrentDocument { get; init; } public required string NavigationHtml { get; init; } public required string? UrlPathPrefix { get; init; } + public required Deployment? Applies { get; init; } } public class LayoutViewModel diff --git a/src/Elastic.Markdown/_static/custom.css b/src/Elastic.Markdown/_static/custom.css index 34884a893..2f2935485 100644 --- a/src/Elastic.Markdown/_static/custom.css +++ b/src/Elastic.Markdown/_static/custom.css @@ -30,4 +30,37 @@ body > footer > div > div > div.sy-foot-copyright > p:nth-child(2){ .yue img { margin-top: 0; margin-bottom: 0; +} + +.applies-badge { + font-size: 1em; + margin-top: 0.4em; +} + +h1 { + padding-bottom: 0.4em; + border-bottom: 1px solid #dfdfdf; +} +.sd-outline-muted +{ + border: 1px solid #dfdfdf; +} +.product-availability { + padding-bottom: 0.8em; + border-bottom: 1px solid #dfdfdf; +} + +h1:has(+ .product-availability) { + margin-bottom: 0.4em; + padding-bottom: 0; + border-bottom: none; +} + +.applies-to-label { + font-size: 1em; + margin-top: 0.4em; + margin-left: 0; + font-weight: bold; + text-align: left; + padding-left: 0; } \ No newline at end of file