diff --git a/docs/source/markup/applies.md b/docs/source/markup/applies.md index e8818e2..fece0b0 100644 --- a/docs/source/markup/applies.md +++ b/docs/source/markup/applies.md @@ -45,4 +45,16 @@ applies: 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 +Are equivalent, note `all` just means we won't be rendering the version portion in the html. + + +## This section has its own applies annotations +```{applies} +:stack: unavailable +:serverless: tech-preview +``` + +This section describes a feature that's unavailable in `stack` and in tech preview on `serverless` + + +the `{applies}` directive **MUST** be preceded by a heading. \ No newline at end of file diff --git a/src/Elastic.Markdown/Myst/Directives/AppliesBlock.cs b/src/Elastic.Markdown/Myst/Directives/AppliesBlock.cs new file mode 100644 index 0000000..f475034 --- /dev/null +++ b/src/Elastic.Markdown/Myst/Directives/AppliesBlock.cs @@ -0,0 +1,68 @@ +// 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.Myst.FrontMatter; +using Markdig.Syntax; + +namespace Elastic.Markdown.Myst.Directives; + +public class AppliesBlock(DirectiveBlockParser parser, Dictionary properties) + : DirectiveBlock(parser, properties) +{ + public override string Directive => "mermaid"; + + public Deployment? Deployment { get; private set; } + + public override void FinalizeAndValidate(ParserContext context) + { + if (TryGetAvailability("stack", out var version)) + { + Deployment ??= new Deployment(); + Deployment.SelfManaged ??= new SelfManagedDeployment(); + Deployment.SelfManaged.Stack = version; + } + if (TryGetAvailability("ece", out version)) + { + Deployment ??= new Deployment(); + Deployment.SelfManaged ??= new SelfManagedDeployment(); + Deployment.SelfManaged.Ece = version; + } + if (TryGetAvailability("eck", out version)) + { + Deployment ??= new Deployment(); + Deployment.SelfManaged ??= new SelfManagedDeployment(); + Deployment.SelfManaged.Eck = version; + } + if (TryGetAvailability("hosted", out version)) + { + Deployment ??= new Deployment(); + Deployment.Cloud ??= new CloudManagedDeployment(); + Deployment.Cloud.Hosted = version; + } + if (TryGetAvailability("serverless", out version)) + { + Deployment ??= new Deployment(); + Deployment.Cloud ??= new CloudManagedDeployment(); + Deployment.Cloud.Serverless = version; + } + + if (Deployment is null) + EmitError(context, "{applies} block with no product availability specified"); + + var index = Parent?.IndexOf(this); + if (Parent is not null && index > 0) + { + var i = index - 1 ?? 0; + var prevSib = Parent[i]; + if (prevSib is not HeadingBlock) + EmitError(context, "{applies} should follow a heading"); + } + + bool TryGetAvailability(string key, out ProductAvailability? semVersion) + { + semVersion = null; + return Prop(key) is {} v && ProductAvailability.TryParse(v, out semVersion); + } + } +} diff --git a/src/Elastic.Markdown/Myst/Directives/DirectiveBlockParser.cs b/src/Elastic.Markdown/Myst/Directives/DirectiveBlockParser.cs index 7406157..ee0e393 100644 --- a/src/Elastic.Markdown/Myst/Directives/DirectiveBlockParser.cs +++ b/src/Elastic.Markdown/Myst/Directives/DirectiveBlockParser.cs @@ -111,6 +111,9 @@ protected override DirectiveBlock CreateFencedBlock(BlockProcessor processor) if (info.IndexOf("{literalinclude}") > 0) return new LiteralIncludeBlock(this, _admonitionData, context); + if (info.IndexOf("{applies}") > 0) + return new AppliesBlock(this, _admonitionData); + foreach (var admonition in _admonitions) { if (info.IndexOf($"{{{admonition}}}") > 0) diff --git a/src/Elastic.Markdown/Myst/Directives/DirectiveHtmlRenderer.cs b/src/Elastic.Markdown/Myst/Directives/DirectiveHtmlRenderer.cs index e851982..9a815a2 100644 --- a/src/Elastic.Markdown/Myst/Directives/DirectiveHtmlRenderer.cs +++ b/src/Elastic.Markdown/Myst/Directives/DirectiveHtmlRenderer.cs @@ -5,6 +5,7 @@ // This file is licensed under the BSD-Clause 2 license. // See the license.txt file in the project root for more information. +using Elastic.Markdown.Myst.FrontMatter; using Elastic.Markdown.Myst.Substitution; using Elastic.Markdown.Slices; using Elastic.Markdown.Slices.Directives; @@ -34,6 +35,9 @@ protected override void Write(HtmlRenderer renderer, DirectiveBlock directiveBlo case MermaidBlock mermaidBlock: WriteMermaid(renderer, mermaidBlock); return; + case AppliesBlock appliesBlock: + WriteApplies(renderer, appliesBlock); + return; case FigureBlock imageBlock: WriteFigure(renderer, imageBlock); return; @@ -179,6 +183,15 @@ private void WriteMermaid(HtmlRenderer renderer, MermaidBlock block) RenderRazorSliceRawContent(slice, renderer, block); } + private void WriteApplies(HtmlRenderer renderer, AppliesBlock block) + { + if (block.Deployment is null || block.Deployment == Deployment.All) + return; + + var slice = Applies.Create(block.Deployment); + RenderRazorSliceNoContent(slice, renderer); + } + private void WriteTabItem(HtmlRenderer renderer, TabItemBlock block) { var slice = TabItem.Create(new TabItemViewModel @@ -240,6 +253,12 @@ private static void RenderRazorSlice(RazorSlice slice, HtmlRenderer render renderer.Write(blocks[1]); } + private static void RenderRazorSliceNoContent(RazorSlice slice, HtmlRenderer renderer) + { + var html = slice.RenderAsync().GetAwaiter().GetResult(); + renderer.Write(html); + } + private static void RenderRazorSliceRawContent(RazorSlice slice, HtmlRenderer renderer, DirectiveBlock obj) { var html = slice.RenderAsync().GetAwaiter().GetResult(); diff --git a/src/Elastic.Markdown/Myst/FrontMatter/Deployment.cs b/src/Elastic.Markdown/Myst/FrontMatter/Deployment.cs index e491666..511c13f 100644 --- a/src/Elastic.Markdown/Myst/FrontMatter/Deployment.cs +++ b/src/Elastic.Markdown/Myst/FrontMatter/Deployment.cs @@ -79,35 +79,34 @@ public class DeploymentConverter : IYamlTypeConverter return null; var deployment = new Deployment(); - - if (TryGetVersion("stack", out var version)) + if (TryGetAvailability("stack", out var version)) { deployment.SelfManaged ??= new SelfManagedDeployment(); deployment.SelfManaged.Stack = version; } - if (TryGetVersion("ece", out version)) + if (TryGetAvailability("ece", out version)) { deployment.SelfManaged ??= new SelfManagedDeployment(); deployment.SelfManaged.Ece = version; } - if (TryGetVersion("eck", out version)) + if (TryGetAvailability("eck", out version)) { deployment.SelfManaged ??= new SelfManagedDeployment(); deployment.SelfManaged.Eck = version; } - if (TryGetVersion("hosted", out version)) + if (TryGetAvailability("hosted", out version)) { deployment.Cloud ??= new CloudManagedDeployment(); deployment.Cloud.Hosted = version; } - if (TryGetVersion("serverless", out version)) + if (TryGetAvailability("serverless", out version)) { deployment.Cloud ??= new CloudManagedDeployment(); deployment.Cloud.Serverless = version; } return deployment; - bool TryGetVersion(string key, out ProductAvailability? semVersion) + bool TryGetAvailability(string key, out ProductAvailability? semVersion) { semVersion = null; return dictionary.TryGetValue(key, out var v) && ProductAvailability.TryParse(v, out semVersion); diff --git a/src/Elastic.Markdown/_static/custom.css b/src/Elastic.Markdown/_static/custom.css index 2f29354..8658ac8 100644 --- a/src/Elastic.Markdown/_static/custom.css +++ b/src/Elastic.Markdown/_static/custom.css @@ -47,6 +47,9 @@ h1 { } .product-availability { padding-bottom: 0.8em; +} + +h1 + .product-availability { border-bottom: 1px solid #dfdfdf; } @@ -56,6 +59,12 @@ h1:has(+ .product-availability) { border-bottom: none; } +section:has(+ .product-availability) h2 { + margin-bottom: 0.0em; + padding-bottom: 0; + border-bottom: none; +} + .applies-to-label { font-size: 1em; margin-top: 0.4em; diff --git a/tests/Elastic.Markdown.Tests/Directives/AppliesBlockTests.cs b/tests/Elastic.Markdown.Tests/Directives/AppliesBlockTests.cs new file mode 100644 index 0000000..ba56b69 --- /dev/null +++ b/tests/Elastic.Markdown.Tests/Directives/AppliesBlockTests.cs @@ -0,0 +1,78 @@ +// 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.Diagnostics; +using Elastic.Markdown.Myst.Directives; +using FluentAssertions; +using Xunit.Abstractions; + +namespace Elastic.Markdown.Tests.Directives; + +public class AppliesBlockTests(ITestOutputHelper output) : DirectiveTest(output, +""" +# heading +```{applies} +:eck: unavailable +``` +""" +) +{ + [Fact] + public void ParsesBlock() => Block.Should().NotBeNull(); + + [Fact] + public void IncludesProductAvailability() => + Html.Should().Contain("Unavailable") + .And.Contain("Elastic Cloud Kubernetes") + .And.Contain("Applies To:"); + + + [Fact] + public void NoErrors() => Collector.Diagnostics.Should().BeEmpty(); +} + +public class EmptyAppliesBlock(ITestOutputHelper output) : DirectiveTest(output, +""" +```{applies} +``` +""" +) +{ + [Fact] + public void ParsesBlock() => Block.Should().NotBeNull(); + + [Fact] + public void DoesNotRender() => + Html.Should().BeNullOrWhiteSpace(); + + [Fact] + public void EmitErrorOnEmptyBlock() + { + Collector.Diagnostics.Should().NotBeNullOrEmpty().And.HaveCount(2); + Collector.Diagnostics.Should().OnlyContain(d => d.Severity == Severity.Error); + Collector.Diagnostics.Should() + .Contain(d => d.Message.Contains("{applies} block with no product availability specified")); + + Collector.Diagnostics.Should() + .Contain(d => d.Message.Contains("{applies} should follow a heading")); + } +} + +// ensures we allow for empty lines between heading and applies block +public class AppliesHeadingTests(ITestOutputHelper output) : DirectiveTest(output, +""" +# heading + + + +```{applies} +:eck: unavailable +``` +""" +) +{ + [Fact] + public void NoErrors() => Collector.Diagnostics.Should().BeEmpty(); +} +