Skip to content

Commit edf52c6

Browse files
authored
Add :applies_to: prop to dropdown directive (#1949)
* Add `:applies_to:` prop to dropdown directive * Fix styling * Run prettier * Fix centering * Run dotnet format * Don't replace substitions in applies_to prop * Fix
1 parent a27a168 commit edf52c6

File tree

8 files changed

+292
-15
lines changed

8 files changed

+292
-15
lines changed

docs/syntax/dropdowns.md

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,3 +50,55 @@ Dropdown content
5050
::::
5151

5252
:::::
53+
54+
## With applies_to badge
55+
56+
You can add an applies_to badge to the dropdown title by specifying the `:applies_to:` option. This displays a badge indicating which deployment types, versions, or other applicability criteria the dropdown content applies to.
57+
58+
:::::{tab-set}
59+
60+
::::{tab-item} Output
61+
62+
:::{dropdown} Dropdown Title
63+
:applies_to: stack: ga 9.0
64+
Dropdown content for Stack GA 9.0
65+
:::
66+
67+
::::
68+
69+
::::{tab-item} Markdown
70+
```markdown
71+
:::{dropdown} Dropdown Title
72+
:applies_to: stack: ga 9.0
73+
Dropdown content for Stack GA 9.0
74+
:::
75+
```
76+
::::
77+
78+
:::::
79+
80+
## Multiple applies_to definitions
81+
82+
You can specify multiple `applies_to` definitions using YAML object notation with curly braces `{}`. This is useful when content applies to multiple deployment types or versions simultaneously.
83+
84+
:::::{tab-set}
85+
86+
::::{tab-item} Output
87+
88+
:::{dropdown} Dropdown Title
89+
:applies_to: { ece:, ess: }
90+
Dropdown content for ECE and ECH
91+
:::
92+
93+
::::
94+
95+
::::{tab-item} Markdown
96+
```markdown
97+
:::{dropdown} Dropdown Title
98+
:applies_to: { ece:, ess: }
99+
Dropdown content for ECE and ECH
100+
:::
101+
```
102+
::::
103+
104+
:::::

src/Elastic.Documentation.Site/Assets/markdown/dropdown.css

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
details.dropdown {
88
@apply border-grey-20 mt-4 block rounded-sm border-1 shadow-xs;
99
summary.dropdown-title {
10-
@apply text-ink-dark flex cursor-pointer items-center justify-between px-4 py-2 font-sans font-bold;
10+
@apply text-ink-dark flex cursor-pointer items-stretch justify-between font-sans font-bold;
1111
}
1212

1313
&[open] .dropdown-title {
@@ -20,6 +20,37 @@
2020
.dropdown-content {
2121
@apply px-4 pb-4;
2222
}
23+
24+
.dropdown-title__container {
25+
@apply flex items-stretch;
26+
}
27+
28+
.dropdown-title__separator {
29+
@apply bg-grey-20 block w-[1px];
30+
}
31+
32+
.dropdown-title__icon {
33+
@apply flex items-center justify-center px-4 py-2;
34+
}
35+
36+
.dropdown-title__summary-text {
37+
@apply flex items-center px-4 py-2;
38+
}
39+
40+
.applies-dropdown {
41+
@apply flex cursor-pointer items-center gap-1 p-2 px-4 font-normal;
42+
.applicable-info {
43+
@apply cursor-pointer border-none bg-transparent p-0;
44+
&:not(:last-child):after {
45+
@apply text-sm;
46+
content: ',';
47+
}
48+
}
49+
.applicable-name,
50+
.applicable-meta {
51+
@apply text-sm;
52+
}
53+
}
2354
}
2455
}
2556
}

src/Elastic.Markdown/Myst/Directives/Admonition/AdmonitionBlock.cs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
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
44

5+
using Elastic.Documentation.AppliesTo;
56
using Elastic.Markdown.Helpers;
67

78
namespace Elastic.Markdown.Myst.Directives.Admonition;
@@ -31,17 +32,40 @@ public AdmonitionBlock(DirectiveBlockParser parser, string admonition, ParserCon
3132

3233
public string Title { get; private set; }
3334

35+
public string? AppliesToDefinition { get; private set; }
36+
37+
public ApplicableTo? AppliesTo { get; private set; }
38+
3439
public override void FinalizeAndValidate(ParserContext context)
3540
{
3641
CrossReferenceName = Prop("name");
3742
DropdownOpen = TryPropBool("open");
3843
if (DropdownOpen.HasValue)
3944
Classes = "dropdown";
4045

46+
// Parse applies_to property if present
47+
AppliesToDefinition = Prop("applies_to");
48+
if (!string.IsNullOrEmpty(AppliesToDefinition))
49+
AppliesTo = ParseApplicableTo(AppliesToDefinition);
50+
4151
if (Admonition is "admonition" or "dropdown" && !string.IsNullOrEmpty(Arguments))
4252
Title = Arguments;
4353
else if (!string.IsNullOrEmpty(Arguments))
4454
Title += $" {Arguments}";
4555
Title = Title.ReplaceSubstitutions(context);
4656
}
57+
58+
private ApplicableTo? ParseApplicableTo(string yaml)
59+
{
60+
try
61+
{
62+
var applicableTo = YamlSerialization.Deserialize<ApplicableTo>(yaml, Build.ProductsConfiguration);
63+
return applicableTo;
64+
}
65+
catch
66+
{
67+
// If parsing fails, return null
68+
return null;
69+
}
70+
}
4771
}

src/Elastic.Markdown/Myst/Directives/Admonition/AdmonitionViewModel.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
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
44

5+
using Elastic.Documentation;
6+
using Elastic.Documentation.AppliesTo;
7+
using Elastic.Documentation.Configuration;
8+
59
namespace Elastic.Markdown.Myst.Directives.Admonition;
610

711
public class AdmonitionViewModel : DirectiveViewModel
@@ -11,4 +15,7 @@ public class AdmonitionViewModel : DirectiveViewModel
1115
public required string? CrossReferenceName { get; init; }
1216
public required string? Classes { get; init; }
1317
public required string? Open { get; init; }
18+
public string? AppliesToDefinition { get; init; }
19+
public ApplicableTo? AppliesTo { get; init; }
20+
public required BuildContext BuildContext { get; init; }
1421
}

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -208,12 +208,13 @@ public override BlockState TryContinue(BlockProcessor processor, Block block)
208208
if (block is not DirectiveBlock directiveBlock)
209209
return base.TryContinue(processor, block);
210210

211-
var tokens = line.ToString().Split(':', 3, RemoveEmptyEntries | TrimEntries);
211+
var tokens = line.ToString().Split(':', 2, RemoveEmptyEntries | TrimEntries);
212212
if (tokens.Length < 1)
213213
return base.TryContinue(processor, block);
214214

215215
var name = tokens[0];
216-
var data = tokens.Length > 1 ? string.Join(":", tokens[1..]) : string.Empty;
216+
var data = tokens.Length > 1 ? tokens[1] : string.Empty;
217+
217218
directiveBlock.AddProperty(name, data);
218219

219220
return BlockState.Continue;

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

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -220,7 +220,10 @@ private static void WriteAdmonition(HtmlRenderer renderer, AdmonitionBlock block
220220
CrossReferenceName = block.CrossReferenceName,
221221
Classes = block.Classes,
222222
Title = block.Title,
223-
Open = block.DropdownOpen.GetValueOrDefault() ? "open" : null
223+
Open = block.DropdownOpen.GetValueOrDefault() ? "open" : null,
224+
AppliesToDefinition = block.AppliesToDefinition,
225+
AppliesTo = block.AppliesTo,
226+
BuildContext = block.Build
224227
});
225228
RenderRazorSlice(slice, renderer);
226229
}
@@ -234,7 +237,10 @@ private static void WriteDropdown(HtmlRenderer renderer, DropdownBlock block)
234237
CrossReferenceName = block.CrossReferenceName,
235238
Classes = block.Classes,
236239
Title = block.Title,
237-
Open = block.DropdownOpen.GetValueOrDefault() ? "open" : null
240+
Open = block.DropdownOpen.GetValueOrDefault() ? "open" : null,
241+
AppliesToDefinition = block.AppliesToDefinition,
242+
AppliesTo = block.AppliesTo,
243+
BuildContext = block.Build
238244
});
239245
RenderRazorSlice(slice, renderer);
240246
}

src/Elastic.Markdown/Myst/Directives/Dropdown/DropdownView.cshtml

Lines changed: 29 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,35 @@
1+
@using Elastic.Markdown.Myst.Components
12
@inherits RazorSlice<Elastic.Markdown.Myst.Directives.Admonition.AdmonitionViewModel>
23
<details class="dropdown @Model.Classes" id="@Model.CrossReferenceName" open="@Model.Open">
34
<summary class="dropdown-title">
4-
<span class="sd-summary-text">@Model.Title</span>
5-
<svg
6-
xmlns="http://www.w3.org/2000/svg"
7-
fill="none"
8-
viewBox="0 0 24 24"
9-
stroke-width="1.5"
10-
stroke="currentColor"
11-
class="w-4 mr-1 shrink -rotate-90 group-has-checked/label:rotate-0 cursor-pointer text-ink">
12-
<path stroke-linecap="round" stroke-linejoin="round" d="m19.5 8.25-7.5 7.5-7.5-7.5"/>
13-
</svg>
5+
<div class="dropdown-title__container">
6+
@if (Model.AppliesTo is not null)
7+
{
8+
<span class="applies applies-dropdown">
9+
@await RenderPartialAsync(ApplicableToComponent.Create(new ApplicableToViewModel
10+
{
11+
AppliesTo = Model.AppliesTo,
12+
Inline = true,
13+
ShowTooltip = false,
14+
VersionsConfig = Model.BuildContext.VersionsConfiguration
15+
}))
16+
</span>
17+
<span class="dropdown-title__separator"></span>
18+
}
19+
<span class="dropdown-title__summary-text">@Model.Title</span>
20+
</div>
21+
<div class="dropdown-title__icon">
22+
<svg
23+
xmlns="http://www.w3.org/2000/svg"
24+
fill="none"
25+
viewBox="0 0 24 24"
26+
stroke-width="1.5"
27+
stroke="currentColor"
28+
class="w-4 mr-1 shrink -rotate-90 group-has-checked/label:rotate-0 cursor-pointer text-ink">
29+
<path stroke-linecap="round" stroke-linejoin="round" d="m19.5 8.25-7.5 7.5-7.5-7.5"/>
30+
</svg>
31+
</div>
32+
1433
</summary>
1534
<div class="dropdown-content">
1635
@Model.RenderBlock()

tests/Elastic.Markdown.Tests/Directives/AdmonitionTests.cs

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,3 +99,140 @@ A regular paragraph.
9999
[Fact]
100100
public void SetsDropdownOpen() => Block!.DropdownOpen.Should().BeTrue();
101101
}
102+
103+
public class DropdownAppliesToTests(ITestOutputHelper output) : DirectiveTest<AdmonitionBlock>(output,
104+
"""
105+
:::{dropdown} This is my custom dropdown
106+
:applies_to: stack: ga 9.0
107+
This is an attention block
108+
:::
109+
A regular paragraph.
110+
"""
111+
)
112+
{
113+
[Fact]
114+
public void SetsCorrectAdmonitionType() => Block!.Admonition.Should().Be("dropdown");
115+
116+
[Fact]
117+
public void SetsCustomTitle() => Block!.Title.Should().Be("This is my custom dropdown");
118+
119+
[Fact]
120+
public void SetsAppliesToDefinition() => Block!.AppliesToDefinition.Should().Be("stack: ga 9.0");
121+
122+
[Fact]
123+
public void ParsesAppliesTo() => Block!.AppliesTo.Should().NotBeNull();
124+
}
125+
126+
public class DropdownPropertyParsingTests(ITestOutputHelper output) : DirectiveTest<AdmonitionBlock>(output,
127+
"""
128+
:::{dropdown} Test Dropdown
129+
:open:
130+
:name: test-dropdown
131+
This is test content
132+
:::
133+
A regular paragraph.
134+
"""
135+
)
136+
{
137+
[Fact]
138+
public void SetsCorrectAdmonitionType() => Block!.Admonition.Should().Be("dropdown");
139+
140+
[Fact]
141+
public void SetsCustomTitle() => Block!.Title.Should().Be("Test Dropdown");
142+
143+
[Fact]
144+
public void SetsDropdownOpen() => Block!.DropdownOpen.Should().BeTrue();
145+
146+
[Fact]
147+
public void SetsCrossReferenceName() => Block!.CrossReferenceName.Should().Be("test-dropdown");
148+
}
149+
150+
public class DropdownNestedContentTests(ITestOutputHelper output) : DirectiveTest<AdmonitionBlock>(output,
151+
"""
152+
::::{dropdown} Nested Content Test
153+
:open:
154+
This dropdown contains nested content with colons:
155+
156+
- Time: 10:30 AM
157+
- URL: https://example.com:8080/path
158+
- Configuration: key:value pairs
159+
- Code: `function test() { return "hello:world"; }`
160+
161+
And even nested directives:
162+
163+
:::{note} Nested Note
164+
This is a nested note with colons: 10:30 AM
165+
:::
166+
167+
More content after nested directive.
168+
::::
169+
A regular paragraph.
170+
"""
171+
)
172+
{
173+
[Fact]
174+
public void SetsCorrectAdmonitionType() => Block!.Admonition.Should().Be("dropdown");
175+
176+
[Fact]
177+
public void SetsCustomTitle() => Block!.Title.Should().Be("Nested Content Test");
178+
179+
[Fact]
180+
public void SetsDropdownOpen() => Block!.DropdownOpen.Should().BeTrue();
181+
182+
[Fact]
183+
public void ContainsContentWithColons()
184+
{
185+
var html = Html;
186+
html.Should().Contain("Time: 10:30 AM");
187+
html.Should().Contain("URL: https://example.com:8080/path");
188+
html.Should().Contain("Configuration: key:value pairs");
189+
html.Should().Contain("function test() { return &quot;hello:world&quot;; }");
190+
}
191+
192+
[Fact]
193+
public void ContainsNestedDirective()
194+
{
195+
var html = Html;
196+
// Output the full HTML for inspection
197+
output.WriteLine("Generated HTML:");
198+
output.WriteLine(html);
199+
200+
html.Should().Contain("Nested Note");
201+
html.Should().Contain("This is a nested note with colons: 10:30 AM");
202+
// Verify the nested note was actually parsed as a directive, not just plain text
203+
html.Should().Contain("class=\"admonition note\"");
204+
html.Should().Contain("admonition-title");
205+
html.Should().Contain("admonition-content");
206+
}
207+
208+
[Fact]
209+
public void ContainsContentAfterNestedDirective()
210+
{
211+
var html = Html;
212+
html.Should().Contain("More content after nested directive");
213+
}
214+
}
215+
216+
public class DropdownComplexPropertyTests(ITestOutputHelper output) : DirectiveTest<AdmonitionBlock>(output,
217+
"""
218+
:::{dropdown} Complex Properties Test
219+
:applies_to: stack: ga 9.0
220+
This is content with applies_to property
221+
:::
222+
A regular paragraph.
223+
"""
224+
)
225+
{
226+
[Fact]
227+
public void SetsCorrectAdmonitionType() => Block!.Admonition.Should().Be("dropdown");
228+
229+
[Fact]
230+
public void SetsCustomTitle() => Block!.Title.Should().Be("Complex Properties Test");
231+
232+
[Fact]
233+
public void ParsesAppliesToWithComplexValue()
234+
{
235+
Block!.AppliesToDefinition.Should().Be("stack: ga 9.0");
236+
Block!.AppliesTo.Should().NotBeNull();
237+
}
238+
}

0 commit comments

Comments
 (0)