Skip to content
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/_docset.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
146 changes: 146 additions & 0 deletions docs/syntax/kbd.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
# 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.
```
:::

::::

## Combining keys

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
Use {kbd}`cmd+shift+enter` to execute the command.
:::

:::{tab-item} Markdown
```markdown
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.

::::{tab-set}

:::{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
{kbd}`ctrl` / {kbd}`cmd` + {kbd}`c` to copy text.
:::


:::{tab-item} Markdown
```markdown
{kbd}`ctrl` / {kbd}`cmd` + {kbd}`c` to copy text.
```
:::

::::

## 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:

| 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` |
7 changes: 7 additions & 0 deletions src/Elastic.Documentation.Site/Assets/markdown/kbd.css
Original file line number Diff line number Diff line change
@@ -0,0 +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 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];
}
}
}
7 changes: 7 additions & 0 deletions src/Elastic.Documentation.Site/Assets/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -227,3 +228,9 @@ body {
.tippy-content {
white-space: pre-line;
}

.icon,
.icon > * {
user-select: none;
pointer-events: none;
}
2 changes: 2 additions & 0 deletions src/Elastic.Markdown/Myst/MarkdownParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -147,6 +148,7 @@ public static MarkdownPipeline Pipeline
.UseEmphasisExtras(EmphasisExtraOptions.Default)
.UseInlineAppliesTo()
.UseInlineIcons()
.UseInlineKbd()
.UseSubstitution()
.UseComments()
.UseYamlFrontMatter()
Expand Down
193 changes: 193 additions & 0 deletions src/Elastic.Markdown/Myst/Roles/Kbd/Kbd.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
// Licensed to Elasticsearch B.V under one or more agreements.
// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
// See the LICENSE file in the project root for more information

using 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<IKeyNode> keys)
{
private IReadOnlyList<IKeyNode> 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 parts = input.Split('+', StringSplitOptions.RemoveEmptyEntries);
var keys = new List<IKeyNode>();

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("<kbd class=\"kbd\"");
if (viewModel.AriaLabel is not null)
_ = sb.Append(" aria-label=\"" + viewModel.AriaLabel + "\"");
_ = sb.Append('>');
if (viewModel.UnicodeIcon is not null)
_ = sb.Append($"<span class=\"kbd-icon\">{viewModel.UnicodeIcon}</span>");
_ = sb.Append(viewModel.DisplayText);
_ = sb.Append("</kbd>");
return sb.ToString();
});

return string.Join(" + ", kbdElements);
}

private static FrozenDictionary<NamedKeyboardKey, KeyboardKeyViewModel> ViewModelMapping { get; } =
Enum.GetValues<NamedKeyboardKey>().ToFrozenDictionary(k => k, GetDisplayModel);

private static KeyboardKeyViewModel 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" },
// 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" },
// 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 },
// 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; }
public string? AriaLabel { get; init; }
}
Loading
Loading