Skip to content

Commit 20d5f86

Browse files
authored
Add initial support for page level annotations for product support (#103)
1 parent 4bf4291 commit 20d5f86

File tree

18 files changed

+605
-5
lines changed

18 files changed

+605
-5
lines changed

docs/source/markup/applies.md

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
---
2+
title: Product Availability
3+
applies:
4+
stack: ga 8.1
5+
serverless: tech-preview
6+
hosted: beta 8.1.1
7+
eck: beta 3.0.2
8+
ece: unavailable
9+
---
10+
11+
12+
Using yaml frontmatter pages can explicitly indicate to each deployment targets availability and lifecycle status
13+
14+
15+
```yaml
16+
applies:
17+
stack: ga 8.1
18+
serverless: tech-preview
19+
hosted: beta 8.1.1
20+
eck: beta 3.0.2
21+
ece: unavailable
22+
```
23+
24+
Its syntax is
25+
26+
```
27+
<product>: <lifecycle> [version]
28+
```
29+
30+
Where version is optional.
31+
32+
`all` and empty string mean generally available for all active versions
33+
34+
```yaml
35+
applies:
36+
stack:
37+
serverless: all
38+
```
39+
40+
`all` and empty string can also be specified at a version level
41+
42+
```yaml
43+
applies:
44+
stack: beta all
45+
serverless: beta
46+
```
47+
48+
Are equivalent, note `all` just means we won't be rendering the version portion in the html.

src/Elastic.Markdown/Helpers/SemVersion.cs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ namespace Elastic.Markdown.Helpers;
1111
/// <summary>
1212
/// A semver2 compatible version.
1313
/// </summary>
14-
public sealed class SemVersion :
14+
public class SemVersion :
1515
IEquatable<SemVersion>,
1616
IComparable<SemVersion>,
1717
IComparable
@@ -92,6 +92,14 @@ public SemVersion(int major, int minor, int patch, string? prerelease, string? m
9292
Metadata = metadata ?? string.Empty;
9393
}
9494

95+
public static explicit operator SemVersion(string b)
96+
{
97+
var semVersion = TryParse(b, out var version) ? version : TryParse(b + ".0", out version) ? version : null;
98+
return semVersion ?? throw new ArgumentException($"'{b}' is not a valid semver2 version string.");
99+
}
100+
101+
public static implicit operator string(SemVersion d) => d.ToString();
102+
95103
/// <summary>
96104
///
97105
/// </summary>

src/Elastic.Markdown/IO/MarkdownFile.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using Elastic.Markdown.Diagnostics;
66
using Elastic.Markdown.Myst;
77
using Elastic.Markdown.Myst.Directives;
8+
using Elastic.Markdown.Myst.FrontMatter;
89
using Elastic.Markdown.Slices;
910
using Markdig;
1011
using Markdig.Extensions.Yaml;

src/Elastic.Markdown/Myst/Directives/IncludeBlock.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using System.IO.Abstractions;
55
using Elastic.Markdown.Diagnostics;
66
using Elastic.Markdown.IO;
7+
using Elastic.Markdown.Myst.FrontMatter;
78

89
namespace Elastic.Markdown.Myst.Directives;
910

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
// Licensed to Elasticsearch B.V under one or more agreements.
2+
// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
3+
// See the LICENSE file in the project root for more information
4+
5+
using Elastic.Markdown.Helpers;
6+
using YamlDotNet.Core;
7+
using YamlDotNet.Core.Events;
8+
using YamlDotNet.Serialization;
9+
10+
namespace Elastic.Markdown.Myst.FrontMatter;
11+
12+
public class AllVersions() : SemVersion(9999, 9999, 9999)
13+
{
14+
public static AllVersions Instance { get; } = new ();
15+
}
16+
17+
public class SemVersionConverter : IYamlTypeConverter
18+
{
19+
public bool Accepts(Type type) => type == typeof(SemVersion);
20+
21+
public object? ReadYaml(IParser parser, Type type, ObjectDeserializer rootDeserializer)
22+
{
23+
var value = parser.Consume<Scalar>();
24+
if (string.IsNullOrWhiteSpace(value.Value))
25+
return AllVersions.Instance;
26+
if (string.Equals(value.Value.Trim(), "all", StringComparison.InvariantCultureIgnoreCase))
27+
return AllVersions.Instance;
28+
return (SemVersion)value.Value;
29+
}
30+
31+
public void WriteYaml(IEmitter emitter, object? value, Type type, ObjectSerializer serializer)
32+
{
33+
if (value == null)
34+
return;
35+
emitter.Emit(new Scalar(value.ToString()!));
36+
}
37+
38+
public static bool TryParse(string? value, out SemVersion? version)
39+
{
40+
version = value?.Trim().ToLowerInvariant() switch
41+
{
42+
null => AllVersions.Instance,
43+
"all" => AllVersions.Instance,
44+
"" => AllVersions.Instance,
45+
_ => SemVersion.TryParse(value, out var v) ? v : SemVersion.TryParse(value + ".0", out v) ? v : null
46+
};
47+
return version is not null;
48+
}
49+
}
50+
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
// Licensed to Elasticsearch B.V under one or more agreements.
2+
// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
3+
// See the LICENSE file in the project root for more information
4+
5+
using YamlDotNet.Core;
6+
using YamlDotNet.Core.Events;
7+
using YamlDotNet.Serialization;
8+
9+
namespace Elastic.Markdown.Myst.FrontMatter;
10+
11+
[YamlSerializable]
12+
public record Deployment
13+
{
14+
[YamlMember(Alias = "self")]
15+
public SelfManagedDeployment? SelfManaged { get; set; }
16+
17+
[YamlMember(Alias = "cloud")]
18+
public CloudManagedDeployment? Cloud { get; set; }
19+
20+
public static Deployment All { get; } = new()
21+
{
22+
Cloud = CloudManagedDeployment.All,
23+
SelfManaged = SelfManagedDeployment.All
24+
};
25+
}
26+
27+
[YamlSerializable]
28+
public record SelfManagedDeployment
29+
{
30+
[YamlMember(Alias = "stack")]
31+
public ProductAvailability? Stack { get; set; }
32+
33+
[YamlMember(Alias = "ece")]
34+
public ProductAvailability? Ece { get; set; }
35+
36+
[YamlMember(Alias = "eck")]
37+
public ProductAvailability? Eck { get; set; }
38+
39+
public static SelfManagedDeployment All { get; } = new()
40+
{
41+
Stack = ProductAvailability.GenerallyAvailable,
42+
Ece = ProductAvailability.GenerallyAvailable,
43+
Eck = ProductAvailability.GenerallyAvailable
44+
};
45+
}
46+
47+
[YamlSerializable]
48+
public record CloudManagedDeployment
49+
{
50+
[YamlMember(Alias = "hosted")]
51+
public ProductAvailability? Hosted { get; set; }
52+
53+
[YamlMember(Alias = "serverless")]
54+
public ProductAvailability? Serverless { get; set; }
55+
56+
public static CloudManagedDeployment All { get; } = new()
57+
{
58+
Hosted = ProductAvailability.GenerallyAvailable,
59+
Serverless = ProductAvailability.GenerallyAvailable
60+
};
61+
62+
}
63+
64+
public class DeploymentConverter : IYamlTypeConverter
65+
{
66+
public bool Accepts(Type type) => type == typeof(Deployment);
67+
68+
public object? ReadYaml(IParser parser, Type type, ObjectDeserializer rootDeserializer)
69+
{
70+
if (parser.TryConsume<Scalar>(out var value))
71+
{
72+
if (string.IsNullOrWhiteSpace(value.Value))
73+
return Deployment.All;
74+
if (string.Equals(value.Value, "all", StringComparison.InvariantCultureIgnoreCase))
75+
return Deployment.All;
76+
}
77+
var x = rootDeserializer.Invoke(typeof(Dictionary<string, string>));
78+
if (x is not Dictionary<string, string> { Count: > 0 } dictionary)
79+
return null;
80+
81+
var deployment = new Deployment();
82+
83+
if (TryGetVersion("stack", out var version))
84+
{
85+
deployment.SelfManaged ??= new SelfManagedDeployment();
86+
deployment.SelfManaged.Stack = version;
87+
}
88+
if (TryGetVersion("ece", out version))
89+
{
90+
deployment.SelfManaged ??= new SelfManagedDeployment();
91+
deployment.SelfManaged.Ece = version;
92+
}
93+
if (TryGetVersion("eck", out version))
94+
{
95+
deployment.SelfManaged ??= new SelfManagedDeployment();
96+
deployment.SelfManaged.Eck = version;
97+
}
98+
if (TryGetVersion("hosted", out version))
99+
{
100+
deployment.Cloud ??= new CloudManagedDeployment();
101+
deployment.Cloud.Hosted = version;
102+
}
103+
if (TryGetVersion("serverless", out version))
104+
{
105+
deployment.Cloud ??= new CloudManagedDeployment();
106+
deployment.Cloud.Serverless = version;
107+
}
108+
return deployment;
109+
110+
bool TryGetVersion(string key, out ProductAvailability? semVersion)
111+
{
112+
semVersion = null;
113+
return dictionary.TryGetValue(key, out var v) && ProductAvailability.TryParse(v, out semVersion);
114+
}
115+
116+
}
117+
118+
public void WriteYaml(IEmitter emitter, object? value, Type type, ObjectSerializer serializer) =>
119+
serializer.Invoke(value, type);
120+
}

src/Elastic.Markdown/Myst/FrontMatterParser.cs renamed to src/Elastic.Markdown/Myst/FrontMatter/FrontMatterParser.cs

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
// Licensed to Elasticsearch B.V under one or more agreements.
22
// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
33
// See the LICENSE file in the project root for more information
4+
45
using YamlDotNet.Serialization;
5-
using YamlDotNet.Serialization.NamingConventions;
66

7-
namespace Elastic.Markdown.Myst;
7+
namespace Elastic.Markdown.Myst.FrontMatter;
88

99
[YamlStaticContext]
1010
public partial class YamlFrontMatterStaticContext;
@@ -20,6 +20,10 @@ public class YamlFrontMatter
2020

2121
[YamlMember(Alias = "sub")]
2222
public Dictionary<string, string>? Properties { get; set; }
23+
24+
25+
[YamlMember(Alias = "applies")]
26+
public Deployment? AppliesTo { get; set; }
2327
}
2428

2529
public static class FrontMatterParser
@@ -30,10 +34,13 @@ public static YamlFrontMatter Deserialize(string yaml)
3034

3135
var deserializer = new StaticDeserializerBuilder(new YamlFrontMatterStaticContext())
3236
.IgnoreUnmatchedProperties()
37+
.WithTypeConverter(new SemVersionConverter())
38+
.WithTypeConverter(new DeploymentConverter())
3339
.Build();
3440

3541
var frontMatter = deserializer.Deserialize<YamlFrontMatter>(input);
3642
return frontMatter;
3743

3844
}
3945
}
46+
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
// Licensed to Elasticsearch B.V under one or more agreements.
2+
// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
3+
// See the LICENSE file in the project root for more information
4+
5+
using Elastic.Markdown.Helpers;
6+
using YamlDotNet.Serialization;
7+
8+
namespace Elastic.Markdown.Myst.FrontMatter;
9+
10+
[YamlSerializable]
11+
public record ProductAvailability
12+
{
13+
public ProductLifecycle Lifecycle { get; init; }
14+
public SemVersion? Version { get; init; }
15+
16+
public static ProductAvailability GenerallyAvailable { get; } = new()
17+
{
18+
Lifecycle = ProductLifecycle.GenerallyAvailable, Version = AllVersions.Instance
19+
};
20+
21+
// <lifecycle> [version]
22+
public static bool TryParse(string? value, out ProductAvailability? availability)
23+
{
24+
if (string.IsNullOrWhiteSpace(value) || string.Equals(value.Trim(), "all", StringComparison.InvariantCultureIgnoreCase))
25+
{
26+
availability = GenerallyAvailable;
27+
return true;
28+
}
29+
30+
var tokens = value.Split(" ", StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries);
31+
if (tokens.Length < 1)
32+
{
33+
availability = null;
34+
return false;
35+
}
36+
var lifecycle = tokens[0].ToLowerInvariant() switch
37+
{
38+
"preview" => ProductLifecycle.TechnicalPreview,
39+
"tech-preview" => ProductLifecycle.TechnicalPreview,
40+
"beta" => ProductLifecycle.Beta,
41+
"dev" => ProductLifecycle.Development,
42+
"development" => ProductLifecycle.Development,
43+
"deprecated" => ProductLifecycle.Deprecated,
44+
"coming" => ProductLifecycle.Coming,
45+
"discontinued" => ProductLifecycle.Discontinued,
46+
"unavailable" => ProductLifecycle.Unavailable,
47+
"ga" => ProductLifecycle.GenerallyAvailable,
48+
_ => throw new ArgumentOutOfRangeException(nameof(tokens), tokens, $"Unknown product lifecycle: {tokens[0]}")
49+
};
50+
51+
var version = tokens.Length < 2 ? null : tokens[1] switch
52+
{
53+
null => AllVersions.Instance,
54+
"all" => AllVersions.Instance,
55+
"" => AllVersions.Instance,
56+
var t => SemVersionConverter.TryParse(t, out var v) ? v : null
57+
};
58+
availability = new ProductAvailability { Version = version, Lifecycle = lifecycle };
59+
return true;
60+
}
61+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
// Licensed to Elasticsearch B.V under one or more agreements.
2+
// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
3+
// See the LICENSE file in the project root for more information
4+
5+
using YamlDotNet.Serialization;
6+
7+
namespace Elastic.Markdown.Myst.FrontMatter;
8+
9+
[YamlSerializable]
10+
public enum ProductLifecycle
11+
{
12+
// technical preview (exists in current docs system per https://github.com/elastic/docs?tab=readme-ov-file#beta-dev-and-preview-experimental)
13+
[YamlMember(Alias = "preview")]
14+
TechnicalPreview,
15+
// beta (ditto)
16+
[YamlMember(Alias = "beta")]
17+
Beta,
18+
// dev (ditto, though it's uncertain whether it's ever used or still needed)
19+
[YamlMember(Alias = "development")]
20+
Development,
21+
// deprecated (exists in current docs system per https://github.com/elastic/docs?tab=readme-ov-file#additions-and-deprecations)
22+
[YamlMember(Alias = "deprecated")]
23+
Deprecated,
24+
// coming (ditto)
25+
[YamlMember(Alias = "coming")]
26+
Coming,
27+
// 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)
28+
[YamlMember(Alias = "discontinued")]
29+
Discontinued,
30+
// unavailable (for content that doesn't exist in a specific context and is never coming or not coming anytime soon)
31+
[YamlMember(Alias = "unavailable")]
32+
Unavailable,
33+
// ga (replaces "added" in the current docs system since it was not entirely clear how/if that overlapped with beta/preview states)
34+
[YamlMember(Alias = "ga")]
35+
GenerallyAvailable
36+
}

0 commit comments

Comments
 (0)