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..a23f7669b --- /dev/null +++ b/docs/syntax/kbd.md @@ -0,0 +1,141 @@ +# 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. See the full list of [available keys](#available-keys). + +## 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. +``` +::: + +:::: + +## Combining keys + +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} + +:::{tab-item} Output +Use {kbd}`cmd+shift+enter` to execute the command. +::: + +:::{tab-item} Markdown +```markdown +Use {kbd}`cmd+shift+enter` to execute the command. +``` +::: + +:::: + +## 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 +Use {kbd}`ctrl|cmd + c` to copy text. +::: + +:::{tab-item} Markdown +```markdown +Use {kbd}`ctrl|cmd + 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. + +- To display the {kbd}`plus` key, use `` `{kbd}`plus` ``. +- To display the {kbd}`pipe` key, use `` `{kbd}`pipe` ``. + +## Common shortcuts by platform + +The platform-specific examples below demonstrate how to combine special keys and regular characters. + +::::{tab-set} + +:::{tab-item} Output + +| 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 +| 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: + +| 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 new file mode 100644 index 000000000..e8f14975c --- /dev/null +++ b/src/Elastic.Documentation.Site/Assets/markdown/kbd.css @@ -0,0 +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 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 mx-1 inline-block self-stretch; + width: 1px; + /*height: .8em;*/ + transform: translateY(-1px) rotate(30deg); + } + + kbd.kbd .kbd-space { + @apply w-0; + } + } +} diff --git a/src/Elastic.Documentation.Site/Assets/styles.css b/src/Elastic.Documentation.Site/Assets/styles.css index 2ff2a0b92..018c897f5 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'; @@ -227,3 +228,9 @@ body { .tippy-content { white-space: pre-line; } + +.icon, +.icon > * { + user-select: none; + pointer-events: none; +} 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/Kbd.cs b/src/Elastic.Markdown/Myst/Roles/Kbd/Kbd.cs new file mode 100644 index 000000000..73bb5e090 --- /dev/null +++ b/src/Elastic.Markdown/Myst/Roles/Kbd/Kbd.cs @@ -0,0 +1,417 @@ +// 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 System.Text; +using System.Web; +using NetEscapades.EnumGenerators; + +namespace Elastic.Markdown.Myst.Roles.Kbd; + +public class KeyboardShortcut(IReadOnlyList keys) +{ + private IReadOnlyList Keys { get; } = keys; + + public static KeyboardShortcut Unknown { get; } = new([ + new CharacterKeyNode + { + Key = '?' + } + ]); + + public static KeyboardShortcut Parse(string input) + { + if (string.IsNullOrWhiteSpace(input)) + return new KeyboardShortcut([]); + + 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(ToViewModel); + var kbdElements = viewModels.Select(viewModel => viewModel switch + { + SingleKeyboardKeyViewModel singleKeyboardKeyViewModel => Render(singleKeyboardKeyViewModel), + AlternateKeyboardKeyViewModel alternateKeyboardKeyViewModel => Render(alternateKeyboardKeyViewModel), + _ => throw new ArgumentException($"Unsupported key: {viewModel}", nameof(viewModel)) + }); + return string.Join(" + ", kbdElements); + } + + private static string Render(AlternateKeyboardKeyViewModel alternateKeyboardKeyViewModel) + { + var sb = new StringBuilder(); + _ = sb.Append("'); + + 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 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 SingleKeyboardKeyViewModel GetDisplayModel(NamedKeyboardKey key) => + key switch + { + // Modifier keys with special symbols + 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 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 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) + }; +} + +[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, + [Display(Name = "pipe")] Pipe +} + +public class IKeyNode; + +public class NamedKeyNode : IKeyNode +{ + public required NamedKeyboardKey Key { get; init; } +} + +public class CharacterKeyNode : IKeyNode +{ + public required char Key { get; init; } +} + +public interface IKeyboardViewModel; + +public record SingleKeyboardKeyViewModel : IKeyboardViewModel +{ + 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/src/Elastic.Markdown/Myst/Roles/Kbd/KbdParser.cs b/src/Elastic.Markdown/Myst/Roles/Kbd/KbdParser.cs new file mode 100644 index 000000000..4cd459946 --- /dev/null +++ b/src/Elastic.Markdown/Myst/Roles/Kbd/KbdParser.cs @@ -0,0 +1,18 @@ +// 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 Elastic.Markdown.Diagnostics; +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, 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 new file mode 100644 index 000000000..36e3dedcf --- /dev/null +++ b/src/Elastic.Markdown/Myst/Roles/Kbd/KbdRole.cs @@ -0,0 +1,27 @@ +// 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; +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 : RoleLeaf +{ + 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.Unknown; + } + } + public KeyboardShortcut KeyboardShortcut { get; } +} 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..6f4197d13 --- /dev/null +++ b/src/Elastic.Markdown/Myst/Roles/Kbd/KbdRoleRenderer.cs @@ -0,0 +1,37 @@ +// 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) + { + var output = KeyboardShortcut.Render(role.KeyboardShortcut); + _ = renderer.Write(output); + } +} + +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()); +} diff --git a/tests/authoring/Inline/KbdRole.fs b/tests/authoring/Inline/KbdRole.fs new file mode 100644 index 000000000..e72567dc0 --- /dev/null +++ b/tests/authoring/Inline/KbdRole.fs @@ -0,0 +1,86 @@ +// 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``.``kbd role`` + +open Xunit +open authoring + +type ``renders single kbd role`` () = + static let markdown = Setup.Markdown """ +{kbd}`cmd` +""" + [] + let ``validate HTML`` () = + markdown |> convertsToHtml """ +

Cmd

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

c

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

Cmd + Shift + c

+""" + +type ``renders combined kbd role with special characters`` () = + static let markdown = Setup.Markdown """ +{kbd}`ctrl+alt+del` +""" + [] + let ``validate HTML`` () = + 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 """ +

|

+""" 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 @@ +