From ea0d057af3ca12d44f428143a27e4be1fabe9a79 Mon Sep 17 00:00:00 2001 From: Jan Calanog Date: Tue, 8 Jul 2025 01:42:38 +0200 Subject: [PATCH 01/13] Add kbd --- docs/_docset.yml | 1 + docs/syntax/kbd.md | 117 ++++++++++++++++++ docs/testing/req.md | 8 +- .../Assets/markdown/kbd.css | 5 + .../Assets/styles.css | 1 + src/Elastic.Markdown/Myst/MarkdownParser.cs | 2 + .../Myst/Roles/Kbd/KbdParser.cs | 17 +++ .../Myst/Roles/Kbd/KbdRole.cs | 13 ++ .../Myst/Roles/Kbd/KbdRoleRenderer.cs | 38 ++++++ 9 files changed, 199 insertions(+), 3 deletions(-) create mode 100644 docs/syntax/kbd.md create mode 100644 src/Elastic.Documentation.Site/Assets/markdown/kbd.css create mode 100644 src/Elastic.Markdown/Myst/Roles/Kbd/KbdParser.cs create mode 100644 src/Elastic.Markdown/Myst/Roles/Kbd/KbdRole.cs create mode 100644 src/Elastic.Markdown/Myst/Roles/Kbd/KbdRoleRenderer.cs diff --git a/docs/_docset.yml b/docs/_docset.yml index 15273c6dd..f5065f22b 100644 --- a/docs/_docset.yml +++ b/docs/_docset.yml @@ -93,6 +93,7 @@ toc: - file: frontmatter.md - file: icons.md - file: images.md + - file: kbd.md - file: lists.md - file: line_breaks.md - file: links.md diff --git a/docs/syntax/kbd.md b/docs/syntax/kbd.md new file mode 100644 index 000000000..856247f12 --- /dev/null +++ b/docs/syntax/kbd.md @@ -0,0 +1,117 @@ +# Keyboard shortcuts + +You can represent keyboard keys and shortcuts in your documentation using the `{kbd}` role. This is useful for showing keyboard commands and shortcuts in a visually consistent way. + +## Basic usage + +To display a keyboard key, use the syntax `` {kbd}`key-name` ``. For example, writing `` {kbd}`Enter` `` will render as a styled keyboard key. + +::::{tab-set} + +:::{tab-item} Output +Press {kbd}`Enter` to submit. +::: + +:::{tab-item} Markdown +```markdown +Press {kbd}`Enter` to submit. +``` +::: + +:::: + +## Keyboard combinations + +You can represent keyboard combinations by joining multiple `{kbd}` roles with a plus sign (+). + +::::{tab-set} + +:::{tab-item} Output +{kbd}`ctrl` + {kbd}`C` to copy text. + +{kbd}`Shift` + {kbd}`Alt` + {kbd}`F` to format the document. +::: + +:::{tab-item} Markdown +```markdown +{kbd}`Ctrl` + {kbd}`C` to copy text. + +{kbd}`Shift` + {kbd}`Alt` + {kbd}`F` to format the document. +``` +::: + +:::: + +## Common shortcuts by platform + +Here are some common keyboard shortcuts across different platforms: + +::::{tab-set} + +:::{tab-item} Output +| Mac | Windows/Linux | Description | +|-------------------------|----------------------------|-----------------------------| +| {kbd}`⌘` + {kbd}`C` | {kbd}`Ctrl` + {kbd}`C` | Copy | +| {kbd}`⌘` + {kbd}`V` | {kbd}`Ctrl` + {kbd}`V` | Paste | +| {kbd}`⌘` + {kbd}`Z` | {kbd}`Ctrl` + {kbd}`Z` | Undo | +| {kbd}`⌘` + {kbd}`Enter` | {kbd}`Ctrl` + {kbd}`Enter` | Run a query | +| {kbd}`⌘` + {kbd}`/` | {kbd}`Ctrl` + {kbd}`/` | Comment or uncomment a line | +::: + +:::{tab-item} Markdown +```markdown +| Mac | Windows/Linux | Description | +|-------------------------|----------------------------|-----------------------------| +| {kbd}`⌘` + {kbd}`C` | {kbd}`Ctrl` + {kbd}`C` | Copy | +| {kbd}`⌘` + {kbd}`V` | {kbd}`Ctrl` + {kbd}`V` | Paste | +| {kbd}`⌘` + {kbd}`Z` | {kbd}`Ctrl` + {kbd}`Z` | Undo | +| {kbd}`⌘` + {kbd}`Enter` | {kbd}`Ctrl` + {kbd}`Enter` | Run a query | +| {kbd}`⌘` + {kbd}`/` | {kbd}`Ctrl` + {kbd}`/` | Comment or uncomment a line | +``` +::: + +:::: + +## Special keys + +Some commonly used special keys: + +::::{tab-set} + +:::{tab-item} Output +| Symbol | Key Description | +|-----------|------------------| +| {kbd}`⌘` | Command (Mac) | +| {kbd}`⌥` | Option/Alt (Mac) | +| {kbd}`⇧` | Shift | +| {kbd}`⌃` | Control | +| {kbd}`↩` | Return/Enter | +| {kbd}`⌫` | Delete/Backspace | +| {kbd}`⇥` | Tab | +| {kbd}`↑` | Up Arrow | +| {kbd}`↓` | Down Arrow | +| {kbd}`←` | Left Arrow | +| {kbd}`→` | Right Arrow | +| {kbd}`⎋` | Escape | +::: + +:::{tab-item} Markdown +```markdown +| Symbol | Key Description | +|-----------|------------------| +| {kbd}`⌘` | Command (Mac) | +| {kbd}`⌥` | Option/Alt (Mac) | +| {kbd}`⇧` | Shift | +| {kbd}`⌃` | Control | +| {kbd}`↩` | Return/Enter | +| {kbd}`⌫` | Delete/Backspace | +| {kbd}`⇥` | Tab | +| {kbd}`↑` | Up Arrow | +| {kbd}`↓` | Down Arrow | +| {kbd}`←` | Left Arrow | +| {kbd}`→` | Right Arrow | +| {kbd}`⎋` | Escape | +``` +::: + +:::: diff --git a/docs/testing/req.md b/docs/testing/req.md index 24cd5f6f9..b6a058c05 100644 --- a/docs/testing/req.md +++ b/docs/testing/req.md @@ -15,10 +15,12 @@ stack: preview 9.0, ga 9.1 {applies_to}`stack: preview 9.0` This tutorial is based on Elasticsearch 9.0. This tutorial is based on Elasticsearch 9.0. This tutorial is based on Elasticsearch 9.0. -This tutorial is based on Elasticsearch 9.0. - -what +This tutorial is {kbd}`⌘` + {kbd}`/` based on Elasticsearch 9.0. +| Mac | Windows/Linux | Description | +|-------------------------|----------------------------|-----------------------------| +| {kbd}`⌘` + {kbd}`Enter` | {kbd}`Ctrl` + {kbd}`Enter` | Run a query | +| {kbd}`⌘` + {kbd}`/` | {kbd}`Ctrl` + {kbd}`/` | Comment or uncomment a line | To follow this tutorial you will need to install the following components: diff --git a/src/Elastic.Documentation.Site/Assets/markdown/kbd.css b/src/Elastic.Documentation.Site/Assets/markdown/kbd.css new file mode 100644 index 000000000..ecaa1d11d --- /dev/null +++ b/src/Elastic.Documentation.Site/Assets/markdown/kbd.css @@ -0,0 +1,5 @@ +@layer components { + kbd { + @apply bg-grey-20 text-grey-100 border-grey-60 shadow-grey-60 relative top-[-2px] cursor-default rounded-sm border px-1 py-0.5 text-center font-mono text-xs leading-none capitalize shadow-[0_2px_0_1px]; + } +} diff --git a/src/Elastic.Documentation.Site/Assets/styles.css b/src/Elastic.Documentation.Site/Assets/styles.css index 2ff2a0b92..14f3b6738 100644 --- a/src/Elastic.Documentation.Site/Assets/styles.css +++ b/src/Elastic.Documentation.Site/Assets/styles.css @@ -9,6 +9,7 @@ @import './markdown/tabs.css'; @import './markdown/code.css'; @import './markdown/icons.css'; +@import './markdown/kbd.css'; @import './copybutton.css'; @import './markdown/admonition.css'; @import './markdown/dropdown.css'; diff --git a/src/Elastic.Markdown/Myst/MarkdownParser.cs b/src/Elastic.Markdown/Myst/MarkdownParser.cs index 2b7025595..2acb938e4 100644 --- a/src/Elastic.Markdown/Myst/MarkdownParser.cs +++ b/src/Elastic.Markdown/Myst/MarkdownParser.cs @@ -15,6 +15,7 @@ using Elastic.Markdown.Myst.Renderers; using Elastic.Markdown.Myst.Roles.AppliesTo; using Elastic.Markdown.Myst.Roles.Icons; +using Elastic.Markdown.Myst.Roles.Kbd; using Markdig; using Markdig.Extensions.EmphasisExtras; using Markdig.Parsers; @@ -147,6 +148,7 @@ public static MarkdownPipeline Pipeline .UseEmphasisExtras(EmphasisExtraOptions.Default) .UseInlineAppliesTo() .UseInlineIcons() + .UseInlineKbd() .UseSubstitution() .UseComments() .UseYamlFrontMatter() diff --git a/src/Elastic.Markdown/Myst/Roles/Kbd/KbdParser.cs b/src/Elastic.Markdown/Myst/Roles/Kbd/KbdParser.cs new file mode 100644 index 000000000..606ff83f4 --- /dev/null +++ b/src/Elastic.Markdown/Myst/Roles/Kbd/KbdParser.cs @@ -0,0 +1,17 @@ +// 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; +using Markdig.Parsers; + +namespace Elastic.Markdown.Myst.Roles.Kbd; + +public class KbdParser : RoleParser +{ + + protected override KbdRole CreateRole(string role, string content, InlineProcessor parserContext) => + new(role, content); + + protected override bool Matches(ReadOnlySpan role) => role is "{kbd}"; +} diff --git a/src/Elastic.Markdown/Myst/Roles/Kbd/KbdRole.cs b/src/Elastic.Markdown/Myst/Roles/Kbd/KbdRole.cs new file mode 100644 index 000000000..0da83136d --- /dev/null +++ b/src/Elastic.Markdown/Myst/Roles/Kbd/KbdRole.cs @@ -0,0 +1,13 @@ +// 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.Diagnostics; + +namespace Elastic.Markdown.Myst.Roles.Kbd; + +[DebuggerDisplay("{GetType().Name} Line: {Line}, Role: {Role}, Content: {Content}")] +public class KbdRole(string role, string content) : RoleLeaf(role, content) +{ + public string KeyboardText { get; } = content; +} diff --git a/src/Elastic.Markdown/Myst/Roles/Kbd/KbdRoleRenderer.cs b/src/Elastic.Markdown/Myst/Roles/Kbd/KbdRoleRenderer.cs new file mode 100644 index 000000000..5b6188aa4 --- /dev/null +++ b/src/Elastic.Markdown/Myst/Roles/Kbd/KbdRoleRenderer.cs @@ -0,0 +1,38 @@ +// 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 Markdig; +using Markdig.Parsers.Inlines; +using Markdig.Renderers; +using Markdig.Renderers.Html; +using Markdig.Renderers.Html.Inlines; + +namespace Elastic.Markdown.Myst.Roles.Kbd; + +public class KbdRoleHtmlRenderer : HtmlObjectRenderer +{ + protected override void Write(HtmlRenderer renderer, KbdRole role) + { + _ = renderer.Write(""); + _ = renderer.Write(role.KeyboardText); + _ = renderer.Write(""); + } +} + +public static class InlineKbdExtensions +{ + public static MarkdownPipelineBuilder UseInlineKbd(this MarkdownPipelineBuilder pipeline) + { + pipeline.Extensions.AddIfNotAlready(); + return pipeline; + } +} + +public class InlineKbdExtension : IMarkdownExtension +{ + public void Setup(MarkdownPipelineBuilder pipeline) => _ = pipeline.InlineParsers.InsertBefore(new KbdParser()); + + public void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer) => + renderer.ObjectRenderers.InsertBefore(new KbdRoleHtmlRenderer()); +} From d110ece23b8e2f49ecc14a42db48afd0bd8b6696 Mon Sep 17 00:00:00 2001 From: Jan Calanog Date: Tue, 8 Jul 2025 09:44:52 +0200 Subject: [PATCH 02/13] Styling adjustments --- src/Elastic.Documentation.Site/Assets/markdown/kbd.css | 2 +- src/Elastic.Documentation.Site/Assets/styles.css | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/Elastic.Documentation.Site/Assets/markdown/kbd.css b/src/Elastic.Documentation.Site/Assets/markdown/kbd.css index ecaa1d11d..5fd11e70a 100644 --- a/src/Elastic.Documentation.Site/Assets/markdown/kbd.css +++ b/src/Elastic.Documentation.Site/Assets/markdown/kbd.css @@ -1,5 +1,5 @@ @layer components { kbd { - @apply bg-grey-20 text-grey-100 border-grey-60 shadow-grey-60 relative top-[-2px] cursor-default rounded-sm border px-1 py-0.5 text-center font-mono text-xs leading-none capitalize shadow-[0_2px_0_1px]; + @apply bg-grey-20 text-grey-100 border-grey-50 shadow-grey-50 relative top-[-2px] inline-block min-w-[18px] cursor-default rounded-sm border px-1 pt-[3px] pb-[2px] text-center align-middle font-mono text-sm leading-none capitalize shadow-[0_2px_0_1px]; } } diff --git a/src/Elastic.Documentation.Site/Assets/styles.css b/src/Elastic.Documentation.Site/Assets/styles.css index 14f3b6738..018c897f5 100644 --- a/src/Elastic.Documentation.Site/Assets/styles.css +++ b/src/Elastic.Documentation.Site/Assets/styles.css @@ -228,3 +228,9 @@ body { .tippy-content { white-space: pre-line; } + +.icon, +.icon > * { + user-select: none; + pointer-events: none; +} From be6391e05a54cd719e4f37107e83606852d2776c Mon Sep 17 00:00:00 2001 From: Jan Calanog Date: Tue, 8 Jul 2025 09:48:02 +0200 Subject: [PATCH 03/13] Revert req.md --- docs/testing/req.md | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/docs/testing/req.md b/docs/testing/req.md index b6a058c05..24cd5f6f9 100644 --- a/docs/testing/req.md +++ b/docs/testing/req.md @@ -15,12 +15,10 @@ stack: preview 9.0, ga 9.1 {applies_to}`stack: preview 9.0` This tutorial is based on Elasticsearch 9.0. This tutorial is based on Elasticsearch 9.0. This tutorial is based on Elasticsearch 9.0. -This tutorial is {kbd}`⌘` + {kbd}`/` based on Elasticsearch 9.0. +This tutorial is based on Elasticsearch 9.0. + +what -| Mac | Windows/Linux | Description | -|-------------------------|----------------------------|-----------------------------| -| {kbd}`⌘` + {kbd}`Enter` | {kbd}`Ctrl` + {kbd}`Enter` | Run a query | -| {kbd}`⌘` + {kbd}`/` | {kbd}`Ctrl` + {kbd}`/` | Comment or uncomment a line | To follow this tutorial you will need to install the following components: From 9c5891ea6c33d249230dd97391c422909fb96b9b Mon Sep 17 00:00:00 2001 From: Jan Calanog Date: Tue, 8 Jul 2025 10:47:52 +0200 Subject: [PATCH 04/13] Add test --- tests/authoring/Inline/AppliesToRole.fs | 151 +--------------------- tests/authoring/Inline/KbdRole.fs | 164 ++++++++++++++++++++++++ tests/authoring/authoring.fsproj | 1 + 3 files changed, 170 insertions(+), 146 deletions(-) create mode 100644 tests/authoring/Inline/KbdRole.fs diff --git a/tests/authoring/Inline/AppliesToRole.fs b/tests/authoring/Inline/AppliesToRole.fs index 42cc35a7f..808e8ed85 100644 --- a/tests/authoring/Inline/AppliesToRole.fs +++ b/tests/authoring/Inline/AppliesToRole.fs @@ -2,7 +2,7 @@ // Elasticsearch B.V licenses this file to you under the Apache 2.0 License. // See the LICENSE file in the project root for more information -module ``inline elements``.``applies_to role`` +module ``inline elements``.``kbd role`` open Elastic.Markdown.Myst.FrontMatter open Elastic.Markdown.Myst.Roles.AppliesTo @@ -11,154 +11,13 @@ open Xunit open authoring open authoring.MarkdownDocumentAssertions -type ``parses inline {applies_to} role`` () = +type ``parses inline {kbd} role`` () = static let markdown = Setup.Markdown """ - -This is an inline {applies_to}`stack: preview 9.1` element. +Press {kbd}`Ctrl` + {kbd}`S` to save. """ [] - let ``parses to AppliesDirective`` () = - let directives = markdown |> converts "index.md" |> parses - test <@ directives.Length = 1 @> - directives |> appliesToDirective (ApplicableTo( - Stack=AppliesCollection.op_Explicit "preview 9.1.0" - )) - - [] - let ``validate HTML: generates link and alt attr`` () = + let ``validate HTML`` () = markdown |> convertsToHtml """ -

This is an inline - - - Stack - - - Planned - - - - element.

-""" - - -type ``parses nested ess moniker`` () = - static let markdown = Setup.Markdown """ - -This is an inline {applies_to}`ess: preview 9.1` element. -""" - - [] - let ``parses to AppliesDirective`` () = - let directives = markdown |> converts "index.md" |> parses - test <@ directives.Length = 1 @> - directives |> appliesToDirective (ApplicableTo( - Deployment=DeploymentApplicability( - Ess=AppliesCollection.op_Explicit "preview 9.1.0" - ) - )) - -type ``parses {preview} shortcut`` () = - static let markdown = Setup.Markdown """ - -This is an inline {preview}`9.1` element. -""" - - [] - let ``parses to AppliesDirective`` () = - let directives = markdown |> converts "index.md" |> parses - test <@ directives.Length = 1 @> - directives |> appliesToDirective (ApplicableTo( - Product=AppliesCollection.op_Explicit "preview 9.1.0" - )) - - -type ``parses applies to without version in table`` () = - static let markdown = Setup.Markdown """ -| col1 | col2 | -|------|------------------------------| -| test | {applies_to}`ece: removed` | -""" - - [] - let ``parses to AppliesDirective`` () = - let directives = markdown |> converts "index.md" |> parses - test <@ directives.Length = 1 @> - directives |> appliesToDirective (ApplicableTo( - Deployment=DeploymentApplicability( - Ece=AppliesCollection.op_Explicit "removed" - ) - )) - -type ``parses applies to with text afterwards`` () = - static let markdown = Setup.Markdown """ -{applies_to}`ece: removed` hello world -""" - - [] - let ``parses to AppliesDirective`` () = - let directives = markdown |> converts "index.md" |> parses - test <@ directives.Length = 1 @> - directives |> appliesToDirective (ApplicableTo( - Deployment=DeploymentApplicability( - Ece=AppliesCollection.op_Explicit "removed" - ) - )) - -type ``parses multiple applies_to in one line`` () = - static let markdown = Setup.Markdown """ -{applies_to}`ece: removed` {applies_to}`ece: removed` -""" - - [] - let ``parses to AppliesDirective`` () = - let directives = markdown |> converts "index.md" |> parses - test <@ directives.Length = 2 @> - directives |> appliesToDirective (ApplicableTo( - Deployment=DeploymentApplicability( - Ece=AppliesCollection.op_Explicit "removed" - ) - )) - -type ``render 'GA Planned' if preview exists alongside ga`` () = - static let markdown = Setup.Markdown """ - -This is an inline {applies_to}`stack: preview 9.0, ga 9.1` element. -""" - - [] - let ``parses to AppliesDirective`` () = - let directives = markdown |> converts "index.md" |> parses - test <@ directives.Length = 1 @> - directives |> appliesToDirective (ApplicableTo( - Stack=AppliesCollection.op_Explicit "preview 9.0, ga 9.1" - )) - - [] - let ``validate HTML: generates link and alt attr`` () = - markdown |> convertsToHtml """ -

This is an inline - - - Stack - - - Planned - - - - Stack - - - GA planned - - - - element.

+

Press Ctrl + S to save.

""" diff --git a/tests/authoring/Inline/KbdRole.fs b/tests/authoring/Inline/KbdRole.fs new file mode 100644 index 000000000..42cc35a7f --- /dev/null +++ b/tests/authoring/Inline/KbdRole.fs @@ -0,0 +1,164 @@ +// 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 + +module ``inline elements``.``applies_to role`` + +open Elastic.Markdown.Myst.FrontMatter +open Elastic.Markdown.Myst.Roles.AppliesTo +open Swensen.Unquote +open Xunit +open authoring +open authoring.MarkdownDocumentAssertions + +type ``parses inline {applies_to} role`` () = + static let markdown = Setup.Markdown """ + +This is an inline {applies_to}`stack: preview 9.1` element. +""" + + [] + let ``parses to AppliesDirective`` () = + let directives = markdown |> converts "index.md" |> parses + test <@ directives.Length = 1 @> + directives |> appliesToDirective (ApplicableTo( + Stack=AppliesCollection.op_Explicit "preview 9.1.0" + )) + + [] + let ``validate HTML: generates link and alt attr`` () = + markdown |> convertsToHtml """ +

This is an inline + + + Stack + + + Planned + + + + element.

+""" + + +type ``parses nested ess moniker`` () = + static let markdown = Setup.Markdown """ + +This is an inline {applies_to}`ess: preview 9.1` element. +""" + + [] + let ``parses to AppliesDirective`` () = + let directives = markdown |> converts "index.md" |> parses + test <@ directives.Length = 1 @> + directives |> appliesToDirective (ApplicableTo( + Deployment=DeploymentApplicability( + Ess=AppliesCollection.op_Explicit "preview 9.1.0" + ) + )) + +type ``parses {preview} shortcut`` () = + static let markdown = Setup.Markdown """ + +This is an inline {preview}`9.1` element. +""" + + [] + let ``parses to AppliesDirective`` () = + let directives = markdown |> converts "index.md" |> parses + test <@ directives.Length = 1 @> + directives |> appliesToDirective (ApplicableTo( + Product=AppliesCollection.op_Explicit "preview 9.1.0" + )) + + +type ``parses applies to without version in table`` () = + static let markdown = Setup.Markdown """ +| col1 | col2 | +|------|------------------------------| +| test | {applies_to}`ece: removed` | +""" + + [] + let ``parses to AppliesDirective`` () = + let directives = markdown |> converts "index.md" |> parses + test <@ directives.Length = 1 @> + directives |> appliesToDirective (ApplicableTo( + Deployment=DeploymentApplicability( + Ece=AppliesCollection.op_Explicit "removed" + ) + )) + +type ``parses applies to with text afterwards`` () = + static let markdown = Setup.Markdown """ +{applies_to}`ece: removed` hello world +""" + + [] + let ``parses to AppliesDirective`` () = + let directives = markdown |> converts "index.md" |> parses + test <@ directives.Length = 1 @> + directives |> appliesToDirective (ApplicableTo( + Deployment=DeploymentApplicability( + Ece=AppliesCollection.op_Explicit "removed" + ) + )) + +type ``parses multiple applies_to in one line`` () = + static let markdown = Setup.Markdown """ +{applies_to}`ece: removed` {applies_to}`ece: removed` +""" + + [] + let ``parses to AppliesDirective`` () = + let directives = markdown |> converts "index.md" |> parses + test <@ directives.Length = 2 @> + directives |> appliesToDirective (ApplicableTo( + Deployment=DeploymentApplicability( + Ece=AppliesCollection.op_Explicit "removed" + ) + )) + +type ``render 'GA Planned' if preview exists alongside ga`` () = + static let markdown = Setup.Markdown """ + +This is an inline {applies_to}`stack: preview 9.0, ga 9.1` element. +""" + + [] + let ``parses to AppliesDirective`` () = + let directives = markdown |> converts "index.md" |> parses + test <@ directives.Length = 1 @> + directives |> appliesToDirective (ApplicableTo( + Stack=AppliesCollection.op_Explicit "preview 9.0, ga 9.1" + )) + + [] + let ``validate HTML: generates link and alt attr`` () = + markdown |> convertsToHtml """ +

This is an inline + + + Stack + + + Planned + + + + Stack + + + GA planned + + + + element.

+""" diff --git a/tests/authoring/authoring.fsproj b/tests/authoring/authoring.fsproj index a58fafc22..10d499049 100644 --- a/tests/authoring/authoring.fsproj +++ b/tests/authoring/authoring.fsproj @@ -43,6 +43,7 @@ + From 634768bfb6311ad6a9e6588c9be92a0fa448a275 Mon Sep 17 00:00:00 2001 From: Jan Calanog Date: Tue, 8 Jul 2025 20:45:40 +0200 Subject: [PATCH 05/13] Use keywords for special keys and allow combined keys --- docs/syntax/kbd.md | 143 ++++++++------ .../Assets/markdown/kbd.css | 6 +- src/Elastic.Markdown/Myst/Roles/Kbd/Kbd.cs | 187 ++++++++++++++++++ .../Myst/Roles/Kbd/KbdRole.cs | 2 +- .../Myst/Roles/Kbd/KbdRoleRenderer.cs | 5 +- tests/authoring/Inline/AppliesToRole.fs | 151 +++++++++++++- tests/authoring/Inline/KbdRole.fs | 154 ++------------- 7 files changed, 445 insertions(+), 203 deletions(-) create mode 100644 src/Elastic.Markdown/Myst/Roles/Kbd/Kbd.cs diff --git a/docs/syntax/kbd.md b/docs/syntax/kbd.md index 856247f12..83539cb05 100644 --- a/docs/syntax/kbd.md +++ b/docs/syntax/kbd.md @@ -4,114 +4,143 @@ You can represent keyboard keys and shortcuts in your documentation using the `{ ## Basic usage -To display a keyboard key, use the syntax `` {kbd}`key-name` ``. For example, writing `` {kbd}`Enter` `` will render as a styled keyboard key. +To display a keyboard key, use the syntax `` {kbd}`key-name` ``. For example, writing `` {kbd}`enter` `` will render as a styled keyboard key. ::::{tab-set} :::{tab-item} Output -Press {kbd}`Enter` to submit. +Press {kbd}`enter` to submit. ::: :::{tab-item} Markdown ```markdown -Press {kbd}`Enter` to submit. +Press {kbd}`enter` to submit. ``` ::: :::: -## Keyboard combinations +## Combining keys -You can represent keyboard combinations by joining multiple `{kbd}` roles with a plus sign (+). +For keyboard shortcuts involving multiple keys, you can combine them within a single `{kbd}` role by separating the key names with a `+`. ::::{tab-set} :::{tab-item} Output -{kbd}`ctrl` + {kbd}`C` to copy text. - -{kbd}`Shift` + {kbd}`Alt` + {kbd}`F` to format the document. +Use {kbd}`cmd+shift+enter` to execute the command. ::: :::{tab-item} Markdown ```markdown -{kbd}`Ctrl` + {kbd}`C` to copy text. - -{kbd}`Shift` + {kbd}`Alt` + {kbd}`F` to format the document. +Use {kbd}`cmd+shift+enter` to execute the command. ``` ::: :::: -## Common shortcuts by platform +Alternatively, you can use multiple `{kbd}` roles to describe a shortcut. This approach is useful when you want to visually separate keys. Use a `+` to represent a combination and a `/` to represent alternative keys. + +::::{tab-set} -Here are some common keyboard shortcuts across different platforms: +:::{tab-item} Output +{kbd}`ctrl` + {kbd}`c` to copy text, or {kbd}`cmd` + {kbd}`c` on Mac. +::: + +:::{tab-item} Markdown +```markdown +{kbd}`ctrl` + {kbd}`c` to copy text, or {kbd}`cmd` + {kbd}`c` on Mac. +``` +::: + +:::: ::::{tab-set} :::{tab-item} Output -| Mac | Windows/Linux | Description | -|-------------------------|----------------------------|-----------------------------| -| {kbd}`⌘` + {kbd}`C` | {kbd}`Ctrl` + {kbd}`C` | Copy | -| {kbd}`⌘` + {kbd}`V` | {kbd}`Ctrl` + {kbd}`V` | Paste | -| {kbd}`⌘` + {kbd}`Z` | {kbd}`Ctrl` + {kbd}`Z` | Undo | -| {kbd}`⌘` + {kbd}`Enter` | {kbd}`Ctrl` + {kbd}`Enter` | Run a query | -| {kbd}`⌘` + {kbd}`/` | {kbd}`Ctrl` + {kbd}`/` | Comment or uncomment a line | +{kbd}`ctrl` / {kbd}`cmd` + {kbd}`c` to copy text. ::: + :::{tab-item} Markdown ```markdown -| Mac | Windows/Linux | Description | -|-------------------------|----------------------------|-----------------------------| -| {kbd}`⌘` + {kbd}`C` | {kbd}`Ctrl` + {kbd}`C` | Copy | -| {kbd}`⌘` + {kbd}`V` | {kbd}`Ctrl` + {kbd}`V` | Paste | -| {kbd}`⌘` + {kbd}`Z` | {kbd}`Ctrl` + {kbd}`Z` | Undo | -| {kbd}`⌘` + {kbd}`Enter` | {kbd}`Ctrl` + {kbd}`Enter` | Run a query | -| {kbd}`⌘` + {kbd}`/` | {kbd}`Ctrl` + {kbd}`/` | Comment or uncomment a line | +{kbd}`ctrl` / {kbd}`cmd` + {kbd}`c` to copy text. ``` ::: :::: -## Special keys +## Common shortcuts by platform -Some commonly used special keys: +The platform-specific examples below demonstrate how to combine special keys and regular characters. ::::{tab-set} :::{tab-item} Output -| Symbol | Key Description | -|-----------|------------------| -| {kbd}`⌘` | Command (Mac) | -| {kbd}`⌥` | Option/Alt (Mac) | -| {kbd}`⇧` | Shift | -| {kbd}`⌃` | Control | -| {kbd}`↩` | Return/Enter | -| {kbd}`⌫` | Delete/Backspace | -| {kbd}`⇥` | Tab | -| {kbd}`↑` | Up Arrow | -| {kbd}`↓` | Down Arrow | -| {kbd}`←` | Left Arrow | -| {kbd}`→` | Right Arrow | -| {kbd}`⎋` | Escape | + +| Mac | Windows/Linux | Description | +|------------------|-------------------|-----------------------------| +| {kbd}`cmd+c` | {kbd}`ctrl+c` | Copy | +| {kbd}`cmd+v` | {kbd}`ctrl+v` | Paste | +| {kbd}`cmd+z` | {kbd}`ctrl+z` | Undo | +| {kbd}`cmd+enter` | {kbd}`ctrl+enter` | Run a query | +| {kbd}`cmd+/` | {kbd}`ctrl+/` | Comment or uncomment a line | + ::: :::{tab-item} Markdown ```markdown -| Symbol | Key Description | -|-----------|------------------| -| {kbd}`⌘` | Command (Mac) | -| {kbd}`⌥` | Option/Alt (Mac) | -| {kbd}`⇧` | Shift | -| {kbd}`⌃` | Control | -| {kbd}`↩` | Return/Enter | -| {kbd}`⌫` | Delete/Backspace | -| {kbd}`⇥` | Tab | -| {kbd}`↑` | Up Arrow | -| {kbd}`↓` | Down Arrow | -| {kbd}`←` | Left Arrow | -| {kbd}`→` | Right Arrow | -| {kbd}`⎋` | Escape | +| Mac | Windows/Linux | Description | +|------------------|-------------------|-----------------------------| +| {kbd}`cmd+c` | {kbd}`ctrl+c` | Copy | +| {kbd}`cmd+v` | {kbd}`ctrl+v` | Paste | +| {kbd}`cmd+z` | {kbd}`ctrl+z` | Undo | +| {kbd}`cmd+enter` | {kbd}`ctrl+enter` | Run a query | +| {kbd}`cmd+/` | {kbd}`ctrl+/` | Comment or uncomment a line | ``` ::: :::: + +## Available Keys + +The `{kbd}` role recognizes a set of special keywords for modifier, navigation, and function keys. Any other text will be rendered as a literal key. + +Here is the full list of available keywords: + +| Keyword | Rendered Output | +|-------------|------------------| +| `shift` | {kbd}`shift` | +| `ctrl` | {kbd}`ctrl` | +| `alt` | {kbd}`alt` | +| `option` | {kbd}`option` | +| `cmd` | {kbd}`cmd` | +| `win` | {kbd}`win` | +| `up` | {kbd}`up` | +| `down` | {kbd}`down` | +| `left` | {kbd}`left` | +| `right` | {kbd}`right` | +| `space` | {kbd}`space` | +| `tab` | {kbd}`tab` | +| `enter` | {kbd}`enter` | +| `esc` | {kbd}`esc` | +| `backspace` | {kbd}`backspace` | +| `del` | {kbd}`delete` | +| `ins` | {kbd}`insert` | +| `pageup` | {kbd}`pageup` | +| `pagedown` | {kbd}`pagedown` | +| `home` | {kbd}`home` | +| `end` | {kbd}`end` | +| `f1` | {kbd}`f1` | +| `f2` | {kbd}`f2` | +| `f3` | {kbd}`f3` | +| `f4` | {kbd}`f4` | +| `f5` | {kbd}`f5` | +| `f6` | {kbd}`f6` | +| `f7` | {kbd}`f7` | +| `f8` | {kbd}`f8` | +| `f9` | {kbd}`f9` | +| `f10` | {kbd}`f10` | +| `f11` | {kbd}`f11` | +| `f12` | {kbd}`f12` | +| `plus` | {kbd}`plus` | +| `fn` | {kbd}`fn` | diff --git a/src/Elastic.Documentation.Site/Assets/markdown/kbd.css b/src/Elastic.Documentation.Site/Assets/markdown/kbd.css index 5fd11e70a..049fa0d1d 100644 --- a/src/Elastic.Documentation.Site/Assets/markdown/kbd.css +++ b/src/Elastic.Documentation.Site/Assets/markdown/kbd.css @@ -1,5 +1,7 @@ @layer components { - kbd { - @apply bg-grey-20 text-grey-100 border-grey-50 shadow-grey-50 relative top-[-2px] inline-block min-w-[18px] cursor-default rounded-sm border px-1 pt-[3px] pb-[2px] text-center align-middle font-mono text-sm leading-none capitalize shadow-[0_2px_0_1px]; + .markdown-content { + kbd.kbd { + @apply bg-grey-20 text-grey-100 border-grey-50 shadow-grey-50 relative top-[-2px] inline-flex gap-1.5 min-w-[18px] cursor-default rounded-sm border px-1.5 pt-[3px] pb-[2px] text-center align-middle font-mono text-sm leading-none capitalize shadow-[0_2px_0_1px]; + } } } diff --git a/src/Elastic.Markdown/Myst/Roles/Kbd/Kbd.cs b/src/Elastic.Markdown/Myst/Roles/Kbd/Kbd.cs new file mode 100644 index 000000000..159942cd1 --- /dev/null +++ b/src/Elastic.Markdown/Myst/Roles/Kbd/Kbd.cs @@ -0,0 +1,187 @@ +// 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.Collections.Frozen; +using System.ComponentModel.DataAnnotations; +using NetEscapades.EnumGenerators; +using System.Text; +using System.Web; + +namespace Elastic.Markdown.Myst.Roles.Kbd; + +public class KeyboardShortcut(IReadOnlyList keys) +{ + private IReadOnlyList Keys { get; } = keys; + + public static KeyboardShortcut Parse(string input) + { + if (string.IsNullOrWhiteSpace(input)) + return new KeyboardShortcut([]); + + var parts = input.Split('+', StringSplitOptions.RemoveEmptyEntries); + var keys = new List(); + + foreach (var part in parts) + { + var trimmedPart = part.Trim().ToLowerInvariant(); + if (NamedKeyboardKeyExtensions.TryParse(trimmedPart, out var specialKey, true, true)) + keys.Add(new NamedKeyNode { Key = specialKey }); + else + { + switch (trimmedPart.Length) + { + case 1: + keys.Add(new CharacterKeyNode { Key = trimmedPart[0] }); + break; + default: + throw new ArgumentException($"Invalid keyboard shortcut: {input}", nameof(input)); + } + } + } + return new KeyboardShortcut(keys); + } + + public static string Render(KeyboardShortcut shortcut) + { + var viewModels = shortcut.Keys.Select(keyNode => + { + return keyNode switch + { + NamedKeyNode s => ViewModelMapping[s.Key], + CharacterKeyNode c => new KeyboardKeyViewModel { DisplayText = c.Key.ToString(), UnicodeIcon = null }, + _ => throw new ArgumentException($"Unknown key: {keyNode}") + }; + }); + + var kbdElements = viewModels.Select(viewModel => + { + var sb = new StringBuilder(); + _ = sb.Append(""); + if (viewModel.UnicodeIcon is not null) + _ = sb.Append($"{viewModel.UnicodeIcon}"); + _ = sb.Append(viewModel.DisplayText); + _ = sb.Append(""); + return sb.ToString(); + }); + + return string.Join(" + ", kbdElements); + } + + private static FrozenDictionary ViewModelMapping { get; } = + Enum.GetValues().ToFrozenDictionary(k => k, GetDisplayModel); + + private static KeyboardKeyViewModel GetDisplayModel(NamedKeyboardKey key) => + key switch + { + // Modifier keys with special symbols + NamedKeyboardKey.Command => new KeyboardKeyViewModel { DisplayText = "Cmd", UnicodeIcon = "⌘" }, + NamedKeyboardKey.Shift => new KeyboardKeyViewModel { DisplayText = "Shift", UnicodeIcon = "⇧" }, + NamedKeyboardKey.Ctrl => new KeyboardKeyViewModel { DisplayText = "Ctrl", UnicodeIcon = "⌃" }, + NamedKeyboardKey.Alt => new KeyboardKeyViewModel { DisplayText = "Alt", UnicodeIcon = "⌥" }, + NamedKeyboardKey.Option => new KeyboardKeyViewModel { DisplayText = "Option", UnicodeIcon = "⌥" }, + NamedKeyboardKey.Win => new KeyboardKeyViewModel { DisplayText = "Win", UnicodeIcon = "⊞" }, + // Directional keys + NamedKeyboardKey.Up => new KeyboardKeyViewModel { DisplayText = "Up", UnicodeIcon = "↑" }, + NamedKeyboardKey.Down => new KeyboardKeyViewModel { DisplayText = "Down", UnicodeIcon = "↓" }, + NamedKeyboardKey.Left => new KeyboardKeyViewModel { DisplayText = "Left", UnicodeIcon = "←" }, + NamedKeyboardKey.Right => new KeyboardKeyViewModel { DisplayText = "Right", UnicodeIcon = "→" }, + // Other special keys with symbols + NamedKeyboardKey.Enter => new KeyboardKeyViewModel { DisplayText = "Enter", UnicodeIcon = "↵" }, + NamedKeyboardKey.Escape => new KeyboardKeyViewModel { DisplayText = "Esc", UnicodeIcon = "⎋" }, + NamedKeyboardKey.Tab => new KeyboardKeyViewModel { DisplayText = "Tab", UnicodeIcon = "↹" }, + NamedKeyboardKey.Backspace => new KeyboardKeyViewModel { DisplayText = "Backspace", UnicodeIcon = "⌫" }, + NamedKeyboardKey.Delete => new KeyboardKeyViewModel { DisplayText = "Del", UnicodeIcon = null }, + NamedKeyboardKey.Home => new KeyboardKeyViewModel { DisplayText = "Home", UnicodeIcon = "⇱" }, + NamedKeyboardKey.End => new KeyboardKeyViewModel { DisplayText = "End", UnicodeIcon = "⇲" }, + NamedKeyboardKey.PageUp => new KeyboardKeyViewModel { DisplayText = "PageUp", UnicodeIcon = "⇞" }, + NamedKeyboardKey.PageDown => new KeyboardKeyViewModel { DisplayText = "PageDown", UnicodeIcon = "⇟" }, + NamedKeyboardKey.Space => new KeyboardKeyViewModel { DisplayText = "Space", UnicodeIcon = "␣" }, + NamedKeyboardKey.Insert => new KeyboardKeyViewModel { DisplayText = "Ins", UnicodeIcon = null }, + NamedKeyboardKey.Plus => new KeyboardKeyViewModel { DisplayText = "+", UnicodeIcon = null }, + NamedKeyboardKey.Fn => new KeyboardKeyViewModel { DisplayText = "Fn", UnicodeIcon = null }, + NamedKeyboardKey.F1 => new KeyboardKeyViewModel { DisplayText = "F1", UnicodeIcon = null }, + NamedKeyboardKey.F2 => new KeyboardKeyViewModel { DisplayText = "F2", UnicodeIcon = null }, + NamedKeyboardKey.F3 => new KeyboardKeyViewModel { DisplayText = "F3", UnicodeIcon = null }, + NamedKeyboardKey.F4 => new KeyboardKeyViewModel { DisplayText = "F4", UnicodeIcon = null }, + NamedKeyboardKey.F5 => new KeyboardKeyViewModel { DisplayText = "F5", UnicodeIcon = null }, + NamedKeyboardKey.F6 => new KeyboardKeyViewModel { DisplayText = "F6", UnicodeIcon = null }, + NamedKeyboardKey.F7 => new KeyboardKeyViewModel { DisplayText = "F7", UnicodeIcon = null }, + NamedKeyboardKey.F8 => new KeyboardKeyViewModel { DisplayText = "F8", UnicodeIcon = null }, + NamedKeyboardKey.F9 => new KeyboardKeyViewModel { DisplayText = "F9", UnicodeIcon = null }, + NamedKeyboardKey.F10 => new KeyboardKeyViewModel { DisplayText = "F10", UnicodeIcon = null }, + NamedKeyboardKey.F11 => new KeyboardKeyViewModel { DisplayText = "F11", UnicodeIcon = null }, + NamedKeyboardKey.F12 => new KeyboardKeyViewModel { DisplayText = "F12", UnicodeIcon = null }, + // Function keys + _ => throw new ArgumentOutOfRangeException(nameof(key), key, null) + }; +} + +[EnumExtensions] +public enum NamedKeyboardKey +{ + // Modifier Keys + [Display(Name = "shift")] Shift, + [Display(Name = "ctrl")] Ctrl, + [Display(Name = "alt")] Alt, + [Display(Name = "option")] Option, + [Display(Name = "cmd")] Command, + [Display(Name = "win")] Win, + + // Directional Keys + [Display(Name = "up")] Up, + [Display(Name = "down")] Down, + [Display(Name = "left")] Left, + [Display(Name = "right")] Right, + + // Control Keys + [Display(Name = "space")] Space, + [Display(Name = "tab")] Tab, + [Display(Name = "enter")] Enter, + [Display(Name = "esc")] Escape, + [Display(Name = "backspace")] Backspace, + [Display(Name = "del")] Delete, + [Display(Name = "ins")] Insert, + + // Navigation Keys + [Display(Name = "pageup")] PageUp, + [Display(Name = "pagedown")] PageDown, + [Display(Name = "home")] Home, + [Display(Name = "end")] End, + + // Function Keys + [Display(Name = "f1")] F1, + [Display(Name = "f2")] F2, + [Display(Name = "f3")] F3, + [Display(Name = "f4")] F4, + [Display(Name = "f5")] F5, + [Display(Name = "f6")] F6, + [Display(Name = "f7")] F7, + [Display(Name = "f8")] F8, + [Display(Name = "f9")] F9, + [Display(Name = "f10")] F10, + [Display(Name = "f11")] F11, + [Display(Name = "f12")] F12, + + // Other Keys + [Display(Name = "plus")] Plus, + [Display(Name = "fn")] Fn +} + +public class IKeyNode; + +public class NamedKeyNode : IKeyNode +{ + public required NamedKeyboardKey Key { get; init; } +} + +public class CharacterKeyNode : IKeyNode +{ + public required char Key { get; init; } +} + +public record KeyboardKeyViewModel +{ + public required string? UnicodeIcon { get; init; } + public required string DisplayText { get; init; } +} diff --git a/src/Elastic.Markdown/Myst/Roles/Kbd/KbdRole.cs b/src/Elastic.Markdown/Myst/Roles/Kbd/KbdRole.cs index 0da83136d..45b8ea2a0 100644 --- a/src/Elastic.Markdown/Myst/Roles/Kbd/KbdRole.cs +++ b/src/Elastic.Markdown/Myst/Roles/Kbd/KbdRole.cs @@ -9,5 +9,5 @@ namespace Elastic.Markdown.Myst.Roles.Kbd; [DebuggerDisplay("{GetType().Name} Line: {Line}, Role: {Role}, Content: {Content}")] public class KbdRole(string role, string content) : RoleLeaf(role, content) { - public string KeyboardText { get; } = content; + public KeyboardShortcut KeyboardShortcut { get; } = KeyboardShortcut.Parse(content); } diff --git a/src/Elastic.Markdown/Myst/Roles/Kbd/KbdRoleRenderer.cs b/src/Elastic.Markdown/Myst/Roles/Kbd/KbdRoleRenderer.cs index 5b6188aa4..6f4197d13 100644 --- a/src/Elastic.Markdown/Myst/Roles/Kbd/KbdRoleRenderer.cs +++ b/src/Elastic.Markdown/Myst/Roles/Kbd/KbdRoleRenderer.cs @@ -14,9 +14,8 @@ public class KbdRoleHtmlRenderer : HtmlObjectRenderer { protected override void Write(HtmlRenderer renderer, KbdRole role) { - _ = renderer.Write(""); - _ = renderer.Write(role.KeyboardText); - _ = renderer.Write(""); + var output = KeyboardShortcut.Render(role.KeyboardShortcut); + _ = renderer.Write(output); } } diff --git a/tests/authoring/Inline/AppliesToRole.fs b/tests/authoring/Inline/AppliesToRole.fs index 808e8ed85..42cc35a7f 100644 --- a/tests/authoring/Inline/AppliesToRole.fs +++ b/tests/authoring/Inline/AppliesToRole.fs @@ -2,7 +2,7 @@ // Elasticsearch B.V licenses this file to you under the Apache 2.0 License. // See the LICENSE file in the project root for more information -module ``inline elements``.``kbd role`` +module ``inline elements``.``applies_to role`` open Elastic.Markdown.Myst.FrontMatter open Elastic.Markdown.Myst.Roles.AppliesTo @@ -11,13 +11,154 @@ open Xunit open authoring open authoring.MarkdownDocumentAssertions -type ``parses inline {kbd} role`` () = +type ``parses inline {applies_to} role`` () = static let markdown = Setup.Markdown """ -Press {kbd}`Ctrl` + {kbd}`S` to save. + +This is an inline {applies_to}`stack: preview 9.1` element. """ [] - let ``validate HTML`` () = + let ``parses to AppliesDirective`` () = + let directives = markdown |> converts "index.md" |> parses + test <@ directives.Length = 1 @> + directives |> appliesToDirective (ApplicableTo( + Stack=AppliesCollection.op_Explicit "preview 9.1.0" + )) + + [] + let ``validate HTML: generates link and alt attr`` () = markdown |> convertsToHtml """ -

Press Ctrl + S to save.

+

This is an inline + + + Stack + + + Planned + + + + element.

+""" + + +type ``parses nested ess moniker`` () = + static let markdown = Setup.Markdown """ + +This is an inline {applies_to}`ess: preview 9.1` element. +""" + + [] + let ``parses to AppliesDirective`` () = + let directives = markdown |> converts "index.md" |> parses + test <@ directives.Length = 1 @> + directives |> appliesToDirective (ApplicableTo( + Deployment=DeploymentApplicability( + Ess=AppliesCollection.op_Explicit "preview 9.1.0" + ) + )) + +type ``parses {preview} shortcut`` () = + static let markdown = Setup.Markdown """ + +This is an inline {preview}`9.1` element. +""" + + [] + let ``parses to AppliesDirective`` () = + let directives = markdown |> converts "index.md" |> parses + test <@ directives.Length = 1 @> + directives |> appliesToDirective (ApplicableTo( + Product=AppliesCollection.op_Explicit "preview 9.1.0" + )) + + +type ``parses applies to without version in table`` () = + static let markdown = Setup.Markdown """ +| col1 | col2 | +|------|------------------------------| +| test | {applies_to}`ece: removed` | +""" + + [] + let ``parses to AppliesDirective`` () = + let directives = markdown |> converts "index.md" |> parses + test <@ directives.Length = 1 @> + directives |> appliesToDirective (ApplicableTo( + Deployment=DeploymentApplicability( + Ece=AppliesCollection.op_Explicit "removed" + ) + )) + +type ``parses applies to with text afterwards`` () = + static let markdown = Setup.Markdown """ +{applies_to}`ece: removed` hello world +""" + + [] + let ``parses to AppliesDirective`` () = + let directives = markdown |> converts "index.md" |> parses + test <@ directives.Length = 1 @> + directives |> appliesToDirective (ApplicableTo( + Deployment=DeploymentApplicability( + Ece=AppliesCollection.op_Explicit "removed" + ) + )) + +type ``parses multiple applies_to in one line`` () = + static let markdown = Setup.Markdown """ +{applies_to}`ece: removed` {applies_to}`ece: removed` +""" + + [] + let ``parses to AppliesDirective`` () = + let directives = markdown |> converts "index.md" |> parses + test <@ directives.Length = 2 @> + directives |> appliesToDirective (ApplicableTo( + Deployment=DeploymentApplicability( + Ece=AppliesCollection.op_Explicit "removed" + ) + )) + +type ``render 'GA Planned' if preview exists alongside ga`` () = + static let markdown = Setup.Markdown """ + +This is an inline {applies_to}`stack: preview 9.0, ga 9.1` element. +""" + + [] + let ``parses to AppliesDirective`` () = + let directives = markdown |> converts "index.md" |> parses + test <@ directives.Length = 1 @> + directives |> appliesToDirective (ApplicableTo( + Stack=AppliesCollection.op_Explicit "preview 9.0, ga 9.1" + )) + + [] + let ``validate HTML: generates link and alt attr`` () = + markdown |> convertsToHtml """ +

This is an inline + + + Stack + + + Planned + + + + Stack + + + GA planned + + + + element.

""" diff --git a/tests/authoring/Inline/KbdRole.fs b/tests/authoring/Inline/KbdRole.fs index 42cc35a7f..8462736b2 100644 --- a/tests/authoring/Inline/KbdRole.fs +++ b/tests/authoring/Inline/KbdRole.fs @@ -2,163 +2,47 @@ // 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 -module ``inline elements``.``applies_to role`` +module ``inline elements``.``kbd role`` -open Elastic.Markdown.Myst.FrontMatter -open Elastic.Markdown.Myst.Roles.AppliesTo -open Swensen.Unquote open Xunit open authoring -open authoring.MarkdownDocumentAssertions -type ``parses inline {applies_to} role`` () = +type ``renders single kbd role`` () = static let markdown = Setup.Markdown """ - -This is an inline {applies_to}`stack: preview 9.1` element. +{kbd}`cmd` """ - - [] - let ``parses to AppliesDirective`` () = - let directives = markdown |> converts "index.md" |> parses - test <@ directives.Length = 1 @> - directives |> appliesToDirective (ApplicableTo( - Stack=AppliesCollection.op_Explicit "preview 9.1.0" - )) - [] - let ``validate HTML: generates link and alt attr`` () = + let ``validate HTML`` () = markdown |> convertsToHtml """ -

This is an inline - - - Stack - - - Planned - - - - element.

-""" - - -type ``parses nested ess moniker`` () = - static let markdown = Setup.Markdown """ - -This is an inline {applies_to}`ess: preview 9.1` element. +

Cmd

""" - [] - let ``parses to AppliesDirective`` () = - let directives = markdown |> converts "index.md" |> parses - test <@ directives.Length = 1 @> - directives |> appliesToDirective (ApplicableTo( - Deployment=DeploymentApplicability( - Ess=AppliesCollection.op_Explicit "preview 9.1.0" - ) - )) - -type ``parses {preview} shortcut`` () = +type ``renders single character kbd role`` () = static let markdown = Setup.Markdown """ - -This is an inline {preview}`9.1` element. +{kbd}`c` """ - [] - let ``parses to AppliesDirective`` () = - let directives = markdown |> converts "index.md" |> parses - test <@ directives.Length = 1 @> - directives |> appliesToDirective (ApplicableTo( - Product=AppliesCollection.op_Explicit "preview 9.1.0" - )) - - -type ``parses applies to without version in table`` () = - static let markdown = Setup.Markdown """ -| col1 | col2 | -|------|------------------------------| -| test | {applies_to}`ece: removed` | + let ``validate HTML`` () = + markdown |> convertsToHtml """ +

c

""" - [] - let ``parses to AppliesDirective`` () = - let directives = markdown |> converts "index.md" |> parses - test <@ directives.Length = 1 @> - directives |> appliesToDirective (ApplicableTo( - Deployment=DeploymentApplicability( - Ece=AppliesCollection.op_Explicit "removed" - ) - )) - -type ``parses applies to with text afterwards`` () = +type ``renders combined kbd role`` () = static let markdown = Setup.Markdown """ -{applies_to}`ece: removed` hello world +{kbd}`cmd+shift+c` """ - [] - let ``parses to AppliesDirective`` () = - let directives = markdown |> converts "index.md" |> parses - test <@ directives.Length = 1 @> - directives |> appliesToDirective (ApplicableTo( - Deployment=DeploymentApplicability( - Ece=AppliesCollection.op_Explicit "removed" - ) - )) - -type ``parses multiple applies_to in one line`` () = - static let markdown = Setup.Markdown """ -{applies_to}`ece: removed` {applies_to}`ece: removed` + let ``validate HTML`` () = + markdown |> convertsToHtml """ +

Cmd + Shift + c

""" - [] - let ``parses to AppliesDirective`` () = - let directives = markdown |> converts "index.md" |> parses - test <@ directives.Length = 2 @> - directives |> appliesToDirective (ApplicableTo( - Deployment=DeploymentApplicability( - Ece=AppliesCollection.op_Explicit "removed" - ) - )) - -type ``render 'GA Planned' if preview exists alongside ga`` () = +type ``renders combined kbd role with special characters`` () = static let markdown = Setup.Markdown """ - -This is an inline {applies_to}`stack: preview 9.0, ga 9.1` element. +{kbd}`ctrl+alt+del` """ - [] - let ``parses to AppliesDirective`` () = - let directives = markdown |> converts "index.md" |> parses - test <@ directives.Length = 1 @> - directives |> appliesToDirective (ApplicableTo( - Stack=AppliesCollection.op_Explicit "preview 9.0, ga 9.1" - )) - - [] - let ``validate HTML: generates link and alt attr`` () = + let ``validate HTML`` () = markdown |> convertsToHtml """ -

This is an inline - - - Stack - - - Planned - - - - Stack - - - GA planned - - - - element.

+

Ctrl + Alt + Del

""" From 462ae2926bc3ff21c8d4fc80341d881284f2f114 Mon Sep 17 00:00:00 2001 From: Jan Calanog Date: Tue, 8 Jul 2025 22:22:27 +0200 Subject: [PATCH 06/13] Fix formatting --- src/Elastic.Documentation.Site/Assets/markdown/kbd.css | 2 +- src/Elastic.Markdown/Myst/Roles/Kbd/Kbd.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Elastic.Documentation.Site/Assets/markdown/kbd.css b/src/Elastic.Documentation.Site/Assets/markdown/kbd.css index 049fa0d1d..1bdd7942e 100644 --- a/src/Elastic.Documentation.Site/Assets/markdown/kbd.css +++ b/src/Elastic.Documentation.Site/Assets/markdown/kbd.css @@ -1,7 +1,7 @@ @layer components { .markdown-content { kbd.kbd { - @apply bg-grey-20 text-grey-100 border-grey-50 shadow-grey-50 relative top-[-2px] inline-flex gap-1.5 min-w-[18px] cursor-default rounded-sm border px-1.5 pt-[3px] pb-[2px] text-center align-middle font-mono text-sm leading-none capitalize shadow-[0_2px_0_1px]; + @apply bg-grey-20 text-grey-100 border-grey-50 shadow-grey-50 relative top-[-2px] inline-flex min-w-[18px] cursor-default gap-1.5 rounded-sm border px-1.5 pt-[3px] pb-[2px] text-center align-middle font-mono text-sm leading-none capitalize shadow-[0_2px_0_1px]; } } } diff --git a/src/Elastic.Markdown/Myst/Roles/Kbd/Kbd.cs b/src/Elastic.Markdown/Myst/Roles/Kbd/Kbd.cs index 159942cd1..9e7662f84 100644 --- a/src/Elastic.Markdown/Myst/Roles/Kbd/Kbd.cs +++ b/src/Elastic.Markdown/Myst/Roles/Kbd/Kbd.cs @@ -4,9 +4,9 @@ using System.Collections.Frozen; using System.ComponentModel.DataAnnotations; -using NetEscapades.EnumGenerators; using System.Text; using System.Web; +using NetEscapades.EnumGenerators; namespace Elastic.Markdown.Myst.Roles.Kbd; From 1909a666e05451014348d673e70ca9feb074cdb1 Mon Sep 17 00:00:00 2001 From: Jan Calanog Date: Tue, 8 Jul 2025 22:58:10 +0200 Subject: [PATCH 07/13] Emit error when parsing fails --- src/Elastic.Markdown/Myst/Roles/Kbd/Kbd.cs | 2 ++ .../Myst/Roles/Kbd/KbdParser.cs | 5 +++-- src/Elastic.Markdown/Myst/Roles/Kbd/KbdRole.cs | 18 ++++++++++++++++-- 3 files changed, 21 insertions(+), 4 deletions(-) diff --git a/src/Elastic.Markdown/Myst/Roles/Kbd/Kbd.cs b/src/Elastic.Markdown/Myst/Roles/Kbd/Kbd.cs index 9e7662f84..d2e296da8 100644 --- a/src/Elastic.Markdown/Myst/Roles/Kbd/Kbd.cs +++ b/src/Elastic.Markdown/Myst/Roles/Kbd/Kbd.cs @@ -14,6 +14,8 @@ public class KeyboardShortcut(IReadOnlyList keys) { private IReadOnlyList Keys { get; } = keys; + public static KeyboardShortcut Empty { get; } = new([]); + public static KeyboardShortcut Parse(string input) { if (string.IsNullOrWhiteSpace(input)) diff --git a/src/Elastic.Markdown/Myst/Roles/Kbd/KbdParser.cs b/src/Elastic.Markdown/Myst/Roles/Kbd/KbdParser.cs index 606ff83f4..4cd459946 100644 --- a/src/Elastic.Markdown/Myst/Roles/Kbd/KbdParser.cs +++ b/src/Elastic.Markdown/Myst/Roles/Kbd/KbdParser.cs @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information using System; +using Elastic.Markdown.Diagnostics; using Markdig.Parsers; namespace Elastic.Markdown.Myst.Roles.Kbd; @@ -10,8 +11,8 @@ namespace Elastic.Markdown.Myst.Roles.Kbd; public class KbdParser : RoleParser { - protected override KbdRole CreateRole(string role, string content, InlineProcessor parserContext) => - new(role, content); + protected override KbdRole CreateRole(string role, string content, InlineProcessor parserContext) + => new(role, content, parserContext); protected override bool Matches(ReadOnlySpan role) => role is "{kbd}"; } diff --git a/src/Elastic.Markdown/Myst/Roles/Kbd/KbdRole.cs b/src/Elastic.Markdown/Myst/Roles/Kbd/KbdRole.cs index 45b8ea2a0..3a75f940b 100644 --- a/src/Elastic.Markdown/Myst/Roles/Kbd/KbdRole.cs +++ b/src/Elastic.Markdown/Myst/Roles/Kbd/KbdRole.cs @@ -3,11 +3,25 @@ // See the LICENSE file in the project root for more information using System.Diagnostics; +using Elastic.Markdown.Diagnostics; +using Markdig.Parsers; namespace Elastic.Markdown.Myst.Roles.Kbd; [DebuggerDisplay("{GetType().Name} Line: {Line}, Role: {Role}, Content: {Content}")] -public class KbdRole(string role, string content) : RoleLeaf(role, content) +public class KbdRole : RoleLeaf { - public KeyboardShortcut KeyboardShortcut { get; } = KeyboardShortcut.Parse(content); + public KbdRole(string role, string content, InlineProcessor parserContext) : base(role, content) + { + try + { + KeyboardShortcut = KeyboardShortcut.Parse(content); + } + catch (Exception ex) + { + parserContext.EmitError(this, Role.Length + content.Length, $"Failed to parse keyboard shortcut: \"{content}\"", ex); + KeyboardShortcut = KeyboardShortcut.Empty; + } + } + public KeyboardShortcut KeyboardShortcut { get; } } From 7f000bf740caaad912514dd8c6b76eaf93d00ed2 Mon Sep 17 00:00:00 2001 From: Jan Calanog Date: Tue, 8 Jul 2025 23:01:08 +0200 Subject: [PATCH 08/13] Use unknown key on error --- src/Elastic.Markdown/Myst/Roles/Kbd/Kbd.cs | 2 +- src/Elastic.Markdown/Myst/Roles/Kbd/KbdRole.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Elastic.Markdown/Myst/Roles/Kbd/Kbd.cs b/src/Elastic.Markdown/Myst/Roles/Kbd/Kbd.cs index d2e296da8..2da4de096 100644 --- a/src/Elastic.Markdown/Myst/Roles/Kbd/Kbd.cs +++ b/src/Elastic.Markdown/Myst/Roles/Kbd/Kbd.cs @@ -14,7 +14,7 @@ public class KeyboardShortcut(IReadOnlyList keys) { private IReadOnlyList Keys { get; } = keys; - public static KeyboardShortcut Empty { get; } = new([]); + public static KeyboardShortcut Unknown { get; } = new([new CharacterKeyNode { Key = '?' }]); public static KeyboardShortcut Parse(string input) { diff --git a/src/Elastic.Markdown/Myst/Roles/Kbd/KbdRole.cs b/src/Elastic.Markdown/Myst/Roles/Kbd/KbdRole.cs index 3a75f940b..36e3dedcf 100644 --- a/src/Elastic.Markdown/Myst/Roles/Kbd/KbdRole.cs +++ b/src/Elastic.Markdown/Myst/Roles/Kbd/KbdRole.cs @@ -20,7 +20,7 @@ public KbdRole(string role, string content, InlineProcessor parserContext) : bas catch (Exception ex) { parserContext.EmitError(this, Role.Length + content.Length, $"Failed to parse keyboard shortcut: \"{content}\"", ex); - KeyboardShortcut = KeyboardShortcut.Empty; + KeyboardShortcut = KeyboardShortcut.Unknown; } } public KeyboardShortcut KeyboardShortcut { get; } From b65fcbcd82ea0a75bbc9234a7669f2d3a302e445 Mon Sep 17 00:00:00 2001 From: Jan Calanog Date: Tue, 8 Jul 2025 23:25:26 +0200 Subject: [PATCH 09/13] Add aria-label --- src/Elastic.Markdown/Myst/Roles/Kbd/Kbd.cs | 36 ++++++++++++---------- tests/authoring/Inline/KbdRole.fs | 6 ++-- 2 files changed, 23 insertions(+), 19 deletions(-) diff --git a/src/Elastic.Markdown/Myst/Roles/Kbd/Kbd.cs b/src/Elastic.Markdown/Myst/Roles/Kbd/Kbd.cs index 2da4de096..985e0b6f4 100644 --- a/src/Elastic.Markdown/Myst/Roles/Kbd/Kbd.cs +++ b/src/Elastic.Markdown/Myst/Roles/Kbd/Kbd.cs @@ -59,7 +59,10 @@ public static string Render(KeyboardShortcut shortcut) var kbdElements = viewModels.Select(viewModel => { var sb = new StringBuilder(); - _ = sb.Append(""); + _ = sb.Append("'); if (viewModel.UnicodeIcon is not null) _ = sb.Append($"{viewModel.UnicodeIcon}"); _ = sb.Append(viewModel.DisplayText); @@ -77,31 +80,31 @@ private static KeyboardKeyViewModel GetDisplayModel(NamedKeyboardKey key) => key switch { // Modifier keys with special symbols - NamedKeyboardKey.Command => new KeyboardKeyViewModel { DisplayText = "Cmd", UnicodeIcon = "⌘" }, + NamedKeyboardKey.Command => new KeyboardKeyViewModel { DisplayText = "Cmd", UnicodeIcon = "⌘", AriaLabel = "Command" }, NamedKeyboardKey.Shift => new KeyboardKeyViewModel { DisplayText = "Shift", UnicodeIcon = "⇧" }, - NamedKeyboardKey.Ctrl => new KeyboardKeyViewModel { DisplayText = "Ctrl", UnicodeIcon = "⌃" }, + NamedKeyboardKey.Ctrl => new KeyboardKeyViewModel { DisplayText = "Ctrl", UnicodeIcon = "⌃", AriaLabel = "Control" }, NamedKeyboardKey.Alt => new KeyboardKeyViewModel { DisplayText = "Alt", UnicodeIcon = "⌥" }, - NamedKeyboardKey.Option => new KeyboardKeyViewModel { DisplayText = "Option", UnicodeIcon = "⌥" }, - NamedKeyboardKey.Win => new KeyboardKeyViewModel { DisplayText = "Win", UnicodeIcon = "⊞" }, + NamedKeyboardKey.Option => new KeyboardKeyViewModel { DisplayText = "Opt", UnicodeIcon = "⌥", AriaLabel = "Option" }, + NamedKeyboardKey.Win => new KeyboardKeyViewModel { DisplayText = "Win", UnicodeIcon = "⊞", AriaLabel = "Windows" }, // Directional keys - NamedKeyboardKey.Up => new KeyboardKeyViewModel { DisplayText = "Up", UnicodeIcon = "↑" }, - NamedKeyboardKey.Down => new KeyboardKeyViewModel { DisplayText = "Down", UnicodeIcon = "↓" }, - NamedKeyboardKey.Left => new KeyboardKeyViewModel { DisplayText = "Left", UnicodeIcon = "←" }, - NamedKeyboardKey.Right => new KeyboardKeyViewModel { DisplayText = "Right", UnicodeIcon = "→" }, + NamedKeyboardKey.Up => new KeyboardKeyViewModel { DisplayText = "Up", UnicodeIcon = "↑", AriaLabel = "Up Arrow" }, + NamedKeyboardKey.Down => new KeyboardKeyViewModel { DisplayText = "Down", UnicodeIcon = "↓", AriaLabel = "Down Arrow" }, + NamedKeyboardKey.Left => new KeyboardKeyViewModel { DisplayText = "Left", UnicodeIcon = "←", AriaLabel = "Left Arrow" }, + NamedKeyboardKey.Right => new KeyboardKeyViewModel { DisplayText = "Right", UnicodeIcon = "→", AriaLabel = "Right Arrow" }, // Other special keys with symbols NamedKeyboardKey.Enter => new KeyboardKeyViewModel { DisplayText = "Enter", UnicodeIcon = "↵" }, - NamedKeyboardKey.Escape => new KeyboardKeyViewModel { DisplayText = "Esc", UnicodeIcon = "⎋" }, - NamedKeyboardKey.Tab => new KeyboardKeyViewModel { DisplayText = "Tab", UnicodeIcon = "↹" }, + NamedKeyboardKey.Escape => new KeyboardKeyViewModel { DisplayText = "Esc", UnicodeIcon = "⎋", AriaLabel = "Escape" }, + NamedKeyboardKey.Tab => new KeyboardKeyViewModel { DisplayText = "Tab", UnicodeIcon = "↹", AriaLabel = "Tab" }, NamedKeyboardKey.Backspace => new KeyboardKeyViewModel { DisplayText = "Backspace", UnicodeIcon = "⌫" }, - NamedKeyboardKey.Delete => new KeyboardKeyViewModel { DisplayText = "Del", UnicodeIcon = null }, + NamedKeyboardKey.Delete => new KeyboardKeyViewModel { DisplayText = "Del", UnicodeIcon = null, AriaLabel = "Delete" }, NamedKeyboardKey.Home => new KeyboardKeyViewModel { DisplayText = "Home", UnicodeIcon = "⇱" }, NamedKeyboardKey.End => new KeyboardKeyViewModel { DisplayText = "End", UnicodeIcon = "⇲" }, - NamedKeyboardKey.PageUp => new KeyboardKeyViewModel { DisplayText = "PageUp", UnicodeIcon = "⇞" }, - NamedKeyboardKey.PageDown => new KeyboardKeyViewModel { DisplayText = "PageDown", UnicodeIcon = "⇟" }, + NamedKeyboardKey.PageUp => new KeyboardKeyViewModel { DisplayText = "PageUp", UnicodeIcon = "⇞", AriaLabel = "Page Up" }, + NamedKeyboardKey.PageDown => new KeyboardKeyViewModel { DisplayText = "PageDown", UnicodeIcon = "⇟", AriaLabel = "Page Down" }, NamedKeyboardKey.Space => new KeyboardKeyViewModel { DisplayText = "Space", UnicodeIcon = "␣" }, - NamedKeyboardKey.Insert => new KeyboardKeyViewModel { DisplayText = "Ins", UnicodeIcon = null }, + NamedKeyboardKey.Insert => new KeyboardKeyViewModel { DisplayText = "Ins", UnicodeIcon = null, AriaLabel = "Insert" }, NamedKeyboardKey.Plus => new KeyboardKeyViewModel { DisplayText = "+", UnicodeIcon = null }, - NamedKeyboardKey.Fn => new KeyboardKeyViewModel { DisplayText = "Fn", UnicodeIcon = null }, + NamedKeyboardKey.Fn => new KeyboardKeyViewModel { DisplayText = "Fn", UnicodeIcon = null, AriaLabel = "Fn" }, NamedKeyboardKey.F1 => new KeyboardKeyViewModel { DisplayText = "F1", UnicodeIcon = null }, NamedKeyboardKey.F2 => new KeyboardKeyViewModel { DisplayText = "F2", UnicodeIcon = null }, NamedKeyboardKey.F3 => new KeyboardKeyViewModel { DisplayText = "F3", UnicodeIcon = null }, @@ -186,4 +189,5 @@ public record KeyboardKeyViewModel { public required string? UnicodeIcon { get; init; } public required string DisplayText { get; init; } + public string? AriaLabel { get; init; } } diff --git a/tests/authoring/Inline/KbdRole.fs b/tests/authoring/Inline/KbdRole.fs index 8462736b2..9b507c44f 100644 --- a/tests/authoring/Inline/KbdRole.fs +++ b/tests/authoring/Inline/KbdRole.fs @@ -14,7 +14,7 @@ type ``renders single kbd role`` () = [] let ``validate HTML`` () = markdown |> convertsToHtml """ -

Cmd

+

Cmd

""" type ``renders single character kbd role`` () = @@ -34,7 +34,7 @@ type ``renders combined kbd role`` () = [] let ``validate HTML`` () = markdown |> convertsToHtml """ -

Cmd + Shift + c

+

Cmd + Shift + c

""" type ``renders combined kbd role with special characters`` () = @@ -44,5 +44,5 @@ type ``renders combined kbd role with special characters`` () = [] let ``validate HTML`` () = markdown |> convertsToHtml """ -

Ctrl + Alt + Del

+

Ctrl + Alt + Del

""" From 4d4690a6ea8d7ebb5a382985d721e7939ad37979 Mon Sep 17 00:00:00 2001 From: Jan Calanog Date: Tue, 8 Jul 2025 23:27:55 +0200 Subject: [PATCH 10/13] Fix heading casing --- docs/syntax/kbd.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/syntax/kbd.md b/docs/syntax/kbd.md index 83539cb05..4a879777d 100644 --- a/docs/syntax/kbd.md +++ b/docs/syntax/kbd.md @@ -101,7 +101,7 @@ The platform-specific examples below demonstrate how to combine special keys and :::: -## Available Keys +## Available keys The `{kbd}` role recognizes a set of special keywords for modifier, navigation, and function keys. Any other text will be rendered as a literal key. From c3cb493fc833a2c408dd211af968a512534e8145 Mon Sep 17 00:00:00 2001 From: Jan Calanog Date: Wed, 9 Jul 2025 12:04:57 +0200 Subject: [PATCH 11/13] Add ability to show alternate keys with '|' --- docs/syntax/kbd.md | 103 +++-- .../Assets/markdown/kbd.css | 13 +- src/Elastic.Markdown/Myst/Roles/Kbd/Kbd.cs | 388 ++++++++++++++---- tests/authoring/Inline/KbdRole.fs | 30 ++ 4 files changed, 397 insertions(+), 137 deletions(-) diff --git a/docs/syntax/kbd.md b/docs/syntax/kbd.md index 4a879777d..a23f7669b 100644 --- a/docs/syntax/kbd.md +++ b/docs/syntax/kbd.md @@ -1,6 +1,6 @@ # Keyboard shortcuts -You can represent keyboard keys and shortcuts in your documentation using the `{kbd}` role. This is useful for showing keyboard commands and shortcuts in a visually consistent way. +You can represent keyboard keys and shortcuts in your documentation using the `{kbd}` role. This is useful for showing keyboard commands and shortcuts in a visually consistent way. See the full list of [available keys](#available-keys). ## Basic usage @@ -22,7 +22,7 @@ Press {kbd}`enter` to submit. ## Combining keys -For keyboard shortcuts involving multiple keys, you can combine them within a single `{kbd}` role by separating the key names with a `+`. +For keyboard shortcuts involving multiple keys, you can combine them within a single `{kbd}` role by separating the key names with a `+`. Keys are always visually separated, even when using the combined syntax. ::::{tab-set} @@ -38,36 +38,30 @@ Use {kbd}`cmd+shift+enter` to execute the command. :::: -Alternatively, you can use multiple `{kbd}` roles to describe a shortcut. This approach is useful when you want to visually separate keys. Use a `+` to represent a combination and a `/` to represent alternative keys. +## Alternative keys + +To display alternative keys for a shortcut, use `|` to separate the alternate keys within the same `{kbd}` role. This is useful for showing platform-specific shortcuts, such as `ctrl` on Windows and `cmd` on macOS. ::::{tab-set} :::{tab-item} Output -{kbd}`ctrl` + {kbd}`c` to copy text, or {kbd}`cmd` + {kbd}`c` on Mac. +Use {kbd}`ctrl|cmd + c` to copy text. ::: :::{tab-item} Markdown ```markdown -{kbd}`ctrl` + {kbd}`c` to copy text, or {kbd}`cmd` + {kbd}`c` on Mac. +Use {kbd}`ctrl|cmd + c` to copy text. ``` ::: :::: -::::{tab-set} - -:::{tab-item} Output -{kbd}`ctrl` / {kbd}`cmd` + {kbd}`c` to copy text. -::: +## Reserved characters +The `+` and `|` characters have special meaning for combining keys and specifying alternatives. To render them as literal keys, you must use their keyword equivalents. -:::{tab-item} Markdown -```markdown -{kbd}`ctrl` / {kbd}`cmd` + {kbd}`c` to copy text. -``` -::: - -:::: +- To display the {kbd}`plus` key, use `` `{kbd}`plus` ``. +- To display the {kbd}`pipe` key, use `` `{kbd}`pipe` ``. ## Common shortcuts by platform @@ -107,40 +101,41 @@ The `{kbd}` role recognizes a set of special keywords for modifier, navigation, Here is the full list of available keywords: -| Keyword | Rendered Output | -|-------------|------------------| -| `shift` | {kbd}`shift` | -| `ctrl` | {kbd}`ctrl` | -| `alt` | {kbd}`alt` | -| `option` | {kbd}`option` | -| `cmd` | {kbd}`cmd` | -| `win` | {kbd}`win` | -| `up` | {kbd}`up` | -| `down` | {kbd}`down` | -| `left` | {kbd}`left` | -| `right` | {kbd}`right` | -| `space` | {kbd}`space` | -| `tab` | {kbd}`tab` | -| `enter` | {kbd}`enter` | -| `esc` | {kbd}`esc` | -| `backspace` | {kbd}`backspace` | -| `del` | {kbd}`delete` | -| `ins` | {kbd}`insert` | -| `pageup` | {kbd}`pageup` | -| `pagedown` | {kbd}`pagedown` | -| `home` | {kbd}`home` | -| `end` | {kbd}`end` | -| `f1` | {kbd}`f1` | -| `f2` | {kbd}`f2` | -| `f3` | {kbd}`f3` | -| `f4` | {kbd}`f4` | -| `f5` | {kbd}`f5` | -| `f6` | {kbd}`f6` | -| `f7` | {kbd}`f7` | -| `f8` | {kbd}`f8` | -| `f9` | {kbd}`f9` | -| `f10` | {kbd}`f10` | -| `f11` | {kbd}`f11` | -| `f12` | {kbd}`f12` | -| `plus` | {kbd}`plus` | -| `fn` | {kbd}`fn` | +| Syntax | Rendered Output | +|-------------------------|------------------| +| `` {kbd}`shift` `` | {kbd}`shift` | +| `` {kbd}`ctrl` `` | {kbd}`ctrl` | +| `` {kbd}`alt` `` | {kbd}`alt` | +| `` {kbd}`option` `` | {kbd}`option` | +| `` {kbd}`cmd` `` | {kbd}`cmd` | +| `` {kbd}`win` `` | {kbd}`win` | +| `` {kbd}`up` `` | {kbd}`up` | +| `` {kbd}`down` `` | {kbd}`down` | +| `` {kbd}`left` `` | {kbd}`left` | +| `` {kbd}`right` `` | {kbd}`right` | +| `` {kbd}`space` `` | {kbd}`space` | +| `` {kbd}`tab` `` | {kbd}`tab` | +| `` {kbd}`enter` `` | {kbd}`enter` | +| `` {kbd}`esc` `` | {kbd}`esc` | +| `` {kbd}`backspace` `` | {kbd}`backspace` | +| `` {kbd}`del` `` | {kbd}`delete` | +| `` {kbd}`ins` `` | {kbd}`insert` | +| `` {kbd}`pageup` `` | {kbd}`pageup` | +| `` {kbd}`pagedown` `` | {kbd}`pagedown` | +| `` {kbd}`home` `` | {kbd}`home` | +| `` {kbd}`end` `` | {kbd}`end` | +| `` {kbd}`f1` `` | {kbd}`f1` | +| `` {kbd}`f2` `` | {kbd}`f2` | +| `` {kbd}`f3` `` | {kbd}`f3` | +| `` {kbd}`f4` `` | {kbd}`f4` | +| `` {kbd}`f5` `` | {kbd}`f5` | +| `` {kbd}`f6` `` | {kbd}`f6` | +| `` {kbd}`f7` `` | {kbd}`f7` | +| `` {kbd}`f8` `` | {kbd}`f8` | +| `` {kbd}`f9` `` | {kbd}`f9` | +| `` {kbd}`f10` `` | {kbd}`f10` | +| `` {kbd}`f11` `` | {kbd}`f11` | +| `` {kbd}`f12` `` | {kbd}`f12` | +| `` {kbd}`plus` `` | {kbd}`plus` | +| `` {kbd}`fn` `` | {kbd}`fn` | +| `` {kbd}`pipe` `` | {kbd}`pipe` | diff --git a/src/Elastic.Documentation.Site/Assets/markdown/kbd.css b/src/Elastic.Documentation.Site/Assets/markdown/kbd.css index 1bdd7942e..2544c142f 100644 --- a/src/Elastic.Documentation.Site/Assets/markdown/kbd.css +++ b/src/Elastic.Documentation.Site/Assets/markdown/kbd.css @@ -1,7 +1,18 @@ @layer components { .markdown-content { kbd.kbd { - @apply bg-grey-20 text-grey-100 border-grey-50 shadow-grey-50 relative top-[-2px] inline-flex min-w-[18px] cursor-default gap-1.5 rounded-sm border px-1.5 pt-[3px] pb-[2px] text-center align-middle font-mono text-sm leading-none capitalize shadow-[0_2px_0_1px]; + @apply relative bg-grey-20 text-grey-100 border-grey-50 shadow-grey-50 top-[-2px] inline-flex items-center min-w-[18px] cursor-default gap-1.5 rounded-sm border px-1.5 pt-[3px] pb-[2px] text-center align-middle font-mono text-sm leading-none capitalize shadow-[0_2px_0_1px]; + } + + kbd.kbd .kbd-separator { + @apply bg-grey-100 inline-block mx-1 self-stretch; + width: 1px; + /*height: .8em;*/ + transform: translateY(-1px) rotate(30deg); + } + + kbd.kbd .kbd-space { + @apply w-0; } } } diff --git a/src/Elastic.Markdown/Myst/Roles/Kbd/Kbd.cs b/src/Elastic.Markdown/Myst/Roles/Kbd/Kbd.cs index 985e0b6f4..73bb5e090 100644 --- a/src/Elastic.Markdown/Myst/Roles/Kbd/Kbd.cs +++ b/src/Elastic.Markdown/Myst/Roles/Kbd/Kbd.cs @@ -14,109 +14,318 @@ public class KeyboardShortcut(IReadOnlyList keys) { private IReadOnlyList Keys { get; } = keys; - public static KeyboardShortcut Unknown { get; } = new([new CharacterKeyNode { Key = '?' }]); + public static KeyboardShortcut Unknown { get; } = new([ + new CharacterKeyNode + { + Key = '?' + } + ]); public static KeyboardShortcut Parse(string input) { if (string.IsNullOrWhiteSpace(input)) return new KeyboardShortcut([]); - var parts = input.Split('+', StringSplitOptions.RemoveEmptyEntries); - var keys = new List(); - - foreach (var part in parts) - { - var trimmedPart = part.Trim().ToLowerInvariant(); - if (NamedKeyboardKeyExtensions.TryParse(trimmedPart, out var specialKey, true, true)) - keys.Add(new NamedKeyNode { Key = specialKey }); - else - { - switch (trimmedPart.Length) - { - case 1: - keys.Add(new CharacterKeyNode { Key = trimmedPart[0] }); - break; - default: - throw new ArgumentException($"Invalid keyboard shortcut: {input}", nameof(input)); - } - } - } + var keySegments = input.Split('+', StringSplitOptions.RemoveEmptyEntries); + var keys = keySegments.Select(ParseKey).ToList(); return new KeyboardShortcut(keys); } + private static IKeyNode ParseKey(string keySegment) + { + var trimmedSegment = keySegment.Trim(); + var alternateParts = trimmedSegment.Split('|', StringSplitOptions.RemoveEmptyEntries); + + if (alternateParts.Length > 2) + throw new ArgumentException($"You can only use two alternate keyboard keys: {keySegment}", nameof(keySegment)); + + return alternateParts.Length == 2 + ? new AlternateKeyNode { Primary = ParseSingleKey(alternateParts[0]), Alternate = ParseSingleKey(alternateParts[1]) } + : ParseSingleKey(trimmedSegment); + } + + private static IKeyNode ParseSingleKey(string key) + { + var trimmedKey = key.Trim().ToLowerInvariant(); + if (NamedKeyboardKeyExtensions.TryParse(trimmedKey, out var namedKey, true, true)) + return new NamedKeyNode { Key = namedKey }; + + if (trimmedKey.Length == 1) + return new CharacterKeyNode { Key = trimmedKey[0] }; + + throw new ArgumentException($"Unknown keyboard key: {key}", nameof(key)); + } + public static string Render(KeyboardShortcut shortcut) { - var viewModels = shortcut.Keys.Select(keyNode => + var viewModels = shortcut.Keys.Select(ToViewModel); + var kbdElements = viewModels.Select(viewModel => viewModel switch { - return keyNode switch - { - NamedKeyNode s => ViewModelMapping[s.Key], - CharacterKeyNode c => new KeyboardKeyViewModel { DisplayText = c.Key.ToString(), UnicodeIcon = null }, - _ => throw new ArgumentException($"Unknown key: {keyNode}") - }; + SingleKeyboardKeyViewModel singleKeyboardKeyViewModel => Render(singleKeyboardKeyViewModel), + AlternateKeyboardKeyViewModel alternateKeyboardKeyViewModel => Render(alternateKeyboardKeyViewModel), + _ => throw new ArgumentException($"Unsupported key: {viewModel}", nameof(viewModel)) }); + return string.Join(" + ", kbdElements); + } - var kbdElements = viewModels.Select(viewModel => - { - var sb = new StringBuilder(); - _ = sb.Append("'); - if (viewModel.UnicodeIcon is not null) - _ = sb.Append($"{viewModel.UnicodeIcon}"); - _ = sb.Append(viewModel.DisplayText); - _ = sb.Append(""); - return sb.ToString(); - }); + private static string Render(AlternateKeyboardKeyViewModel alternateKeyboardKeyViewModel) + { + var sb = new StringBuilder(); + _ = sb.Append("'); - return string.Join(" + ", kbdElements); + if (alternateKeyboardKeyViewModel.Primary.UnicodeIcon is not null) + _ = sb.Append($"{alternateKeyboardKeyViewModel.Primary.UnicodeIcon}"); + _ = sb.Append(alternateKeyboardKeyViewModel.Primary.DisplayText); + + _ = sb.Append(""); + + if (alternateKeyboardKeyViewModel.Alternate.UnicodeIcon is not null) + _ = sb.Append($"{alternateKeyboardKeyViewModel.Alternate.UnicodeIcon}"); + _ = sb.Append(alternateKeyboardKeyViewModel.Alternate.DisplayText); + _ = sb.Append(""); + return sb.ToString(); + } + + private static string Render(SingleKeyboardKeyViewModel singleKeyboardKeyViewModel) + { + var sb = new StringBuilder(); + _ = sb.Append("'); + if (singleKeyboardKeyViewModel.UnicodeIcon is not null) + _ = sb.Append($"{singleKeyboardKeyViewModel.UnicodeIcon}"); + _ = sb.Append(singleKeyboardKeyViewModel.DisplayText); + _ = sb.Append(""); + return sb.ToString(); } - private static FrozenDictionary ViewModelMapping { get; } = + private static IKeyboardViewModel ToViewModel(IKeyNode keyNode) => + keyNode switch + { + AlternateKeyNode alternateKeyNode => ToViewModel(alternateKeyNode), + CharacterKeyNode characterKeyNode => ToViewModel(characterKeyNode), + NamedKeyNode namedKeyNode => ToViewModel(namedKeyNode), + _ => throw new ArgumentException($"Unknown key: {keyNode}") + }; + + private static AlternateKeyboardKeyViewModel ToViewModel(AlternateKeyNode keyNode) => + new() + { + Primary = keyNode.Primary switch + { + NamedKeyNode namedKeyNode => ToViewModel(namedKeyNode), + CharacterKeyNode characterKeyNode => ToViewModel(characterKeyNode), + _ => throw new ArgumentException($"Unsupported key: {keyNode.Primary}") + }, + Alternate = keyNode.Alternate switch + { + NamedKeyNode namedKeyNode => ToViewModel(namedKeyNode), + CharacterKeyNode characterKeyNode => ToViewModel(characterKeyNode), + _ => throw new ArgumentException($"Unsupported key: {keyNode.Primary}") + }, + }; + + private static SingleKeyboardKeyViewModel ToViewModel(CharacterKeyNode keyNode) => new() + { + DisplayText = HttpUtility.HtmlEncode(keyNode.Key.ToString()), + UnicodeIcon = null + }; + + private static SingleKeyboardKeyViewModel ToViewModel(NamedKeyNode keyNode) => ViewModelMapping[keyNode.Key]; + + private static FrozenDictionary ViewModelMapping { get; } = Enum.GetValues().ToFrozenDictionary(k => k, GetDisplayModel); - private static KeyboardKeyViewModel GetDisplayModel(NamedKeyboardKey key) => + private static SingleKeyboardKeyViewModel GetDisplayModel(NamedKeyboardKey key) => key switch { // Modifier keys with special symbols - NamedKeyboardKey.Command => new KeyboardKeyViewModel { DisplayText = "Cmd", UnicodeIcon = "⌘", AriaLabel = "Command" }, - NamedKeyboardKey.Shift => new KeyboardKeyViewModel { DisplayText = "Shift", UnicodeIcon = "⇧" }, - NamedKeyboardKey.Ctrl => new KeyboardKeyViewModel { DisplayText = "Ctrl", UnicodeIcon = "⌃", AriaLabel = "Control" }, - NamedKeyboardKey.Alt => new KeyboardKeyViewModel { DisplayText = "Alt", UnicodeIcon = "⌥" }, - NamedKeyboardKey.Option => new KeyboardKeyViewModel { DisplayText = "Opt", UnicodeIcon = "⌥", AriaLabel = "Option" }, - NamedKeyboardKey.Win => new KeyboardKeyViewModel { DisplayText = "Win", UnicodeIcon = "⊞", AriaLabel = "Windows" }, + NamedKeyboardKey.Command => new SingleKeyboardKeyViewModel + { + DisplayText = "Cmd", + UnicodeIcon = "⌘", + AriaLabel = "Command" + }, + NamedKeyboardKey.Shift => new SingleKeyboardKeyViewModel + { + DisplayText = "Shift", + UnicodeIcon = "⇧" + }, + NamedKeyboardKey.Ctrl => new SingleKeyboardKeyViewModel + { + DisplayText = "Ctrl", + UnicodeIcon = "⌃", + AriaLabel = "Control" + }, + NamedKeyboardKey.Alt => new SingleKeyboardKeyViewModel + { + DisplayText = "Alt", + UnicodeIcon = "⌥" + }, + NamedKeyboardKey.Option => new SingleKeyboardKeyViewModel + { + DisplayText = "Opt", + UnicodeIcon = "⌥", + AriaLabel = "Option" + }, + NamedKeyboardKey.Win => new SingleKeyboardKeyViewModel + { + DisplayText = "Win", + UnicodeIcon = "⊞", + AriaLabel = "Windows" + }, // Directional keys - NamedKeyboardKey.Up => new KeyboardKeyViewModel { DisplayText = "Up", UnicodeIcon = "↑", AriaLabel = "Up Arrow" }, - NamedKeyboardKey.Down => new KeyboardKeyViewModel { DisplayText = "Down", UnicodeIcon = "↓", AriaLabel = "Down Arrow" }, - NamedKeyboardKey.Left => new KeyboardKeyViewModel { DisplayText = "Left", UnicodeIcon = "←", AriaLabel = "Left Arrow" }, - NamedKeyboardKey.Right => new KeyboardKeyViewModel { DisplayText = "Right", UnicodeIcon = "→", AriaLabel = "Right Arrow" }, + NamedKeyboardKey.Up => new SingleKeyboardKeyViewModel + { + DisplayText = "Up", + UnicodeIcon = "↑", + AriaLabel = "Up Arrow" + }, + NamedKeyboardKey.Down => new SingleKeyboardKeyViewModel + { + DisplayText = "Down", + UnicodeIcon = "↓", + AriaLabel = "Down Arrow" + }, + NamedKeyboardKey.Left => new SingleKeyboardKeyViewModel + { + DisplayText = "Left", + UnicodeIcon = "←", + AriaLabel = "Left Arrow" + }, + NamedKeyboardKey.Right => new SingleKeyboardKeyViewModel + { + DisplayText = "Right", + UnicodeIcon = "→", + AriaLabel = "Right Arrow" + }, // Other special keys with symbols - NamedKeyboardKey.Enter => new KeyboardKeyViewModel { DisplayText = "Enter", UnicodeIcon = "↵" }, - NamedKeyboardKey.Escape => new KeyboardKeyViewModel { DisplayText = "Esc", UnicodeIcon = "⎋", AriaLabel = "Escape" }, - NamedKeyboardKey.Tab => new KeyboardKeyViewModel { DisplayText = "Tab", UnicodeIcon = "↹", AriaLabel = "Tab" }, - NamedKeyboardKey.Backspace => new KeyboardKeyViewModel { DisplayText = "Backspace", UnicodeIcon = "⌫" }, - NamedKeyboardKey.Delete => new KeyboardKeyViewModel { DisplayText = "Del", UnicodeIcon = null, AriaLabel = "Delete" }, - NamedKeyboardKey.Home => new KeyboardKeyViewModel { DisplayText = "Home", UnicodeIcon = "⇱" }, - NamedKeyboardKey.End => new KeyboardKeyViewModel { DisplayText = "End", UnicodeIcon = "⇲" }, - NamedKeyboardKey.PageUp => new KeyboardKeyViewModel { DisplayText = "PageUp", UnicodeIcon = "⇞", AriaLabel = "Page Up" }, - NamedKeyboardKey.PageDown => new KeyboardKeyViewModel { DisplayText = "PageDown", UnicodeIcon = "⇟", AriaLabel = "Page Down" }, - NamedKeyboardKey.Space => new KeyboardKeyViewModel { DisplayText = "Space", UnicodeIcon = "␣" }, - NamedKeyboardKey.Insert => new KeyboardKeyViewModel { DisplayText = "Ins", UnicodeIcon = null, AriaLabel = "Insert" }, - NamedKeyboardKey.Plus => new KeyboardKeyViewModel { DisplayText = "+", UnicodeIcon = null }, - NamedKeyboardKey.Fn => new KeyboardKeyViewModel { DisplayText = "Fn", UnicodeIcon = null, AriaLabel = "Fn" }, - NamedKeyboardKey.F1 => new KeyboardKeyViewModel { DisplayText = "F1", UnicodeIcon = null }, - NamedKeyboardKey.F2 => new KeyboardKeyViewModel { DisplayText = "F2", UnicodeIcon = null }, - NamedKeyboardKey.F3 => new KeyboardKeyViewModel { DisplayText = "F3", UnicodeIcon = null }, - NamedKeyboardKey.F4 => new KeyboardKeyViewModel { DisplayText = "F4", UnicodeIcon = null }, - NamedKeyboardKey.F5 => new KeyboardKeyViewModel { DisplayText = "F5", UnicodeIcon = null }, - NamedKeyboardKey.F6 => new KeyboardKeyViewModel { DisplayText = "F6", UnicodeIcon = null }, - NamedKeyboardKey.F7 => new KeyboardKeyViewModel { DisplayText = "F7", UnicodeIcon = null }, - NamedKeyboardKey.F8 => new KeyboardKeyViewModel { DisplayText = "F8", UnicodeIcon = null }, - NamedKeyboardKey.F9 => new KeyboardKeyViewModel { DisplayText = "F9", UnicodeIcon = null }, - NamedKeyboardKey.F10 => new KeyboardKeyViewModel { DisplayText = "F10", UnicodeIcon = null }, - NamedKeyboardKey.F11 => new KeyboardKeyViewModel { DisplayText = "F11", UnicodeIcon = null }, - NamedKeyboardKey.F12 => new KeyboardKeyViewModel { DisplayText = "F12", UnicodeIcon = null }, + NamedKeyboardKey.Enter => new SingleKeyboardKeyViewModel + { + DisplayText = "Enter", + UnicodeIcon = "↵" + }, + NamedKeyboardKey.Escape => new SingleKeyboardKeyViewModel + { + DisplayText = "Esc", + UnicodeIcon = "⎋", + AriaLabel = "Escape" + }, + NamedKeyboardKey.Tab => new SingleKeyboardKeyViewModel + { + DisplayText = "Tab", + UnicodeIcon = "↹", + AriaLabel = "Tab" + }, + NamedKeyboardKey.Backspace => new SingleKeyboardKeyViewModel + { + DisplayText = "Backspace", + UnicodeIcon = "⌫" + }, + NamedKeyboardKey.Delete => new SingleKeyboardKeyViewModel + { + DisplayText = "Del", + AriaLabel = "Delete" + }, + NamedKeyboardKey.Home => new SingleKeyboardKeyViewModel + { + DisplayText = "Home", + UnicodeIcon = "⇱" + }, + NamedKeyboardKey.End => new SingleKeyboardKeyViewModel + { + DisplayText = "End", + UnicodeIcon = "⇲" + }, + NamedKeyboardKey.PageUp => new SingleKeyboardKeyViewModel + { + DisplayText = "PageUp", + UnicodeIcon = "⇞", + AriaLabel = "Page Up" + }, + NamedKeyboardKey.PageDown => new SingleKeyboardKeyViewModel + { + DisplayText = "PageDown", + UnicodeIcon = "⇟", + AriaLabel = "Page Down" + }, + NamedKeyboardKey.Space => new SingleKeyboardKeyViewModel + { + DisplayText = "Space", + UnicodeIcon = "␣" + }, + NamedKeyboardKey.Insert => new SingleKeyboardKeyViewModel + { + DisplayText = "Ins", + AriaLabel = "Insert" + }, + NamedKeyboardKey.Plus => new SingleKeyboardKeyViewModel + { + DisplayText = "+", + }, + NamedKeyboardKey.Pipe => new SingleKeyboardKeyViewModel + { + DisplayText = "|", + AriaLabel = "Pipe" + }, + NamedKeyboardKey.Fn => new SingleKeyboardKeyViewModel + { + DisplayText = "Fn", + AriaLabel = "Function key" + }, + NamedKeyboardKey.F1 => new SingleKeyboardKeyViewModel + { + DisplayText = "F1", + }, + NamedKeyboardKey.F2 => new SingleKeyboardKeyViewModel + { + DisplayText = "F2", + }, + NamedKeyboardKey.F3 => new SingleKeyboardKeyViewModel + { + DisplayText = "F3", + }, + NamedKeyboardKey.F4 => new SingleKeyboardKeyViewModel + { + DisplayText = "F4", + }, + NamedKeyboardKey.F5 => new SingleKeyboardKeyViewModel + { + DisplayText = "F5", + UnicodeIcon = null + }, + NamedKeyboardKey.F6 => new SingleKeyboardKeyViewModel + { + DisplayText = "F6", + }, + NamedKeyboardKey.F7 => new SingleKeyboardKeyViewModel + { + DisplayText = "F7", + }, + NamedKeyboardKey.F8 => new SingleKeyboardKeyViewModel + { + DisplayText = "F8", + }, + NamedKeyboardKey.F9 => new SingleKeyboardKeyViewModel + { + DisplayText = "F9", + }, + NamedKeyboardKey.F10 => new SingleKeyboardKeyViewModel + { + DisplayText = "F10", + }, + NamedKeyboardKey.F11 => new SingleKeyboardKeyViewModel + { + DisplayText = "F11", + }, + NamedKeyboardKey.F12 => new SingleKeyboardKeyViewModel + { + DisplayText = "F12", + }, // Function keys _ => throw new ArgumentOutOfRangeException(nameof(key), key, null) }; @@ -170,7 +379,8 @@ public enum NamedKeyboardKey // Other Keys [Display(Name = "plus")] Plus, - [Display(Name = "fn")] Fn + [Display(Name = "fn")] Fn, + [Display(Name = "pipe")] Pipe } public class IKeyNode; @@ -185,9 +395,23 @@ public class CharacterKeyNode : IKeyNode public required char Key { get; init; } } -public record KeyboardKeyViewModel +public interface IKeyboardViewModel; + +public record SingleKeyboardKeyViewModel : IKeyboardViewModel { - public required string? UnicodeIcon { get; init; } + public string? UnicodeIcon { get; init; } public required string DisplayText { get; init; } public string? AriaLabel { get; init; } } + +public record AlternateKeyboardKeyViewModel : IKeyboardViewModel +{ + public required SingleKeyboardKeyViewModel Primary { get; init; } + public required SingleKeyboardKeyViewModel Alternate { get; init; } +} + +public class AlternateKeyNode : IKeyNode +{ + public required IKeyNode Primary { get; init; } + public required IKeyNode Alternate { get; init; } +} diff --git a/tests/authoring/Inline/KbdRole.fs b/tests/authoring/Inline/KbdRole.fs index 9b507c44f..90b600666 100644 --- a/tests/authoring/Inline/KbdRole.fs +++ b/tests/authoring/Inline/KbdRole.fs @@ -46,3 +46,33 @@ type ``renders combined kbd role with special characters`` () = markdown |> convertsToHtml """

Ctrl + Alt + Del

""" + +type ``renders alternative kbd role`` () = + static let markdown = Setup.Markdown """ +{kbd}`ctrl|cmd+c` +""" + [] + let ``validate HTML`` () = + markdown |> convertsToHtml """ +

Ctrl / Cmd + c

+""" + +type ``renders plus kbd role`` () = + static let markdown = Setup.Markdown """ +{kbd}`plus` +""" + [] + let ``validate HTML`` () = + markdown |> convertsToHtml """ +

+

+""" + +type ``renders pipe kbd role`` () = + static let markdown = Setup.Markdown """ +{kbd}`pipe` +""" + [] + let ``validate HTML`` () = + markdown |> convertsToHtml """ +

|

+""" From bd2d7fa7f720224ff51b38dc47ba319744637246 Mon Sep 17 00:00:00 2001 From: Jan Calanog Date: Wed, 9 Jul 2025 12:07:18 +0200 Subject: [PATCH 12/13] Adjust tests --- tests/authoring/Inline/KbdRole.fs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/tests/authoring/Inline/KbdRole.fs b/tests/authoring/Inline/KbdRole.fs index 90b600666..e72567dc0 100644 --- a/tests/authoring/Inline/KbdRole.fs +++ b/tests/authoring/Inline/KbdRole.fs @@ -54,7 +54,15 @@ type ``renders alternative kbd role`` () = [] let ``validate HTML`` () = markdown |> convertsToHtml """ -

Ctrl / Cmd + c

+

+ + Ctrl + + Cmd + + + + c +

""" type ``renders plus kbd role`` () = @@ -74,5 +82,5 @@ type ``renders pipe kbd role`` () = [] let ``validate HTML`` () = markdown |> convertsToHtml """ -

|

+

|

""" From fa30512c98974b8915ff5104aefe06be154e10e6 Mon Sep 17 00:00:00 2001 From: Jan Calanog Date: Wed, 9 Jul 2025 12:08:45 +0200 Subject: [PATCH 13/13] Run prettier --- src/Elastic.Documentation.Site/Assets/markdown/kbd.css | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Elastic.Documentation.Site/Assets/markdown/kbd.css b/src/Elastic.Documentation.Site/Assets/markdown/kbd.css index 2544c142f..e8f14975c 100644 --- a/src/Elastic.Documentation.Site/Assets/markdown/kbd.css +++ b/src/Elastic.Documentation.Site/Assets/markdown/kbd.css @@ -1,16 +1,16 @@ @layer components { .markdown-content { kbd.kbd { - @apply relative bg-grey-20 text-grey-100 border-grey-50 shadow-grey-50 top-[-2px] inline-flex items-center min-w-[18px] cursor-default gap-1.5 rounded-sm border px-1.5 pt-[3px] pb-[2px] text-center align-middle font-mono text-sm leading-none capitalize shadow-[0_2px_0_1px]; + @apply bg-grey-20 text-grey-100 border-grey-50 shadow-grey-50 relative top-[-2px] inline-flex min-w-[18px] cursor-default items-center gap-1.5 rounded-sm border px-1.5 pt-[3px] pb-[2px] text-center align-middle font-mono text-sm leading-none capitalize shadow-[0_2px_0_1px]; } - + kbd.kbd .kbd-separator { - @apply bg-grey-100 inline-block mx-1 self-stretch; + @apply bg-grey-100 mx-1 inline-block self-stretch; width: 1px; /*height: .8em;*/ transform: translateY(-1px) rotate(30deg); } - + kbd.kbd .kbd-space { @apply w-0; }