Skip to content

Commit f31c9e0

Browse files
authored
Merge pull request #145 from posit-dev/feat-kbd-shortcode
feat: add `keys` shortcode extension
2 parents f3afb5f + d961fca commit f31c9e0

9 files changed

Lines changed: 1106 additions & 0 deletions

File tree

great-docs.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -265,6 +265,7 @@ nav_icons:
265265
Table Previews: table
266266
Table Explorer: telescope
267267
Horizontal Rules: minus
268+
Keyboard Keys: keyboard
268269

269270
# Author Information
270271
# ------------------
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
title: Keyboard Keys
2+
author: Great Docs
3+
version: 1.0.0
4+
quarto-required: ">=1.3.0"
5+
contributes:
6+
shortcodes:
7+
- keys.lua
Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
-- keys.lua — Quarto shortcode for styled keyboard key caps
2+
--
3+
-- Usage in .qmd files:
4+
--
5+
-- {{< keys "Esc" >}}
6+
-- {{< keys "Ctrl" >}}+{{< keys "Shift" >}}+{{< keys "P" >}}
7+
-- {{< keys shortcut="Ctrl+Shift+P" >}}
8+
-- {{< keys shortcut="Ctrl+Shift+P" platform="mac" >}}
9+
-- {{< keys shortcut="Cmd+K" platform="win" >}}
10+
--
11+
-- Pure Lua implementation — no Python helper needed.
12+
-- All styling is handled via CSS classes in great-docs.scss.
13+
--
14+
-- NOTE: Quarto ships a built-in {{< kbd >}} shortcode that renders plain-text
15+
-- shortcuts with per-OS keyword args (mac=, win=, linux=). This extension is
16+
-- intentionally named "keys" to avoid conflict. It provides *styled* key caps
17+
-- with a 3D border effect and macOS symbol translation.
18+
19+
local function kwarg_str(kwargs, key)
20+
local raw = kwargs[key]
21+
if raw == nil then return "" end
22+
local s = pandoc.utils.stringify(raw)
23+
return s or ""
24+
end
25+
26+
-- Map generic key names to macOS symbols
27+
local MAC_KEYS = {
28+
ctrl = "",
29+
control = "",
30+
alt = "",
31+
option = "",
32+
opt = "",
33+
shift = "",
34+
cmd = "",
35+
command = "",
36+
meta = "",
37+
super = "",
38+
enter = "",
39+
["return"] = "",
40+
tab = "",
41+
delete = "",
42+
backspace = "",
43+
esc = "",
44+
escape = "",
45+
space = "",
46+
up = "",
47+
down = "",
48+
left = "",
49+
right = "",
50+
}
51+
52+
-- Map macOS-specific key names to Windows/generic equivalents
53+
local WIN_KEYS = {
54+
cmd = "Ctrl",
55+
command = "Ctrl",
56+
meta = "Win",
57+
super = "Win",
58+
option = "Alt",
59+
opt = "Alt",
60+
}
61+
62+
-- Tooltip text for symbolic key labels (shown on hover)
63+
local TOOLTIPS = {
64+
[""] = "Control",
65+
[""] = "Option",
66+
[""] = "Shift",
67+
[""] = "Command",
68+
[""] = "Enter",
69+
[""] = "Tab",
70+
[""] = "Delete",
71+
[""] = "Escape",
72+
[""] = "Space",
73+
[""] = "Up",
74+
[""] = "Down",
75+
[""] = "Left",
76+
[""] = "Right",
77+
}
78+
79+
--- Escape HTML special characters.
80+
local function escape_html(s)
81+
return s:gsub("&", "&amp;"):gsub("<", "&lt;"):gsub(">", "&gt;"):gsub('"', "&quot;")
82+
end
83+
84+
--- Check whether a label is a function key (F1–F20).
85+
local function is_fn_key(label)
86+
return label:match("^[Ff]%d%d?$") ~= nil
87+
end
88+
89+
--- Render a single key label into an HTML <kbd> element.
90+
--- @param label string The display text for the key
91+
--- @return string HTML string
92+
local function render_key(label)
93+
local cls = "gd-keys"
94+
if is_fn_key(label) then
95+
cls = "gd-keys gd-keys-fn"
96+
end
97+
local tooltip = TOOLTIPS[label]
98+
if tooltip then
99+
return '<kbd class="' .. cls .. '" title="' .. tooltip .. '">' .. escape_html(label) .. '</kbd>'
100+
end
101+
return '<kbd class="' .. cls .. '">' .. escape_html(label) .. '</kbd>'
102+
end
103+
104+
--- Translate a single key name for a given platform.
105+
--- @param key string Raw key name (e.g. "Ctrl", "Cmd")
106+
--- @param platform string "mac" | "win" | ""
107+
--- @return string Display label for the key
108+
local function translate_key(key, platform)
109+
local lower = key:lower()
110+
111+
if platform == "mac" then
112+
local sym = MAC_KEYS[lower]
113+
if sym then return sym end
114+
elseif platform == "win" then
115+
local mapped = WIN_KEYS[lower]
116+
if mapped then return mapped end
117+
end
118+
119+
-- For keys that aren't platform-special, title-case single-char keys
120+
-- and preserve the original casing for multi-char keys
121+
if #key == 1 then
122+
return key:upper()
123+
end
124+
return key
125+
end
126+
127+
--- Split a shortcut string on "+" while respecting a literal "+" key.
128+
--- E.g. "Ctrl+Shift++" => {"Ctrl", "Shift", "+"}
129+
--- @param shortcut string
130+
--- @return table List of key names
131+
local function split_shortcut(shortcut)
132+
local keys = {}
133+
local i = 1
134+
local len = #shortcut
135+
136+
while i <= len do
137+
-- Find next "+"
138+
local j = shortcut:find("+", i, true)
139+
if j == nil then
140+
-- Last segment
141+
local seg = shortcut:sub(i)
142+
if seg ~= "" then
143+
table.insert(keys, seg)
144+
end
145+
break
146+
end
147+
148+
local seg = shortcut:sub(i, j - 1)
149+
if seg == "" then
150+
-- The "+" itself is the key (e.g. at start, or after another "+")
151+
table.insert(keys, "+")
152+
i = j + 1
153+
else
154+
table.insert(keys, seg)
155+
i = j + 1
156+
end
157+
end
158+
159+
return keys
160+
end
161+
162+
return {
163+
["keys"] = function(args, kwargs)
164+
local shortcut = kwarg_str(kwargs, "shortcut")
165+
local platform = kwarg_str(kwargs, "platform")
166+
167+
-- Normalise platform
168+
if platform ~= "" then
169+
platform = platform:lower()
170+
if platform ~= "mac" and platform ~= "win" then
171+
platform = ""
172+
end
173+
end
174+
175+
-- Single-key mode: positional arg or key="" kwarg
176+
if shortcut == "" then
177+
local key = kwarg_str(kwargs, "key")
178+
if key == "" and #args > 0 then
179+
key = pandoc.utils.stringify(args[1])
180+
end
181+
182+
if key == "" then
183+
return pandoc.RawInline(
184+
"html",
185+
"<!-- keys shortcode error: missing key or shortcut -->"
186+
)
187+
end
188+
189+
local label = translate_key(key, platform)
190+
return pandoc.RawInline("html", render_key(label))
191+
end
192+
193+
-- Shortcut combo mode: split on "+" and render each key
194+
local keys = split_shortcut(shortcut)
195+
local parts = {}
196+
for _, k in ipairs(keys) do
197+
local label = translate_key(k, platform)
198+
table.insert(parts, render_key(label))
199+
end
200+
201+
local separator = '<span class="gd-keys-sep">+</span>'
202+
return pandoc.RawInline("html", table.concat(parts, separator))
203+
end
204+
}

great_docs/assets/great-docs.scss

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6192,6 +6192,67 @@ td > p:has(> svg.gd-icon) {
61926192
}
61936193

61946194

6195+
/* ── Keyboard Keys Shortcode ──────────────────────────────────── */
6196+
6197+
.gd-keys {
6198+
display: inline-block;
6199+
font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas,
6200+
"Liberation Mono", monospace;
6201+
font-size: 0.8em;
6202+
font-weight: 500;
6203+
line-height: 1;
6204+
padding: 0.15em 0.45em;
6205+
min-width: 1.6em;
6206+
text-align: center;
6207+
white-space: nowrap;
6208+
vertical-align: baseline;
6209+
color: #1f2328;
6210+
background: linear-gradient(180deg, #f6f8fa 0%, #eaeef2 100%);
6211+
border: 1px solid #d0d7de;
6212+
border-bottom-width: 2px;
6213+
border-radius: 5px;
6214+
box-shadow: 0 1px 0 rgba(27, 31, 36, 0.04),
6215+
inset 0 1px 0 rgba(255, 255, 255, 0.25);
6216+
}
6217+
6218+
.gd-keys-sep {
6219+
display: inline-block;
6220+
margin: 0 0.15em;
6221+
font-size: 0.85em;
6222+
color: #656d76;
6223+
vertical-align: baseline;
6224+
user-select: none;
6225+
}
6226+
6227+
/* Function keys (F1–F20): smallcaps-height text on the baseline */
6228+
.gd-keys-fn {
6229+
font-size: 0.7em;
6230+
font-weight: 600;
6231+
letter-spacing: 0.04em;
6232+
padding: 0.25em 0.45em;
6233+
text-transform: uppercase;
6234+
position: relative;
6235+
top: -1.1px;
6236+
}
6237+
6238+
/* Dark mode */
6239+
body.quarto-dark .gd-keys,
6240+
html.quarto-dark .gd-keys,
6241+
:root[data-bs-theme="dark"] .gd-keys {
6242+
color: #e6edf3;
6243+
background: linear-gradient(180deg, #2d333b 0%, #22272e 100%);
6244+
border-color: #444c56;
6245+
box-shadow: 0 1px 0 rgba(0, 0, 0, 0.3),
6246+
inset 0 1px 0 rgba(255, 255, 255, 0.04);
6247+
}
6248+
6249+
body.quarto-dark .gd-keys-sep,
6250+
html.quarto-dark .gd-keys-sep,
6251+
:root[data-bs-theme="dark"] .gd-keys-sep {
6252+
color: #8b949e;
6253+
}
6254+
6255+
61956256
/* ── Video embedding enhancements ────────────────────────────── */
61966257

61976258
/* Subtle border around video containers so the frame edge is visible */

test-packages/synthetic/catalog.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -368,6 +368,8 @@
368368
"gdtest_hr_shortcode", # 181
369369
# 182: Site-wide accent_color config option
370370
"gdtest_accent_color", # 182
371+
# 183: Keyboard keys shortcode showcase
372+
"gdtest_keys_shortcode", # 183
371373
]
372374

373375

@@ -2065,6 +2067,13 @@
20652067
"and text dividers while palette colors (sky, peach, etc.) retain "
20662068
"their own hues. Tests light and dark mode handling."
20672069
),
2070+
"gdtest_keys_shortcode": (
2071+
"Keyboard keys shortcode showcase exercising the {{< keys >}} shortcode "
2072+
"in four user-guide pages: single keys (Esc, Enter, Tab, modifiers, "
2073+
"arrows), shortcut combos (Ctrl+Shift+P auto-split), platform-aware "
2074+
"rendering (macOS symbols ⌘⌥⇧⌃ vs Windows labels), and keys in "
2075+
"context (headings, callouts, lists, blockquotes, prose)."
2076+
),
20682077
}
20692078

20702079

0 commit comments

Comments
 (0)