From ee1beb05e9d69be28075910d4f1337e800f52e1e Mon Sep 17 00:00:00 2001 From: Jan Calanog Date: Wed, 24 Sep 2025 14:34:50 +0200 Subject: [PATCH 01/11] Add applies-switch component --- docs/_docset.yml | 3 +- docs/syntax/applies-switch.md | 163 +++++++++++++++ .../Assets/applicability-switch.ts | 88 ++++++++ .../Assets/applies-switch.ts | 88 ++++++++ src/Elastic.Documentation.Site/Assets/main.ts | 2 + .../Assets/markdown/applicability-switch.css | 52 +++++ .../Assets/markdown/applies-switch.css | 52 +++++ .../Assets/open-details-with-anchor.ts | 3 +- .../Assets/styles.css | 1 + src/Elastic.Markdown/Elastic.Markdown.csproj | 5 + .../Myst/Components/ApplicableToViewModel.cs | 1 - .../AppliesSwitch/AppliesItemView.cshtml | 35 ++++ .../AppliesSwitch/AppliesItemViewModel.cs | 20 ++ .../AppliesSwitch/AppliesSwitchBlock.cs | 99 +++++++++ .../AppliesSwitch/AppliesSwitchView.cshtml | 5 + .../AppliesSwitch/AppliesSwitchViewModel.cs | 8 + .../Myst/Directives/DirectiveBlockParser.cs | 7 + .../Myst/Directives/DirectiveHtmlRenderer.cs | 52 +++++ .../Directives/ApplicabilitySwitchTests.cs | 193 ++++++++++++++++++ 19 files changed, 873 insertions(+), 4 deletions(-) create mode 100644 docs/syntax/applies-switch.md create mode 100644 src/Elastic.Documentation.Site/Assets/applicability-switch.ts create mode 100644 src/Elastic.Documentation.Site/Assets/applies-switch.ts create mode 100644 src/Elastic.Documentation.Site/Assets/markdown/applicability-switch.css create mode 100644 src/Elastic.Documentation.Site/Assets/markdown/applies-switch.css create mode 100644 src/Elastic.Markdown/Myst/Directives/AppliesSwitch/AppliesItemView.cshtml create mode 100644 src/Elastic.Markdown/Myst/Directives/AppliesSwitch/AppliesItemViewModel.cs create mode 100644 src/Elastic.Markdown/Myst/Directives/AppliesSwitch/AppliesSwitchBlock.cs create mode 100644 src/Elastic.Markdown/Myst/Directives/AppliesSwitch/AppliesSwitchView.cshtml create mode 100644 src/Elastic.Markdown/Myst/Directives/AppliesSwitch/AppliesSwitchViewModel.cs create mode 100644 tests/Elastic.Markdown.Tests/Directives/ApplicabilitySwitchTests.cs diff --git a/docs/_docset.yml b/docs/_docset.yml index bfbd5018a..bf25daf6f 100644 --- a/docs/_docset.yml +++ b/docs/_docset.yml @@ -92,6 +92,7 @@ toc: - file: headings.md - file: admonitions.md - file: applies.md + - file: applies-switch.md - file: automated_settings.md - file: code.md - file: comments.md @@ -156,4 +157,4 @@ toc: - file: bar.md - folder: baz children: - - file: qux.md \ No newline at end of file + - file: qux.md diff --git a/docs/syntax/applies-switch.md b/docs/syntax/applies-switch.md new file mode 100644 index 000000000..6d4c6e6ec --- /dev/null +++ b/docs/syntax/applies-switch.md @@ -0,0 +1,163 @@ +# Applies Switch + +The applies-switch directive creates tabbed content where each tab displays an applies_to badge instead of a text title. This is useful for showing content that varies by deployment type, version, or other applicability criteria. + +## Basic Usage + +### Example + +#### Syntax + +```markdown +:::::{applies-switch} + +::::{applies-item} stack: preview 9.1 +:::{tip} +This feature is in preview for Elastic Stack 9.1. +::: +:::: + +::::{applies-item} ess: preview 9.1 +:::{note} +This feature is available for Elastic Cloud. +::: +:::: + +::::{applies-item} ece: removed +:::{warning} +This feature has been removed from Elastic Cloud Enterprise. +::: +:::: + +::::: +``` + +#### Result + +:::::{applies-switch} + +::::{applies-item} stack: preview 9.1 +:::{tip} +This feature is in preview for Elastic Stack 9.1. +::: +:::: + +::::{applies-item} ess: preview 9.1 +:::{note} +This feature is available for Elastic Cloud. +::: +:::: + +::::{applies-item} ece: removed +:::{warning} +This feature has been removed from Elastic Cloud Enterprise. +::: +:::: + +::::: + +## Automatic Grouping + +Applies switches are automatically grouped together by default. This means all applies switches on a page will sync with each other - when you select a version in one switch, all other switches will automatically switch to the same version. + +Items with the same applies_to definition will sync together across all switches on the page. For example, if you have `stack: preview 9.1` in multiple switches, selecting it in one will select it in all others. + +### Example + +In the following example we have two applies switch sets that are automatically grouped together. +Hence, both switch sets will be in sync. + +#### Syntax + +```markdown +::::{applies-switch} +:::{applies-item} stack: ga 8.11 +Content for GA version +::: + +:::{applies-item} stack: preview 9.1 +Content for preview version +::: + +:::: + +::::{applies-switch} +:::{applies-item} stack: ga 8.11 +Other content for GA version +::: + +:::{applies-item} stack: preview 9.1 +Other content for preview version +::: + +:::: +``` + +#### Result + +##### Automatically Grouped Applies Switches + +::::{applies-switch} +:::{applies-item} stack: ga 8.11 +Content for GA version +::: + +:::{applies-item} stack: preview 9.1 +Content for preview version +::: + +:::: + +::::{applies-switch} +:::{applies-item} stack: ga 8.11 +Other content for GA version +::: + +:::{applies-item} stack: preview 9.1 +Other content for preview version +::: + +:::: + +## Supported Applies To Definitions + +The `applies-item` directive accepts any valid applies_to definition that would work with the `{applies_to}` role. This includes: + +- **Stack versions**: `stack: ga 8.11`, `stack: preview 9.1` +- **Deployment types**: `ess: preview 9.1`, `ece: removed`, `eck: ga 8.11` +- **Product versions**: `product: preview 9.1` +- **Serverless projects**: `serverless: observability: preview 9.1` + +## Best Practices + +**DOs**
+✅ **Do:** Use clear, descriptive applies_to definitions
+✅ **Do:** Make sure all switch items have the same type of content and similar goals
+✅ **Do:** Keep switch content scannable and self-contained
+✅ **Do:** Include other block elements in switches, like [admonitions](admonitions.md)
+✅ **Do:** Use applies_to definitions that are meaningful to your users + +**DON'Ts**
+❌ **Don't:** Nest applies switches
+❌ **Don't:** Split step-by-step procedures across switches
+❌ **Don't:** Use more than 6 switch items (use as few as possible)
+❌ **Don't:** Use applies switches in [dropdowns](dropdowns.md)
+❌ **Don't:** Use applies_to definitions that are too similar or confusing + +## When to Use + +Use applies switches when: + +- Content varies significantly by deployment type, version, or other applicability criteria +- You want to show applies_to badges as tab titles instead of text +- You need to group related content that differs by applicability +- You want to provide a clear visual indication of what each content section applies to + +## Comparison with Regular Tabs + +| Feature | Regular Tabs | Applies Switch | +|---------|--------------|----------------| +| Tab titles | Text labels | Applies_to badges | +| Use case | General content organization | Content that varies by applicability | +| Visual indication | Text | Badge with version/deployment info | +| Best for | General content grouping | Version-specific or deployment-specific content | diff --git a/src/Elastic.Documentation.Site/Assets/applicability-switch.ts b/src/Elastic.Documentation.Site/Assets/applicability-switch.ts new file mode 100644 index 000000000..5f7fe933b --- /dev/null +++ b/src/Elastic.Documentation.Site/Assets/applicability-switch.ts @@ -0,0 +1,88 @@ +// TODO: refactor to typescript. this was copied from the tabs implementation + +// @ts-check + +// Extra JS capability for selected applies switches to be synced +// The selection is stored in local storage so that it persists across page loads. + +const as_id_to_elements = {} +const storageKeyPrefix = 'sphinx-design-applies-switch-id-' + +function create_key(el: HTMLElement) { + const syncId = el.getAttribute('data-sync-id') + const syncGroup = el.getAttribute('data-sync-group') + if (!syncId || !syncGroup) return null + return [syncGroup, syncId, syncGroup + '--' + syncId] +} + +/** + * Initialize the applies switch selection. + * + */ +function ready() { + // Find all applies switches with sync data + + const groups = [] + + document.querySelectorAll('.applies-switch-label').forEach((label) => { + if (label instanceof HTMLElement) { + const data = create_key(label) + if (data) { + const [group, id, key] = data + + // add click event listener + label.onclick = onAppliesSwitchLabelClick + + // store map of key to elements + if (!as_id_to_elements[key]) { + as_id_to_elements[key] = [] + } + as_id_to_elements[key].push(label) + + if (groups.indexOf(group) === -1) { + groups.push(group) + // Check if a specific switch has been selected via URL parameter + const switchParam = new URLSearchParams( + window.location.search + ).get(group) + if (switchParam) { + window.sessionStorage.setItem( + storageKeyPrefix + group, + switchParam + ) + } + } + + // Check is a specific switch has been selected previously + const previousId = window.sessionStorage.getItem( + storageKeyPrefix + group + ) + if (previousId === id) { + ;( + label.previousElementSibling as HTMLInputElement + ).checked = true + } + } + } + }) +} + +/** + * Activate other switches with the same sync id. + * + * @this {HTMLElement} - The element that was clicked. + */ +function onAppliesSwitchLabelClick() { + const data = create_key(this) + if (!data) return + const [group, id, key] = data + for (const label of as_id_to_elements[key]) { + if (label === this) continue + label.previousElementSibling.checked = true + } + window.sessionStorage.setItem(storageKeyPrefix + group, id) +} + +export function initAppliesSwitch() { + ready() +} diff --git a/src/Elastic.Documentation.Site/Assets/applies-switch.ts b/src/Elastic.Documentation.Site/Assets/applies-switch.ts new file mode 100644 index 000000000..5f7fe933b --- /dev/null +++ b/src/Elastic.Documentation.Site/Assets/applies-switch.ts @@ -0,0 +1,88 @@ +// TODO: refactor to typescript. this was copied from the tabs implementation + +// @ts-check + +// Extra JS capability for selected applies switches to be synced +// The selection is stored in local storage so that it persists across page loads. + +const as_id_to_elements = {} +const storageKeyPrefix = 'sphinx-design-applies-switch-id-' + +function create_key(el: HTMLElement) { + const syncId = el.getAttribute('data-sync-id') + const syncGroup = el.getAttribute('data-sync-group') + if (!syncId || !syncGroup) return null + return [syncGroup, syncId, syncGroup + '--' + syncId] +} + +/** + * Initialize the applies switch selection. + * + */ +function ready() { + // Find all applies switches with sync data + + const groups = [] + + document.querySelectorAll('.applies-switch-label').forEach((label) => { + if (label instanceof HTMLElement) { + const data = create_key(label) + if (data) { + const [group, id, key] = data + + // add click event listener + label.onclick = onAppliesSwitchLabelClick + + // store map of key to elements + if (!as_id_to_elements[key]) { + as_id_to_elements[key] = [] + } + as_id_to_elements[key].push(label) + + if (groups.indexOf(group) === -1) { + groups.push(group) + // Check if a specific switch has been selected via URL parameter + const switchParam = new URLSearchParams( + window.location.search + ).get(group) + if (switchParam) { + window.sessionStorage.setItem( + storageKeyPrefix + group, + switchParam + ) + } + } + + // Check is a specific switch has been selected previously + const previousId = window.sessionStorage.getItem( + storageKeyPrefix + group + ) + if (previousId === id) { + ;( + label.previousElementSibling as HTMLInputElement + ).checked = true + } + } + } + }) +} + +/** + * Activate other switches with the same sync id. + * + * @this {HTMLElement} - The element that was clicked. + */ +function onAppliesSwitchLabelClick() { + const data = create_key(this) + if (!data) return + const [group, id, key] = data + for (const label of as_id_to_elements[key]) { + if (label === this) continue + label.previousElementSibling.checked = true + } + window.sessionStorage.setItem(storageKeyPrefix + group, id) +} + +export function initAppliesSwitch() { + ready() +} diff --git a/src/Elastic.Documentation.Site/Assets/main.ts b/src/Elastic.Documentation.Site/Assets/main.ts index 1765ed596..462de6a1d 100644 --- a/src/Elastic.Documentation.Site/Assets/main.ts +++ b/src/Elastic.Documentation.Site/Assets/main.ts @@ -6,6 +6,7 @@ import { openDetailsWithAnchor } from './open-details-with-anchor' import { initNav } from './pages-nav' import { initSmoothScroll } from './smooth-scroll' import { initTabs } from './tabs' +import { initAppliesSwitch } from './applies-switch' import { initTocNav } from './toc-nav' import 'htmx-ext-head-support' import 'htmx-ext-preload' @@ -21,6 +22,7 @@ document.addEventListener('htmx:load', function (event) { initHighlight() initCopyButton() initTabs() + initAppliesSwitch() // We do this so that the navigation is not initialized twice if (isLazyLoadNavigationEnabled) { diff --git a/src/Elastic.Documentation.Site/Assets/markdown/applicability-switch.css b/src/Elastic.Documentation.Site/Assets/markdown/applicability-switch.css new file mode 100644 index 000000000..a935ff99e --- /dev/null +++ b/src/Elastic.Documentation.Site/Assets/markdown/applicability-switch.css @@ -0,0 +1,52 @@ +@layer components { + .applies-switch { + @apply relative mt-4 flex flex-wrap overflow-hidden; + + .applicable-info { + @apply border-none bg-transparent; + } + + .applicable-name, + .applicable-meta { + @apply text-sm! + } + + .applies-switch-label { + @apply text-ink-light border-grey-20 z-20 -mb-[1px] flex cursor-pointer items-center border-1 px-6 py-2; + &:not(:nth-of-type(1)) { + margin-left: -1px; + } + + &:hover { + @apply bg-grey-10 border-b-1 border-b-black text-black; + } + } + + .applies-switch-input { + @apply absolute opacity-0; + } + + .applies-switch-content { + @apply border-grey-20 z-0 order-99 hidden w-full border-1 px-6 pt-2 pb-6; + } + + .applies-switch-input:checked + .applies-switch-label + .applies-switch-content { + @apply block; + } + + .applies-switch-input:checked + .applies-switch-label, + .applies-switch-label:active { + @apply border-b-blue-elastic text-blue-elastic border-b-1; + } + + .applies-switch-input:focus-visible + .applies-switch-label { + outline: var(--outline-size) var(--outline-style) + var(--outline-color); + outline-offset: var(--outline-offset, var(--outline-size)); + } + + .applies-switch-fallback { + @apply text-sm font-medium; + } + } +} diff --git a/src/Elastic.Documentation.Site/Assets/markdown/applies-switch.css b/src/Elastic.Documentation.Site/Assets/markdown/applies-switch.css new file mode 100644 index 000000000..a935ff99e --- /dev/null +++ b/src/Elastic.Documentation.Site/Assets/markdown/applies-switch.css @@ -0,0 +1,52 @@ +@layer components { + .applies-switch { + @apply relative mt-4 flex flex-wrap overflow-hidden; + + .applicable-info { + @apply border-none bg-transparent; + } + + .applicable-name, + .applicable-meta { + @apply text-sm! + } + + .applies-switch-label { + @apply text-ink-light border-grey-20 z-20 -mb-[1px] flex cursor-pointer items-center border-1 px-6 py-2; + &:not(:nth-of-type(1)) { + margin-left: -1px; + } + + &:hover { + @apply bg-grey-10 border-b-1 border-b-black text-black; + } + } + + .applies-switch-input { + @apply absolute opacity-0; + } + + .applies-switch-content { + @apply border-grey-20 z-0 order-99 hidden w-full border-1 px-6 pt-2 pb-6; + } + + .applies-switch-input:checked + .applies-switch-label + .applies-switch-content { + @apply block; + } + + .applies-switch-input:checked + .applies-switch-label, + .applies-switch-label:active { + @apply border-b-blue-elastic text-blue-elastic border-b-1; + } + + .applies-switch-input:focus-visible + .applies-switch-label { + outline: var(--outline-size) var(--outline-style) + var(--outline-color); + outline-offset: var(--outline-offset, var(--outline-size)); + } + + .applies-switch-fallback { + @apply text-sm font-medium; + } + } +} diff --git a/src/Elastic.Documentation.Site/Assets/open-details-with-anchor.ts b/src/Elastic.Documentation.Site/Assets/open-details-with-anchor.ts index bec3af533..dd715904e 100644 --- a/src/Elastic.Documentation.Site/Assets/open-details-with-anchor.ts +++ b/src/Elastic.Documentation.Site/Assets/open-details-with-anchor.ts @@ -1,6 +1,6 @@ import { UAParser } from 'ua-parser-js' -const { getBrowser } = new UAParser() +const { browser } = UAParser() // This is a fix for anchors in details elements in non-Chrome browsers. export function openDetailsWithAnchor() { @@ -9,7 +9,6 @@ export function openDetailsWithAnchor() { if (target) { const closestDetails = target.closest('details') if (closestDetails) { - const browser = getBrowser() if (browser.name !== 'Chrome') { closestDetails.open = true target.scrollIntoView({ diff --git a/src/Elastic.Documentation.Site/Assets/styles.css b/src/Elastic.Documentation.Site/Assets/styles.css index f68803c76..dce11a744 100644 --- a/src/Elastic.Documentation.Site/Assets/styles.css +++ b/src/Elastic.Documentation.Site/Assets/styles.css @@ -7,6 +7,7 @@ @import './markdown/typography.css'; @import './markdown/list.css'; @import './markdown/tabs.css'; +@import './markdown/applies-switch.css'; @import './markdown/code.css'; @import './markdown/icons.css'; @import './markdown/kbd.css'; diff --git a/src/Elastic.Markdown/Elastic.Markdown.csproj b/src/Elastic.Markdown/Elastic.Markdown.csproj index e9fabe6d9..9d5529c8d 100644 --- a/src/Elastic.Markdown/Elastic.Markdown.csproj +++ b/src/Elastic.Markdown/Elastic.Markdown.csproj @@ -43,4 +43,9 @@ + + + + + diff --git a/src/Elastic.Markdown/Myst/Components/ApplicableToViewModel.cs b/src/Elastic.Markdown/Myst/Components/ApplicableToViewModel.cs index 756a020e7..dcc8494b0 100644 --- a/src/Elastic.Markdown/Myst/Components/ApplicableToViewModel.cs +++ b/src/Elastic.Markdown/Myst/Components/ApplicableToViewModel.cs @@ -162,4 +162,3 @@ private IEnumerable CombineItemsByKey(List } - diff --git a/src/Elastic.Markdown/Myst/Directives/AppliesSwitch/AppliesItemView.cshtml b/src/Elastic.Markdown/Myst/Directives/AppliesSwitch/AppliesItemView.cshtml new file mode 100644 index 000000000..72cde5d1d --- /dev/null +++ b/src/Elastic.Markdown/Myst/Directives/AppliesSwitch/AppliesItemView.cshtml @@ -0,0 +1,35 @@ +@using Elastic.Markdown.Myst.Components +@inherits RazorSlice + +@{ + var id = $"applies-switch-item-{Model.AppliesSwitchIndex}-{Model.Index}"; +} + + + +
+ @Model.RenderBlock() +
diff --git a/src/Elastic.Markdown/Myst/Directives/AppliesSwitch/AppliesItemViewModel.cs b/src/Elastic.Markdown/Myst/Directives/AppliesSwitch/AppliesItemViewModel.cs new file mode 100644 index 000000000..2219a247c --- /dev/null +++ b/src/Elastic.Markdown/Myst/Directives/AppliesSwitch/AppliesItemViewModel.cs @@ -0,0 +1,20 @@ +// 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.Documentation; +using Elastic.Documentation.AppliesTo; +using Elastic.Documentation.Configuration; + +namespace Elastic.Markdown.Myst.Directives.AppliesSwitch; + +public class AppliesItemViewModel : DirectiveViewModel +{ + public required int Index { get; init; } + public required int AppliesSwitchIndex { get; init; } + public required string AppliesToDefinition { get; init; } + public required ApplicableTo? AppliesTo { get; init; } + public required string? SyncKey { get; init; } + public required string? AppliesSwitchGroupKey { get; init; } + public required BuildContext BuildContext { get; init; } +} diff --git a/src/Elastic.Markdown/Myst/Directives/AppliesSwitch/AppliesSwitchBlock.cs b/src/Elastic.Markdown/Myst/Directives/AppliesSwitch/AppliesSwitchBlock.cs new file mode 100644 index 000000000..e430eeccc --- /dev/null +++ b/src/Elastic.Markdown/Myst/Directives/AppliesSwitch/AppliesSwitchBlock.cs @@ -0,0 +1,99 @@ +// 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.Linq; +using Elastic.Markdown.Diagnostics; +using Elastic.Markdown.Helpers; + +namespace Elastic.Markdown.Myst.Directives.AppliesSwitch; + +public class AppliesSwitchBlock(DirectiveBlockParser parser, ParserContext context) + : DirectiveBlock(parser, context) +{ + public override string Directive => "applies-switch"; + + public int Index { get; set; } + public string GetGroupKey() => Prop("group") ?? "applies-switches"; + + public override void FinalizeAndValidate(ParserContext context) => Index = FindIndex(); + + private int _index = -1; + + // For simplicity, we use the line number as the index. + // It's not ideal, but it's unique. + // This is more efficient than finding the root block and then finding the index. + public int FindIndex() + { + if (_index > -1) + return _index; + + _index = Line; + return _index; + } +} + +public class AppliesItemBlock(DirectiveBlockParser parser, ParserContext context) + : DirectiveBlock(parser, context), IBlockTitle +{ + public override string Directive => "applies-item"; + + public string AppliesToDefinition { get; private set; } = default!; + public string Title => AppliesToDefinition; // IBlockTitle implementation + public int Index { get; private set; } + public int AppliesSwitchIndex { get; private set; } + public string? AppliesSwitchGroupKey { get; private set; } + public string? SyncKey { get; private set; } + public bool Selected { get; private set; } + + public override void FinalizeAndValidate(ParserContext context) + { + if (string.IsNullOrWhiteSpace(Arguments)) + this.EmitError("{applies-item} requires an argument with applies_to definition."); + + AppliesToDefinition = (Arguments ?? "{undefined}").ReplaceSubstitutions(context); + Index = Parent!.IndexOf(this); + + var appliesSwitch = Parent as AppliesSwitchBlock; + + AppliesSwitchIndex = appliesSwitch?.FindIndex() ?? -1; + AppliesSwitchGroupKey = appliesSwitch?.GetGroupKey(); + + // Auto-generate sync key from applies_to definition if not provided + SyncKey = Prop("sync") ?? GenerateSyncKey(AppliesToDefinition); + Selected = PropBool("selected"); + } + + public static string GenerateSyncKey(string appliesToDefinition) + { + // Generate a consistent sync key from the applies_to definition + // This ensures items with the same applies_to definition sync together + // Normalize order by sorting comma-separated values + var normalized = NormalizeAppliesToDefinition(appliesToDefinition); + return normalized.Slugify().Replace(".", "-"); + } + + private static string NormalizeAppliesToDefinition(string appliesToDefinition) + { + // Find the colon to separate the key from the values + var colonIndex = appliesToDefinition.IndexOf(':'); + if (colonIndex == -1) + return appliesToDefinition; + + var key = appliesToDefinition.Substring(0, colonIndex).Trim(); + var valuesPart = appliesToDefinition.Substring(colonIndex + 1).Trim(); + + // Split by comma and sort the values to ensure consistent ordering + // e.g., "stack: preview 9.0, ga 9.1" becomes "stack: ga 9.1, preview 9.0" + var values = valuesPart.Split(',') + .Select(v => v.Trim()) + .Where(v => !string.IsNullOrEmpty(v)) + .OrderBy(v => v) + .ToArray(); + + if (values.Length <= 1) + return appliesToDefinition; + + return $"{key}: {string.Join(", ", values)}"; + } +} diff --git a/src/Elastic.Markdown/Myst/Directives/AppliesSwitch/AppliesSwitchView.cshtml b/src/Elastic.Markdown/Myst/Directives/AppliesSwitch/AppliesSwitchView.cshtml new file mode 100644 index 000000000..d6946d1b7 --- /dev/null +++ b/src/Elastic.Markdown/Myst/Directives/AppliesSwitch/AppliesSwitchView.cshtml @@ -0,0 +1,5 @@ +@inherits RazorSlice +
+ @Model.RenderBlock() +
+ diff --git a/src/Elastic.Markdown/Myst/Directives/AppliesSwitch/AppliesSwitchViewModel.cs b/src/Elastic.Markdown/Myst/Directives/AppliesSwitch/AppliesSwitchViewModel.cs new file mode 100644 index 000000000..1b65b7087 --- /dev/null +++ b/src/Elastic.Markdown/Myst/Directives/AppliesSwitch/AppliesSwitchViewModel.cs @@ -0,0 +1,8 @@ +// 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 + +namespace Elastic.Markdown.Myst.Directives.AppliesSwitch; + +public class AppliesSwitchViewModel : DirectiveViewModel; + diff --git a/src/Elastic.Markdown/Myst/Directives/DirectiveBlockParser.cs b/src/Elastic.Markdown/Myst/Directives/DirectiveBlockParser.cs index f53825fc0..01a33ea7f 100644 --- a/src/Elastic.Markdown/Myst/Directives/DirectiveBlockParser.cs +++ b/src/Elastic.Markdown/Myst/Directives/DirectiveBlockParser.cs @@ -4,6 +4,7 @@ using System.Collections.Frozen; using Elastic.Markdown.Myst.Directives.Admonition; +using Elastic.Markdown.Myst.Directives.AppliesSwitch; using Elastic.Markdown.Myst.Directives.CsvInclude; using Elastic.Markdown.Myst.Directives.Diagram; using Elastic.Markdown.Myst.Directives.Image; @@ -90,6 +91,12 @@ protected override DirectiveBlock CreateFencedBlock(BlockProcessor processor) if (info.IndexOf("{tab-item}") > 0) return new TabItemBlock(this, context); + if (info.IndexOf("{applies-switch}") > 0) + return new AppliesSwitchBlock(this, context); + + if (info.IndexOf("{applies-item}") > 0) + return new AppliesItemBlock(this, context); + if (info.IndexOf("{dropdown}") > 0) return new DropdownBlock(this, context); diff --git a/src/Elastic.Markdown/Myst/Directives/DirectiveHtmlRenderer.cs b/src/Elastic.Markdown/Myst/Directives/DirectiveHtmlRenderer.cs index b17629f3e..faace8aa3 100644 --- a/src/Elastic.Markdown/Myst/Directives/DirectiveHtmlRenderer.cs +++ b/src/Elastic.Markdown/Myst/Directives/DirectiveHtmlRenderer.cs @@ -3,9 +3,11 @@ // See the LICENSE file in the project root for more information using System.Diagnostics.CodeAnalysis; +using Elastic.Documentation.AppliesTo; using Elastic.Markdown.Diagnostics; using Elastic.Markdown.Myst.CodeBlocks; using Elastic.Markdown.Myst.Directives.Admonition; +using Elastic.Markdown.Myst.Directives.AppliesSwitch; using Elastic.Markdown.Myst.Directives.CsvInclude; using Elastic.Markdown.Myst.Directives.Diagram; using Elastic.Markdown.Myst.Directives.Dropdown; @@ -70,6 +72,12 @@ protected override void Write(HtmlRenderer renderer, DirectiveBlock directiveBlo case TabItemBlock tabItem: WriteTabItem(renderer, tabItem); return; + case AppliesSwitchBlock appliesSwitch: + WriteAppliesSwitch(renderer, appliesSwitch); + return; + case AppliesItemBlock appliesItem: + WriteAppliesItem(renderer, appliesItem); + return; case LiteralIncludeBlock literalIncludeBlock: WriteLiteralIncludeBlock(renderer, literalIncludeBlock); return; @@ -251,6 +259,50 @@ private static void WriteTabItem(HtmlRenderer renderer, TabItemBlock block) RenderRazorSlice(slice, renderer); } + private static void WriteAppliesSwitch(HtmlRenderer renderer, AppliesSwitchBlock block) + { + var slice = AppliesSwitchView.Create(new AppliesSwitchViewModel { DirectiveBlock = block }); + RenderRazorSlice(slice, renderer); + } + + private static void WriteAppliesItem(HtmlRenderer renderer, AppliesItemBlock block) + { + // Parse the applies_to definition to get the ApplicableTo object + var appliesTo = ParseApplicableTo(block.AppliesToDefinition, block); + var slice = AppliesItemView.Create(new AppliesItemViewModel + { + DirectiveBlock = block, + Index = block.Index, + AppliesToDefinition = block.AppliesToDefinition, + AppliesTo = appliesTo, + AppliesSwitchIndex = block.AppliesSwitchIndex, + SyncKey = block.SyncKey, + AppliesSwitchGroupKey = block.AppliesSwitchGroupKey, + BuildContext = block.Build + }); + RenderRazorSlice(slice, renderer); + } + + private static ApplicableTo? ParseApplicableTo(string yaml, DirectiveBlock block) + { + try + { + var applicableTo = YamlSerialization.Deserialize(yaml); + if (applicableTo.Diagnostics is null) + return applicableTo; + foreach (var (severity, message) in applicableTo.Diagnostics) + block.Emit(severity, message); + applicableTo.Diagnostics = null; + return applicableTo; + } + catch (Exception e) + { + block.EmitError($"Unable to parse applies_to definition: {yaml}", e); + } + + return null; + } + private static void WriteDiagram(HtmlRenderer renderer, DiagramBlock block) { var slice = DiagramView.Create(new DiagramViewModel diff --git a/tests/Elastic.Markdown.Tests/Directives/ApplicabilitySwitchTests.cs b/tests/Elastic.Markdown.Tests/Directives/ApplicabilitySwitchTests.cs new file mode 100644 index 000000000..41cafe62c --- /dev/null +++ b/tests/Elastic.Markdown.Tests/Directives/ApplicabilitySwitchTests.cs @@ -0,0 +1,193 @@ +// 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.Directives.AppliesSwitch; +using FluentAssertions; + +namespace Elastic.Markdown.Tests.Directives; + +public class ApplicabilitySwitchTests(ITestOutputHelper output) : DirectiveTest(output, +""" +:::::{applies-switch} + +::::{applies-item} stack: preview 9.1 +:::{tip} +This feature is in preview for Elastic Stack 9.1. +::: +:::: + +::::{applies-item} ess: preview 9.1 +:::{note} +This feature is available for Elastic Cloud. +::: +:::: + +::::{applies-item} ece: removed +:::{warning} +This feature has been removed from Elastic Cloud Enterprise. +::: +:::: + +::::: +""" +) +{ + [Fact] + public void ParsesBlock() => Block.Should().NotBeNull(); + + [Fact] + public void ParsesApplicabilitySwitchItems() + { + var items = Block!.OfType().ToArray(); + items.Should().NotBeNull().And.HaveCount(3); + for (var i = 0; i < items.Length; i++) + items[i].Index.Should().Be(i); + } + + [Fact] + public void ParsesAppliesToDefinitions() + { + var items = Block!.OfType().ToArray(); + items[0].AppliesToDefinition.Should().Be("stack: preview 9.1"); + items[1].AppliesToDefinition.Should().Be("ess: preview 9.1"); + items[2].AppliesToDefinition.Should().Be("ece: removed"); + } + + [Fact] + public void SetsCorrectDirectiveType() + { + Block!.Directive.Should().Be("applies-switch"); + var items = Block.OfType().ToArray(); + foreach (var item in items) + item.Directive.Should().Be("applies-item"); + } +} + +public class MultipleApplicabilitySwitchTests(ITestOutputHelper output) : DirectiveTest(output, +""" +:::::{applies-switch} +::::{applies-item} stack: ga 8.11 +Content for GA version +:::: +::::: + +Paragraph + +:::::{applies-switch} +::::{applies-item} stack: preview 9.1 +Content for preview version +:::: +::::: +""" +) +{ + [Fact] + public void ParsesMultipleApplicabilitySwitches() + { + var switches = Document.OfType().ToArray(); + switches.Length.Should().Be(2); + for (var s = 0; s < switches.Length; s++) + { + var items = switches[s].OfType().ToArray(); + items.Should().NotBeNull().And.HaveCount(1); + for (var i = 0; i < items.Length; i++) + { + items[i].Index.Should().Be(i); + items[i].AppliesSwitchIndex.Should().Be(switches[s].Line); + } + } + } +} + +public class GroupApplicabilitySwitchTests(ITestOutputHelper output) : DirectiveTest(output, +""" +::::{applies-switch} +:::{applies-item} stack: ga 8.11 +Content for GA version +::: + +:::{applies-item} stack: preview 9.1 +Content for preview version +::: + +:::{applies-item} stack: removed +Content for removed version +::: + +:::: + +::::{applies-switch} +:::{applies-item} stack: ga 8.11 +Content for GA version +::: + +:::{applies-item} stack: preview 9.1 +Content for preview version +::: + +:::{applies-item} stack: removed +Content for removed version +::: + +:::: +""" +) +{ + [Fact] + public void ParsesMultipleApplicabilitySwitches() + { + var switches = Document.OfType().ToArray(); + switches.Length.Should().Be(2); + for (var s = 0; s < switches.Length; s++) + { + var items = switches[s].OfType().ToArray(); + items.Should().NotBeNull().And.HaveCount(3); + for (var i = 0; i < items.Length; i++) + { + items[i].Index.Should().Be(i); + items[i].AppliesSwitchIndex.Should().Be(switches[s].Line); + } + } + } + + [Fact] + public void ParsesGroup() + { + var switches = Document.OfType().ToArray(); + switches.Length.Should().Be(2); + + foreach (var s in switches) + s.GetGroupKey().Should().Be("applies-switches"); + } + + [Fact] + public void ParsesSyncKey() + { + var switchBlock = Document.OfType().First(); + var items = switchBlock.OfType().ToArray(); + items.Should().HaveCount(3); + items[0].SyncKey.Should().Be("stack-ga-8-11"); + items[1].SyncKey.Should().Be("stack-preview-9-1"); + items[2].SyncKey.Should().Be("stack-removed"); + } + + [Fact] + public void NormalizesSyncKeyOrder() + { + // Test that different orderings of the same applies_to definition generate the same sync key + var testCases = new[] + { + ("stack: preview 9.0, ga 9.1", "stack: ga 9.1, preview 9.0"), + ("ess: preview 8.11, ga 8.10", "ess: ga 8.10, preview 8.11"), + ("stack: removed, preview 9.0", "stack: preview 9.0, removed") + }; + + foreach (var (definition1, definition2) in testCases) + { + var key1 = AppliesItemBlock.GenerateSyncKey(definition1); + var key2 = AppliesItemBlock.GenerateSyncKey(definition2); + key1.Should().Be(key2, $"Sync keys should be the same for '{definition1}' and '{definition2}'"); + } + } +} From 3cdb3a616c51cfb9af76c63d7199f0740967c535 Mon Sep 17 00:00:00 2001 From: Jan Calanog Date: Wed, 24 Sep 2025 17:42:56 +0200 Subject: [PATCH 02/11] Simplify docs --- docs/syntax/applies-switch.md | 159 +++++++++++++--------------------- 1 file changed, 58 insertions(+), 101 deletions(-) diff --git a/docs/syntax/applies-switch.md b/docs/syntax/applies-switch.md index 6d4c6e6ec..fd232fdcd 100644 --- a/docs/syntax/applies-switch.md +++ b/docs/syntax/applies-switch.md @@ -1,150 +1,116 @@ -# Applies Switch +# Applies switch The applies-switch directive creates tabbed content where each tab displays an applies_to badge instead of a text title. This is useful for showing content that varies by deployment type, version, or other applicability criteria. -## Basic Usage +## Basic usage -### Example +::::::{tab-set} +:::::{tab-item} Output -#### Syntax - -```markdown -:::::{applies-switch} +::::{applies-switch} -::::{applies-item} stack: preview 9.1 -:::{tip} -This feature is in preview for Elastic Stack 9.1. +:::{applies-item} stack: +Content for Stack ::: -:::: -::::{applies-item} ess: preview 9.1 -:::{note} -This feature is available for Elastic Cloud. +:::{applies-item} serverless: +Content for Serverless ::: -:::: -::::{applies-item} ece: removed -:::{warning} -This feature has been removed from Elastic Cloud Enterprise. -::: :::: ::::: -``` - -#### Result +:::::{tab-item} Markdown -:::::{applies-switch} +```markdown +::::{applies-switch} -::::{applies-item} stack: preview 9.1 -:::{tip} -This feature is in preview for Elastic Stack 9.1. +:::{applies-item} stack: +Content for Stack ::: -:::: -::::{applies-item} ess: preview 9.1 -:::{note} -This feature is available for Elastic Cloud. +:::{applies-item} serverless: +Content for Serverless ::: -:::: -::::{applies-item} ece: removed -:::{warning} -This feature has been removed from Elastic Cloud Enterprise. -::: :::: - +``` ::::: +:::::: -## Automatic Grouping - -Applies switches are automatically grouped together by default. This means all applies switches on a page will sync with each other - when you select a version in one switch, all other switches will automatically switch to the same version. +## Multiple `applies_to` definitions -Items with the same applies_to definition will sync together across all switches on the page. For example, if you have `stack: preview 9.1` in multiple switches, selecting it in one will select it in all others. - -### Example - -In the following example we have two applies switch sets that are automatically grouped together. -Hence, both switch sets will be in sync. +You can specify multiple `applies_to` definitions in a single `applies-item` using YAML object notation with curly braces `{}`. +This is useful when content applies to multiple deployment types or versions simultaneously. -#### Syntax +::::::{tab-set} +:::::{tab-item} Output -```markdown ::::{applies-switch} -:::{applies-item} stack: ga 8.11 -Content for GA version + +:::{applies-item} { ece:, ess: } +Content for ECE and ECH bla ::: -:::{applies-item} stack: preview 9.1 -Content for preview version +:::{applies-item} serverless: +Content for Serverless ::: :::: +::::: +:::::{tab-item} Markdown + +```markdown ::::{applies-switch} -:::{applies-item} stack: ga 8.11 -Other content for GA version + +:::{applies-item} { ece:, ess: } +Content for ECE and ECH bla ::: -:::{applies-item} stack: preview 9.1 -Other content for preview version +:::{applies-item} serverless: +Content for Serverless ::: :::: ``` +::::: +:::::: + +## Automatic grouping -#### Result +Applies switches are automatically grouped together by default. This means all applies switches on a page will sync with each other - when you select a version in one switch, all other switches will automatically switch to the same version. + +Items with the same applies_to definition will sync together across all switches on the page. For example, if you have `stack: preview 9.1` in multiple switches, selecting it in one will select it in all others. -##### Automatically Grouped Applies Switches +In the following example we have two applies switch sets that are automatically grouped together. +Hence, both switch sets will be in sync. ::::{applies-switch} -:::{applies-item} stack: ga 8.11 -Content for GA version +:::{applies-item} stack: ga 9.0 +Other content for 9.0 version ::: - -:::{applies-item} stack: preview 9.1 -Content for preview version +:::{applies-item} stack: ga 9.1 +Other content for 9.1 version ::: - :::: ::::{applies-switch} -:::{applies-item} stack: ga 8.11 -Other content for GA version +:::{applies-item} stack: ga 9.0 +Other content for 9.0 version ::: - -:::{applies-item} stack: preview 9.1 -Other content for preview version +:::{applies-item} stack: ga 9.1 +Other content for 9.1 version ::: - :::: -## Supported Applies To Definitions +## Supported `applies_to` definitions -The `applies-item` directive accepts any valid applies_to definition that would work with the `{applies_to}` role. This includes: +The `applies-item` directive accepts any valid applies_to definition that would work with the `{applies_to}` role. -- **Stack versions**: `stack: ga 8.11`, `stack: preview 9.1` -- **Deployment types**: `ess: preview 9.1`, `ece: removed`, `eck: ga 8.11` -- **Product versions**: `product: preview 9.1` -- **Serverless projects**: `serverless: observability: preview 9.1` +See the [](applies.md) page for more details on valid `applies_to` definitions. -## Best Practices - -**DOs**
-✅ **Do:** Use clear, descriptive applies_to definitions
-✅ **Do:** Make sure all switch items have the same type of content and similar goals
-✅ **Do:** Keep switch content scannable and self-contained
-✅ **Do:** Include other block elements in switches, like [admonitions](admonitions.md)
-✅ **Do:** Use applies_to definitions that are meaningful to your users - -**DON'Ts**
-❌ **Don't:** Nest applies switches
-❌ **Don't:** Split step-by-step procedures across switches
-❌ **Don't:** Use more than 6 switch items (use as few as possible)
-❌ **Don't:** Use applies switches in [dropdowns](dropdowns.md)
-❌ **Don't:** Use applies_to definitions that are too similar or confusing - -## When to Use +## When to use Use applies switches when: @@ -152,12 +118,3 @@ Use applies switches when: - You want to show applies_to badges as tab titles instead of text - You need to group related content that differs by applicability - You want to provide a clear visual indication of what each content section applies to - -## Comparison with Regular Tabs - -| Feature | Regular Tabs | Applies Switch | -|---------|--------------|----------------| -| Tab titles | Text labels | Applies_to badges | -| Use case | General content organization | Content that varies by applicability | -| Visual indication | Text | Badge with version/deployment info | -| Best for | General content grouping | Version-specific or deployment-specific content | From d212669ad17bceebb4a23b0c30c3e9b3218b1ab9 Mon Sep 17 00:00:00 2001 From: Jan Calanog Date: Wed, 24 Sep 2025 18:25:53 +0200 Subject: [PATCH 03/11] Fix styling --- .../Assets/markdown/applies-switch.css | 11 +---------- .../Directives/AppliesSwitch/AppliesItemView.cshtml | 2 +- 2 files changed, 2 insertions(+), 11 deletions(-) diff --git a/src/Elastic.Documentation.Site/Assets/markdown/applies-switch.css b/src/Elastic.Documentation.Site/Assets/markdown/applies-switch.css index a935ff99e..2556cd47f 100644 --- a/src/Elastic.Documentation.Site/Assets/markdown/applies-switch.css +++ b/src/Elastic.Documentation.Site/Assets/markdown/applies-switch.css @@ -2,17 +2,8 @@ .applies-switch { @apply relative mt-4 flex flex-wrap overflow-hidden; - .applicable-info { - @apply border-none bg-transparent; - } - - .applicable-name, - .applicable-meta { - @apply text-sm! - } - .applies-switch-label { - @apply text-ink-light border-grey-20 z-20 -mb-[1px] flex cursor-pointer items-center border-1 px-6 py-2; + @apply text-ink-light border-grey-20 z-20 -mb-[1px] flex cursor-pointer items-center border-1 px-3 py-2; &:not(:nth-of-type(1)) { margin-left: -1px; } diff --git a/src/Elastic.Markdown/Myst/Directives/AppliesSwitch/AppliesItemView.cshtml b/src/Elastic.Markdown/Myst/Directives/AppliesSwitch/AppliesItemView.cshtml index 72cde5d1d..473dff02d 100644 --- a/src/Elastic.Markdown/Myst/Directives/AppliesSwitch/AppliesItemView.cshtml +++ b/src/Elastic.Markdown/Myst/Directives/AppliesSwitch/AppliesItemView.cshtml @@ -11,7 +11,7 @@ { @if (Model.AppliesTo is not null) { - + @await RenderPartialAsync(ApplicableToComponent.Create(new ApplicableToViewModel { AppliesTo = Model.AppliesTo, From bea60e55276a17b642883ddae0e98b00f428fb11 Mon Sep 17 00:00:00 2001 From: Jan Calanog Date: Wed, 24 Sep 2025 18:30:47 +0200 Subject: [PATCH 04/11] Run prettier --- src/Elastic.Documentation.Site/Assets/main.ts | 2 +- .../Assets/markdown/applicability-switch.css | 8 +++++--- .../Assets/markdown/applies-switch.css | 6 ++++-- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/Elastic.Documentation.Site/Assets/main.ts b/src/Elastic.Documentation.Site/Assets/main.ts index 462de6a1d..ec0f22c6f 100644 --- a/src/Elastic.Documentation.Site/Assets/main.ts +++ b/src/Elastic.Documentation.Site/Assets/main.ts @@ -1,3 +1,4 @@ +import { initAppliesSwitch } from './applies-switch' import { initCopyButton } from './copybutton' import { initHighlight } from './hljs' import { initImageCarousel } from './image-carousel' @@ -6,7 +7,6 @@ import { openDetailsWithAnchor } from './open-details-with-anchor' import { initNav } from './pages-nav' import { initSmoothScroll } from './smooth-scroll' import { initTabs } from './tabs' -import { initAppliesSwitch } from './applies-switch' import { initTocNav } from './toc-nav' import 'htmx-ext-head-support' import 'htmx-ext-preload' diff --git a/src/Elastic.Documentation.Site/Assets/markdown/applicability-switch.css b/src/Elastic.Documentation.Site/Assets/markdown/applicability-switch.css index a935ff99e..efbb28ef5 100644 --- a/src/Elastic.Documentation.Site/Assets/markdown/applicability-switch.css +++ b/src/Elastic.Documentation.Site/Assets/markdown/applicability-switch.css @@ -1,14 +1,14 @@ @layer components { .applies-switch { @apply relative mt-4 flex flex-wrap overflow-hidden; - + .applicable-info { @apply border-none bg-transparent; } .applicable-name, .applicable-meta { - @apply text-sm! + @apply text-sm!; } .applies-switch-label { @@ -30,7 +30,9 @@ @apply border-grey-20 z-0 order-99 hidden w-full border-1 px-6 pt-2 pb-6; } - .applies-switch-input:checked + .applies-switch-label + .applies-switch-content { + .applies-switch-input:checked + + .applies-switch-label + + .applies-switch-content { @apply block; } diff --git a/src/Elastic.Documentation.Site/Assets/markdown/applies-switch.css b/src/Elastic.Documentation.Site/Assets/markdown/applies-switch.css index 2556cd47f..e425e836c 100644 --- a/src/Elastic.Documentation.Site/Assets/markdown/applies-switch.css +++ b/src/Elastic.Documentation.Site/Assets/markdown/applies-switch.css @@ -1,7 +1,7 @@ @layer components { .applies-switch { @apply relative mt-4 flex flex-wrap overflow-hidden; - + .applies-switch-label { @apply text-ink-light border-grey-20 z-20 -mb-[1px] flex cursor-pointer items-center border-1 px-3 py-2; &:not(:nth-of-type(1)) { @@ -21,7 +21,9 @@ @apply border-grey-20 z-0 order-99 hidden w-full border-1 px-6 pt-2 pb-6; } - .applies-switch-input:checked + .applies-switch-label + .applies-switch-content { + .applies-switch-input:checked + + .applies-switch-label + + .applies-switch-content { @apply block; } From 1bef5b4f865ed187497be2a6624f96359da83577 Mon Sep 17 00:00:00 2001 From: Jan Calanog Date: Wed, 24 Sep 2025 18:36:40 +0200 Subject: [PATCH 05/11] Fix cursor --- .../Assets/markdown/applies-switch.css | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Elastic.Documentation.Site/Assets/markdown/applies-switch.css b/src/Elastic.Documentation.Site/Assets/markdown/applies-switch.css index e425e836c..c92d77994 100644 --- a/src/Elastic.Documentation.Site/Assets/markdown/applies-switch.css +++ b/src/Elastic.Documentation.Site/Assets/markdown/applies-switch.css @@ -12,6 +12,10 @@ @apply bg-grey-10 border-b-1 border-b-black text-black; } } + + .applicable-info { + @apply cursor-pointer!; + } .applies-switch-input { @apply absolute opacity-0; From e2cbae5a129b2831dedf60ddfc73f01af7516e27 Mon Sep 17 00:00:00 2001 From: Jan Calanog Date: Wed, 24 Sep 2025 20:19:31 +0200 Subject: [PATCH 06/11] Align applies-item styling with tab-item --- .../Assets/markdown/applies-switch.css | 21 ++++++++++++++++--- .../Components/ApplicableToComponent.cshtml | 2 +- .../Myst/Components/ApplicableToViewModel.cs | 2 ++ .../AppliesSwitch/AppliesItemView.cshtml | 1 + 4 files changed, 22 insertions(+), 4 deletions(-) diff --git a/src/Elastic.Documentation.Site/Assets/markdown/applies-switch.css b/src/Elastic.Documentation.Site/Assets/markdown/applies-switch.css index c92d77994..aaa4ae90d 100644 --- a/src/Elastic.Documentation.Site/Assets/markdown/applies-switch.css +++ b/src/Elastic.Documentation.Site/Assets/markdown/applies-switch.css @@ -12,9 +12,24 @@ @apply bg-grey-10 border-b-1 border-b-black text-black; } } - - .applicable-info { - @apply cursor-pointer!; + + .applies-item { + @apply cursor-pointer text-inherit; + + &:hover { + @apply text-inherit; + } + .applicable-info { + @apply cursor-pointer border-none bg-transparent p-0; + + &:not(:last-child):after { + content: ','; + } + } + .applicable-name, + .applicable-meta { + @apply text-base; + } } .applies-switch-input { diff --git a/src/Elastic.Markdown/Myst/Components/ApplicableToComponent.cshtml b/src/Elastic.Markdown/Myst/Components/ApplicableToComponent.cshtml index a4f7aa3d6..a9831405c 100644 --- a/src/Elastic.Markdown/Myst/Components/ApplicableToComponent.cshtml +++ b/src/Elastic.Markdown/Myst/Components/ApplicableToComponent.cshtml @@ -2,7 +2,7 @@ @foreach (var item in Model.GetApplicabilityItems()) { - + @item.Key @if (!string.IsNullOrEmpty(item.Key) && (item.RenderData.ShowLifecycleName || item.RenderData.ShowVersion || !string.IsNullOrEmpty(item.RenderData.BadgeLifecycleText))) diff --git a/src/Elastic.Markdown/Myst/Components/ApplicableToViewModel.cs b/src/Elastic.Markdown/Myst/Components/ApplicableToViewModel.cs index dcc8494b0..bb7cc6093 100644 --- a/src/Elastic.Markdown/Myst/Components/ApplicableToViewModel.cs +++ b/src/Elastic.Markdown/Myst/Components/ApplicableToViewModel.cs @@ -13,6 +13,8 @@ public class ApplicableToViewModel private readonly ApplicabilityRenderer _applicabilityRenderer = new(); public required bool Inline { get; init; } + + public bool ShowTooltip { get; init; } = true; public required ApplicableTo AppliesTo { get; init; } public required VersionsConfiguration VersionsConfig { get; init; } diff --git a/src/Elastic.Markdown/Myst/Directives/AppliesSwitch/AppliesItemView.cshtml b/src/Elastic.Markdown/Myst/Directives/AppliesSwitch/AppliesItemView.cshtml index 473dff02d..b60efc6df 100644 --- a/src/Elastic.Markdown/Myst/Directives/AppliesSwitch/AppliesItemView.cshtml +++ b/src/Elastic.Markdown/Myst/Directives/AppliesSwitch/AppliesItemView.cshtml @@ -16,6 +16,7 @@ { AppliesTo = Model.AppliesTo, Inline = true, + ShowTooltip = false, VersionsConfig = Model.BuildContext.VersionsConfiguration })) From 9f8866f30e98065fea6eb37f103522cc1bfa1efe Mon Sep 17 00:00:00 2001 From: Jan Calanog Date: Wed, 24 Sep 2025 20:22:08 +0200 Subject: [PATCH 07/11] Remove "bla" --- docs/syntax/applies-switch.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/syntax/applies-switch.md b/docs/syntax/applies-switch.md index fd232fdcd..75eb892ab 100644 --- a/docs/syntax/applies-switch.md +++ b/docs/syntax/applies-switch.md @@ -49,7 +49,7 @@ This is useful when content applies to multiple deployment types or versions sim ::::{applies-switch} :::{applies-item} { ece:, ess: } -Content for ECE and ECH bla +Content for ECE and ECH ::: :::{applies-item} serverless: @@ -65,7 +65,7 @@ Content for Serverless ::::{applies-switch} :::{applies-item} { ece:, ess: } -Content for ECE and ECH bla +Content for ECE and ECH ::: :::{applies-item} serverless: From de48daa9e1d89771a78676a7d54a11f4be1e3ec4 Mon Sep 17 00:00:00 2001 From: Jan Calanog Date: Wed, 24 Sep 2025 21:58:09 +0200 Subject: [PATCH 08/11] Bett algorithm to generate a sync key --- .../AppliesSwitch/AppliesSwitchBlock.cs | 48 ++++++++----------- .../Directives/ApplicabilitySwitchTests.cs | 39 +++++++++++++-- 2 files changed, 55 insertions(+), 32 deletions(-) diff --git a/src/Elastic.Markdown/Myst/Directives/AppliesSwitch/AppliesSwitchBlock.cs b/src/Elastic.Markdown/Myst/Directives/AppliesSwitch/AppliesSwitchBlock.cs index e430eeccc..86f06e36c 100644 --- a/src/Elastic.Markdown/Myst/Directives/AppliesSwitch/AppliesSwitchBlock.cs +++ b/src/Elastic.Markdown/Myst/Directives/AppliesSwitch/AppliesSwitchBlock.cs @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information using System.Linq; +using Elastic.Documentation.AppliesTo; using Elastic.Markdown.Diagnostics; using Elastic.Markdown.Helpers; @@ -66,34 +67,23 @@ public override void FinalizeAndValidate(ParserContext context) public static string GenerateSyncKey(string appliesToDefinition) { - // Generate a consistent sync key from the applies_to definition - // This ensures items with the same applies_to definition sync together - // Normalize order by sorting comma-separated values - var normalized = NormalizeAppliesToDefinition(appliesToDefinition); - return normalized.Slugify().Replace(".", "-"); - } - - private static string NormalizeAppliesToDefinition(string appliesToDefinition) - { - // Find the colon to separate the key from the values - var colonIndex = appliesToDefinition.IndexOf(':'); - if (colonIndex == -1) - return appliesToDefinition; - - var key = appliesToDefinition.Substring(0, colonIndex).Trim(); - var valuesPart = appliesToDefinition.Substring(colonIndex + 1).Trim(); - - // Split by comma and sort the values to ensure consistent ordering - // e.g., "stack: preview 9.0, ga 9.1" becomes "stack: ga 9.1, preview 9.0" - var values = valuesPart.Split(',') - .Select(v => v.Trim()) - .Where(v => !string.IsNullOrEmpty(v)) - .OrderBy(v => v) - .ToArray(); - - if (values.Length <= 1) - return appliesToDefinition; - - return $"{key}: {string.Join(", ", values)}"; + // Parse the YAML to get the ApplicableTo object, then use its hash + // This ensures both simple syntax and YAML objects produce consistent sync keys + try + { + var applicableTo = YamlSerialization.Deserialize(appliesToDefinition); + if (applicableTo != null) + { + // Use the object's hash for a consistent, unique identifier + return $"applies-{Math.Abs(applicableTo.GetHashCode())}"; + } + } + catch + { + // If parsing fails, fall back to the original definition + } + + // Fallback to original definition if parsing fails + return appliesToDefinition.Slugify().Replace(".", "-"); } } diff --git a/tests/Elastic.Markdown.Tests/Directives/ApplicabilitySwitchTests.cs b/tests/Elastic.Markdown.Tests/Directives/ApplicabilitySwitchTests.cs index 41cafe62c..70e458bfd 100644 --- a/tests/Elastic.Markdown.Tests/Directives/ApplicabilitySwitchTests.cs +++ b/tests/Elastic.Markdown.Tests/Directives/ApplicabilitySwitchTests.cs @@ -167,9 +167,18 @@ public void ParsesSyncKey() var switchBlock = Document.OfType().First(); var items = switchBlock.OfType().ToArray(); items.Should().HaveCount(3); - items[0].SyncKey.Should().Be("stack-ga-8-11"); - items[1].SyncKey.Should().Be("stack-preview-9-1"); - items[2].SyncKey.Should().Be("stack-removed"); + + // Verify all sync keys have the expected hash-based format + foreach (var item in items) + { + item.SyncKey.Should().StartWith("applies-", "Sync key should start with 'applies-' prefix"); + item.SyncKey.Should().MatchRegex(@"^applies-\d+$", "Sync key should be in format 'applies-{hash}'"); + } + + // Verify that different applies_to definitions produce different sync keys + items[0].SyncKey.Should().NotBe(items[1].SyncKey, "Different applies_to definitions should produce different sync keys"); + items[1].SyncKey.Should().NotBe(items[2].SyncKey, "Different applies_to definitions should produce different sync keys"); + items[0].SyncKey.Should().NotBe(items[2].SyncKey, "Different applies_to definitions should produce different sync keys"); } [Fact] @@ -190,4 +199,28 @@ public void NormalizesSyncKeyOrder() key1.Should().Be(key2, $"Sync keys should be the same for '{definition1}' and '{definition2}'"); } } + + [Fact] + public void GeneratesConsistentSyncKeysForYamlObjects() + { + // Test that YAML object syntax and simple syntax produce the same sync key + var testCases = new[] + { + ("stack: ga 9.1", "stack: ga 9.1"), // Same format should produce same key + ("{ ece: all, ess: all }", "deployment: { ece: all, ess: all }"), // YAML object vs deployment object + ("{ stack: ga 9.1 }", "stack: ga 9.1"), // YAML object vs simple syntax + ("{ deployment: { ece: ga 9.0, ess: ga 9.1 } }", "deployment: { ece: ga 9.0, ess: ga 9.1 }"), // Nested YAML objects + }; + + foreach (var (yamlObject, equivalentSyntax) in testCases) + { + var key1 = AppliesItemBlock.GenerateSyncKey(yamlObject); + var key2 = AppliesItemBlock.GenerateSyncKey(equivalentSyntax); + key1.Should().Be(key2, $"Sync keys should be the same for YAML object '{yamlObject}' and equivalent syntax '{equivalentSyntax}'"); + + // Also verify the key has the expected format + key1.Should().StartWith("applies-", "Sync key should start with 'applies-' prefix"); + key1.Should().MatchRegex(@"^applies-\d+$", "Sync key should be in format 'applies-{hash}'"); + } + } } From 59e07e4619603723f3ac7731be83afc05fe69021 Mon Sep 17 00:00:00 2001 From: Jan Calanog Date: Thu, 25 Sep 2025 10:53:53 +0200 Subject: [PATCH 09/11] Remove duplicate files --- .../Assets/applicability-switch.ts | 88 ------------------- .../Assets/markdown/applicability-switch.css | 54 ------------ 2 files changed, 142 deletions(-) delete mode 100644 src/Elastic.Documentation.Site/Assets/applicability-switch.ts delete mode 100644 src/Elastic.Documentation.Site/Assets/markdown/applicability-switch.css diff --git a/src/Elastic.Documentation.Site/Assets/applicability-switch.ts b/src/Elastic.Documentation.Site/Assets/applicability-switch.ts deleted file mode 100644 index 5f7fe933b..000000000 --- a/src/Elastic.Documentation.Site/Assets/applicability-switch.ts +++ /dev/null @@ -1,88 +0,0 @@ -// TODO: refactor to typescript. this was copied from the tabs implementation - -// @ts-check - -// Extra JS capability for selected applies switches to be synced -// The selection is stored in local storage so that it persists across page loads. - -const as_id_to_elements = {} -const storageKeyPrefix = 'sphinx-design-applies-switch-id-' - -function create_key(el: HTMLElement) { - const syncId = el.getAttribute('data-sync-id') - const syncGroup = el.getAttribute('data-sync-group') - if (!syncId || !syncGroup) return null - return [syncGroup, syncId, syncGroup + '--' + syncId] -} - -/** - * Initialize the applies switch selection. - * - */ -function ready() { - // Find all applies switches with sync data - - const groups = [] - - document.querySelectorAll('.applies-switch-label').forEach((label) => { - if (label instanceof HTMLElement) { - const data = create_key(label) - if (data) { - const [group, id, key] = data - - // add click event listener - label.onclick = onAppliesSwitchLabelClick - - // store map of key to elements - if (!as_id_to_elements[key]) { - as_id_to_elements[key] = [] - } - as_id_to_elements[key].push(label) - - if (groups.indexOf(group) === -1) { - groups.push(group) - // Check if a specific switch has been selected via URL parameter - const switchParam = new URLSearchParams( - window.location.search - ).get(group) - if (switchParam) { - window.sessionStorage.setItem( - storageKeyPrefix + group, - switchParam - ) - } - } - - // Check is a specific switch has been selected previously - const previousId = window.sessionStorage.getItem( - storageKeyPrefix + group - ) - if (previousId === id) { - ;( - label.previousElementSibling as HTMLInputElement - ).checked = true - } - } - } - }) -} - -/** - * Activate other switches with the same sync id. - * - * @this {HTMLElement} - The element that was clicked. - */ -function onAppliesSwitchLabelClick() { - const data = create_key(this) - if (!data) return - const [group, id, key] = data - for (const label of as_id_to_elements[key]) { - if (label === this) continue - label.previousElementSibling.checked = true - } - window.sessionStorage.setItem(storageKeyPrefix + group, id) -} - -export function initAppliesSwitch() { - ready() -} diff --git a/src/Elastic.Documentation.Site/Assets/markdown/applicability-switch.css b/src/Elastic.Documentation.Site/Assets/markdown/applicability-switch.css deleted file mode 100644 index efbb28ef5..000000000 --- a/src/Elastic.Documentation.Site/Assets/markdown/applicability-switch.css +++ /dev/null @@ -1,54 +0,0 @@ -@layer components { - .applies-switch { - @apply relative mt-4 flex flex-wrap overflow-hidden; - - .applicable-info { - @apply border-none bg-transparent; - } - - .applicable-name, - .applicable-meta { - @apply text-sm!; - } - - .applies-switch-label { - @apply text-ink-light border-grey-20 z-20 -mb-[1px] flex cursor-pointer items-center border-1 px-6 py-2; - &:not(:nth-of-type(1)) { - margin-left: -1px; - } - - &:hover { - @apply bg-grey-10 border-b-1 border-b-black text-black; - } - } - - .applies-switch-input { - @apply absolute opacity-0; - } - - .applies-switch-content { - @apply border-grey-20 z-0 order-99 hidden w-full border-1 px-6 pt-2 pb-6; - } - - .applies-switch-input:checked - + .applies-switch-label - + .applies-switch-content { - @apply block; - } - - .applies-switch-input:checked + .applies-switch-label, - .applies-switch-label:active { - @apply border-b-blue-elastic text-blue-elastic border-b-1; - } - - .applies-switch-input:focus-visible + .applies-switch-label { - outline: var(--outline-size) var(--outline-style) - var(--outline-color); - outline-offset: var(--outline-offset, var(--outline-size)); - } - - .applies-switch-fallback { - @apply text-sm font-medium; - } - } -} From 7298004c22271ea7aa19e6b17102d4b9e96afc5e Mon Sep 17 00:00:00 2001 From: Jan Calanog Date: Thu, 25 Sep 2025 11:11:48 +0200 Subject: [PATCH 10/11] Explain automatic grouping a bit better --- docs/syntax/applies-switch.md | 42 +++++++++++++++++++++++++++++------ 1 file changed, 35 insertions(+), 7 deletions(-) diff --git a/docs/syntax/applies-switch.md b/docs/syntax/applies-switch.md index 75eb892ab..395a8110c 100644 --- a/docs/syntax/applies-switch.md +++ b/docs/syntax/applies-switch.md @@ -79,15 +79,26 @@ Content for Serverless ## Automatic grouping -Applies switches are automatically grouped together by default. This means all applies switches on a page will sync with each other - when you select a version in one switch, all other switches will automatically switch to the same version. +All applies switches on a page automatically sync together. When you select an applies_to definition in one switch, all other switches will switch to the same applies_to definition. -Items with the same applies_to definition will sync together across all switches on the page. For example, if you have `stack: preview 9.1` in multiple switches, selecting it in one will select it in all others. +The format of the applies_to definition doesn't matter - `stack: preview 9.1`, `{ "stack": "preview 9.1" }`, and `{ stack: "preview 9.1" }` all identify the same content and will sync together. -In the following example we have two applies switch sets that are automatically grouped together. -Hence, both switch sets will be in sync. +In the following example, both switch sets are automatically grouped and will stay in sync. + +::::::{tab-set} +:::::{tab-item} Output ::::{applies-switch} -:::{applies-item} stack: ga 9.0 +:::{applies-item} { "stack": "preview 9.0" } +Content for 9.0 version +::: +:::{applies-item} { "stack": "ga 9.1" } +Content for 9.1 version +::: +:::: + +::::{applies-switch} +:::{applies-item} stack: preview 9.0 Other content for 9.0 version ::: :::{applies-item} stack: ga 9.1 @@ -95,15 +106,32 @@ Other content for 9.1 version ::: :::: +::::: +:::::{tab-item} Markdown + +```markdown +::::{applies-switch} +:::{applies-item} { "ece": "ga 3.0" } +Other content for ECE 3.0 version +::: +:::{applies-item} { "ece": "ga 4.0" } +Other content for ECE 4.0 version +::: +:::: + ::::{applies-switch} -:::{applies-item} stack: ga 9.0 +:::{applies-item} ece: ga 3.0 Other content for 9.0 version ::: -:::{applies-item} stack: ga 9.1 +:::{applies-item} ece: ga 3.1 Other content for 9.1 version ::: :::: +``` +::::: +:::::: + ## Supported `applies_to` definitions The `applies-item` directive accepts any valid applies_to definition that would work with the `{applies_to}` role. From e797421e4109ac96c3841ae96419edeb559559af Mon Sep 17 00:00:00 2001 From: Jan Calanog Date: Thu, 25 Sep 2025 11:12:42 +0200 Subject: [PATCH 11/11] Fix markdown example --- docs/syntax/applies-switch.md | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/docs/syntax/applies-switch.md b/docs/syntax/applies-switch.md index 395a8110c..52ecf4a29 100644 --- a/docs/syntax/applies-switch.md +++ b/docs/syntax/applies-switch.md @@ -111,23 +111,22 @@ Other content for 9.1 version ```markdown ::::{applies-switch} -:::{applies-item} { "ece": "ga 3.0" } -Other content for ECE 3.0 version +:::{applies-item} { "stack": "preview 9.0" } +Content for 9.0 version ::: -:::{applies-item} { "ece": "ga 4.0" } -Other content for ECE 4.0 version +:::{applies-item} { "stack": "ga 9.1" } +Content for 9.1 version ::: :::: ::::{applies-switch} -:::{applies-item} ece: ga 3.0 +:::{applies-item} stack: preview 9.0 Other content for 9.0 version ::: -:::{applies-item} ece: ga 3.1 +:::{applies-item} stack: ga 9.1 Other content for 9.1 version ::: :::: - ``` ::::: ::::::