Create documentation website

Builds a static documentation website using Eleventy to visualize the keymap
with comprehensive symbol tables and SVG layer diagrams.

Components:
- layout.yaml: Source of truth defining keymap layers and key positions
- Eleventy website: Generates static HTML documentation
- SVG diagrams: Visual layer representations via keymap-drawer
- Symbol tables: Shows all ways to type each character, organized by category
- Physical position notation: LPH, RTI+RMT format for describing key positions

Features:
- 8 layers documented: BASE, BASE_SHIFT, BASE_ALT, BASE_SHIFT_ALT, NAV,
  ALT_NAV, SYMBOL, SHIFT_SYMBOL
- 180 unique symbols categorized across 10 groups (letters, numbers,
  punctuation, common, invisible, navigation, commands, special,
  non-printing, rare)
- Light/dark mode theming with system preference detection
- Real layer grouping: shows access methods per physical layer
- Comprehensive symbol access methods: displays all key combinations

Build pipeline:
1. scripts/generate-svgs.js: Generates SVG diagrams from layout.yaml
2. website/src/_data/keymaps.js: Parses layout.yaml and builds symbol index
3. Eleventy: Renders HTML with templates and symbol tables

The documentation focuses on the user perspective (what can I type and how)
rather than implementation details.
This commit is contained in:
Drew Neil 2026-01-04 22:42:12 +00:00
commit a5d434e44d
18 changed files with 3771 additions and 0 deletions

3
.gitignore vendored
View file

@ -14,3 +14,6 @@
compile_commands.json compile_commands.json
.clangd/ .clangd/
.cache/ .cache/
# Eleventy build output
_site/

View file

@ -0,0 +1,47 @@
layout:
qmk_keyboard: ferris/sweep
layout_name: LAYOUT_split_3x5_2
layers:
BASE:
- [q, w, e, r, t, y, u, i, o, p]
- [a, s, {h: Gui, t: d}, {h: Ctrl, t: f}, g, h, {h: Ctrl, t: j}, {h: Gui, t: k}, l, ";"]
- [z, x, c, v, b, n, m, Repeat, ., /]
- [{t: TO(NAV)}, {t: OSM Shift}, {t: OSM Alt}, Space]
BASE_SHIFT:
- [Q, W, E, R, T, Y, U, I, O, P]
- [A, S, {h: Gui, t: D}, {h: Ctrl, t: F}, G, H, {h: Ctrl, t: J}, {h: Gui, t: K}, L, ":"]
- [Z, X, C, V, B, N, M, "Alt Repeat", ",", "?"]
- [{t: TO(NAV)}, {type: held}, {t: OSM Alt}, Space]
BASE_ALT:
- [Esc, "@", "#", "$", "%", "^", "&", "*", "-", BSpace]
- [Tab, "`", "'", '"', null, "\\", "[", "|", "]", Enter]
- ["~", "-", "+", "=", "_", null, "(", null, ")", "Del Word"]
- [{t: TO(NAV)}, {t: OSM Shift}, {type: held}, Space]
BASE_SHIFT_ALT:
- [null, "€", null, "£", null, null, null, null, null, null]
- [null, null, null, null, null, null, "{", "!", "}", null]
- [null, "“", "", "", "”", null, "<", null, ">", null]
- [{t: TO(NAV)}, {type: held}, {type: held}, Space]
NAV:
- [Esc, "KP_7", "KP_8", "KP_9", null, "Cmd+[", "Ctrl+Shift+Tab", "Ctrl+Tab", "Cmd+]", BSpace]
- [Tab, "KP_4", {h: Gui, t: "KP_5"}, {h: Ctrl, t: "KP_6"}, null, Left, {h: Ctrl, t: Down}, {h: Gui, t: Up}, Right, Enter]
- ["KP_0", "KP_1", "KP_2", "KP_3", null, null, null, Repeat, ".", null]
- [{t: TO(SYMBOL)}, Trans, Trans, {t: TO(BASE)}]
ALT_NAV:
- [Esc, F7, F8, F9, null, "Cmd+[", "Ctrl+Shift+Tab", "Ctrl+Tab", "Cmd+]", BSpace]
- [Tab, F4, {h: Gui, t: F5}, {h: Ctrl, t: F6}, null, Left, {h: Ctrl, t: Down}, {h: Gui, t: Up}, Right, Enter]
- [F10, F1, F2, F3, null, null, null, Repeat, ".", null]
- [{t: TO(SYMBOL)}, Trans, {type: held}, {t: TO(BASE)}]
SYMBOL:
- ["œ", "∑", "´", "®", "†", "¥", "¨", "ˆ", "ø", "π"]
- ["å", "ß", "∂", "ƒ", "©", "˙", "∆", "˚", "¬", "…"]
- ["Ω", "≈", "ç", "√", "∫", "~", "µ", "≤", "≥", "÷"]
- [{t: TO(BASE)}, Trans, Trans, {t: TO(BASE)}]
SHIFT_SYMBOL:
- ["Œ", "„", "‰", "Â", "Ê", "Á", "Ë", "ˆ", "Ø", "∏"]
- ["Å", "Í", "Î", "Ï", "Ì", "Ó", "Ô", "", "Ò", "Ú"]
- ["Û", "Ù", "Ç", "◊", "ı", "ˆ", "˜", "¯", "˘", "¿"]
- [{t: TO(BASE)}, {type: held}, Trans, {t: TO(BASE)}]
combos:
- {p: [0, 1], k: "Esc+Tab combo", l: ["BASE"], a: top}
- {p: [8, 9], k: "Special combo", l: ["BASE"], a: top}

View file

@ -0,0 +1,32 @@
# Keymap cheetsheets
Create a website containing visualisations of my keymaps. The website should be able to run locally with a webserver, and should also be able to be published to Github pages. The website should have an index page, containing links to each keymap, grouped by keyboard. Initially, there will only be a single keymap, that being qwerty for my Ferris Sweep. Later, I may add a variation for Colemak.
The webpage for a keymap should feature:
* SVG diagrams for each layer
* index tables showing all symbols that can be typed, and where on the keyboard they can be found
Note that some symbols may appear multiple times.
## Creating SVG diagrams
The keymap-drawer app can create SVG diagrams from a layout.yaml file. This is a good starting point. We shall define one layout.yaml file for each keymap. There should be a build step that generates all SVG files. The layout.yaml source files should be co-located with the actual source code for the keymap itself. The generated SVG files should appear in the source directory for the website.
## Creating index tables
The index tables should be grouped by category. For example, we'll start with the most common symbols (lowercase letters, uppercase letters, punctuation, numbers), we'll also include non-printing characters (including function keys F1-F12, command+{symbol} combos, and so on), and finally we'll include the rare symbols, such as those accessed on os x with the alt key.
The index table should include several columns:
1. Showing the character (where possible)
2. Describing the symbol (e.g. "Lowercase a", "Tab", "Function key 1")
3. Layer 0: which keys to press to produce that symbol
4. Layer 1: which keys to press to produce that symbol
5. Layer 2: which keys to press to produce that symbol
The purpose of these index tables is to make it easy to look up a symbol/character, and figure out how to type it.
## Theming
The site should support both light mode and dark mode. By default it should follow the user's system preferences.

160
project/keymap-notation.md Normal file
View file

@ -0,0 +1,160 @@
# Keymap Notation System
## Overview
This notation system provides a clear, concise way to describe which physical keys to press on a split keyboard, avoiding tautological descriptions like "press the 'a' key to type 'a'".
The notation describes the **physical position** of keys using a coordinate system based on:
- Which hand (left or right)
- Which finger is used
- Which row the key is on
## Coordinate Format
### Regular Keys (Non-Thumb)
Format: `[L|R][p|r|m|i|e][h|t|b]`
**Hand:**
- `L` = Left hand
- `R` = Right hand
**Finger:**
- `p` = Pinky
- `r` = Ring finger
- `m` = Middle finger
- `i` = Index finger
- `e` = Empty column (index finger reach - the 5th column)
**Row:**
- `h` = Home row
- `t` = Top row
- `b` = Bottom row
### Thumb Keys
Format: `[L|R]t[i|o]`
**Hand:**
- `L` = Left hand
- `R` = Right hand
**Finger:**
- `t` = Thumb
**Position:**
- `i` = Inside (thumb key closer to center/index finger)
- `o` = Outside (thumb key further from center)
## Examples
### Single Key Press
| Symbol | Notation | Description |
|--------|----------|-------------|
| a | `LPH` | Left pinky, home row |
| A | `LPH` (with shift) | Left pinky, home row + modifier |
| s | `LRH` | Left ring, home row |
| d | `LMH` | Left middle, home row |
| f | `LIH` | Left index, home row |
| g | `LEH` | Left index reach (empty column), home row |
| q | `LPT` | Left pinky, top row |
| z | `LPB` | Left pinky, bottom row |
| ; | `RPH` | Right pinky, home row |
| Space | `LTO` | Left thumb, outside |
| Nav layer | `RTO` | Right thumb, outside (TO(NAV)) |
### Key Combinations (Modifiers + Key)
Format: `MODIFIER+KEY`
| Symbol | Notation | Description |
|--------|----------|-------------|
| * | `RTI+RMT` | Right thumb inside (Alt) + Right middle top (i key) |
| @ | `RTI+LRT` | Right thumb inside (Alt) + Left ring top (w key) |
| % | `RTI+RRT` | Right thumb inside (Alt) + Right ring top (t key) |
| Backspace (Alt+P) | `RTI+RPT` | Right thumb inside (Alt) + Right pinky top |
## Physical Keyboard Layout (Ferris Sweep)
### Left Hand
```
Row: Top (t) Home (h) Bottom (b)
Pinky: q a z (p)
Ring: w s x (r)
Middle: e d c (m)
Index: r f v (i)
Reach: t g b (e)
Thumbs: Space (o) OSM Shift (i)
```
### Right Hand
```
Row: Top (t) Home (h) Bottom (b)
Reach: y h n (e)
Index: u j m (i)
Middle: i k Repeat (m)
Ring: o l . (r)
Pinky: p ; / (p)
Thumbs: OSM Alt (i) TO(NAV) (o)
```
## Layer Types
### Real Layers
Accessed with layer-switch keys (e.g., `TO(NAV)`). No modifier keys need to be held.
- **Layer 0**: Base layer (default)
- **Layer 1**: Nav layer (navigation and numbers)
- **Layer 2**: Alt-symbol layer (future/TBD)
### Ghost Layers
Virtual layers accessed by holding modifier keys. Not separate physical layers.
- **SHIFT**: Accessed by holding Shift modifier
- **ALT**: Accessed by holding Alt modifier
- **SHIFT_ALT**: Accessed by holding both Shift and Alt modifiers
## Usage in Documentation
When documenting symbol access methods in the index tables:
1. **Symbol column**: The character itself (e.g., `a`, `*`, `@`)
2. **Description column**: Human-readable name (e.g., "Lowercase a", "Asterisk", "At sign")
3. **Layer columns**: One column per **real layer** (Layer 0, Layer 1, Layer 2)
- Shows all ways to access the symbol from that layer
- Includes both direct key presses and modifier combinations
- Uses coordinate notation for clarity
### Example Table Entries
| Symbol | Description | Layer 0 (Base) | Layer 1 (Nav) | Layer 2 (Alt-Sym) |
|--------|-------------|----------------|---------------|-------------------|
| a | Lowercase a | `LPH` | — | — |
| A | Uppercase A | `LTI+LPH` | — | — |
| * | Asterisk | `RTI+RMT` | — | — |
| Space | Space | `LTO` | — | — |
| ↵ | Enter | `RTI+RPH` | `RPH` | — |
## Notes
- This notation describes **physical key positions**, not the symbols printed on keycaps
- The same physical key may produce different symbols depending on:
- The active layer
- Which modifiers are held
- Modifier combinations are shown with `+` (e.g., `LTI+LPH` = left thumb inside + left pinky home)
- The notation is independent of the keymap layout (QWERTY, Colemak, etc.)
- `—` indicates the symbol is not accessible from that layer
## Benefits
1. **Language-independent**: Describes physical positions, not key labels
2. **Layout-agnostic**: Works with any keymap (QWERTY, Colemak, Dvorak, etc.)
3. **Muscle memory focused**: Emphasizes finger positions rather than abstract key names
4. **Concise**: Short notation (3-4 characters per key)
5. **Unambiguous**: Each physical key has exactly one notation
6. **Avoids tautology**: Describes *how* to type a symbol, not just "press the X key"

255
project/website-plan.md Normal file
View file

@ -0,0 +1,255 @@
Key Changes:
1. ✅ Directory name: Changed from docs/ to website/ throughout
2. ✅ Documentation philosophy: layout.yaml is the SOURCE OF TRUTH - shows user perspective, not implementation details
3. ✅ Symbol tables: Will show ALL methods comprehensively from user perspective
4. ✅ Implementation approach: Incremental with checkpoints after each milestone
5. ✅ Categories: Using the proposed categories (letters, numbers, punctuation, common, non-printing, rare)
6. ✅ Removed: No parsing of keymap.c - documentation driven purely by layout.yaml
The plan is ready for implementation. We'll proceed incrementally:
- Milestone 1: SVG generation → CHECKPOINT
- Milestone 2: Data processing → CHECKPOINT
- Milestone 3: Templates & symbol tables → CHECKPOINT
- Milestone 4: Theming → CHECKPOINT
- Milestone 5: GitHub Actions deployment → CHECKPOINT
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
Ready to code?
Here is Claude's plan:
╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌
Eleventy Documentation Site for QMK Keymaps
Overview
Build a static documentation website using Eleventy to visualize QMK keymaps with:
- SVG layer diagrams (generated by keymap-drawer from layout.yaml)
- Symbol index tables (showing which key combinations produce each symbol)
- Light/dark mode theming
- Local development server
- GitHub Pages deployment
Repository Structure
qmk_userspace/
├── keyboards/ferris/sweep/keymaps/qwerty/
│ ├── keymap.c # Existing - implementation
│ └── layout.yaml # Existing - SOURCE OF TRUTH (5 layers)
├── website/ # NEW - Eleventy project root
│ ├── .eleventy.js # Configuration
│ ├── package.json # Dependencies: @11ty/eleventy, js-yaml
│ ├── src/
│ │ ├── _data/
│ │ │ ├── keymaps.js # Parses layout.yaml (user perspective)
│ │ │ └── symbolMapping.json # Keycode → description mapping
│ │ ├── _includes/
│ │ │ ├── layouts/base.njk # HTML template
│ │ │ ├── layouts/keymap.njk # Per-keymap page
│ │ │ └── partials/symbol-table.njk
│ │ └── index.njk # Landing page
│ ├── public/
│ │ ├── css/theme.css # Light/dark mode variables
│ │ └── generated/ # SVGs from keymap-drawer
│ └── _site/ # Build output (gitignored)
├── scripts/ # NEW - Build orchestration
│ ├── generate-svgs.js # Runs keymap-drawer
│ └── discover-keymaps.js # Finds all layout.yaml files
└── .github/workflows/
└── build_website.yaml # NEW - CI/CD for website
Build Pipeline
Phase 1: SVG Generation (Node.js → Python)
1. scripts/generate-svgs.js finds all layout.yaml files in keyboards/
2. For each file, runs: keymap-drawer parse -c <layout.yaml> -o website/public/generated/
3. Generates SVG for each layer (BASE, SHIFT, ALT, SHIFT_ALT, NAV)
Phase 2: Data Processing (Eleventy)
1. src/_data/keymaps.js parses all layout.yaml files using js-yaml
2. Source of Truth: layout.yaml defines the complete keymap from user perspective
3. Builds symbol accessibility map showing ALL ways to type each symbol
4. Returns array of keymap objects for template use
Phase 3: Site Build (Eleventy)
1. Index page lists all keyboards/keymaps
2. Keymap pages show layer diagrams + symbol tables
3. Templates use symbolMapping.json to convert keycodes to descriptions
Key Technical Decisions
Documentation Philosophy
User Perspective, Not Implementation:
- layout.yaml is the SOURCE OF TRUTH defining how the keymap works
- Show ALL ways to type each symbol (comprehensive symbol tables)
- Hide implementation details (don't reference keymap.c internals)
- Focus on "what can I type and how" rather than "how is this coded"
Symbol Mapping Strategy
Approach: Manual JSON file mapping QMK keycodes to metadata
- Maps KC_A → {symbol: "a", description: "Lowercase a", category: "letters"}
- Categories: letters, numbers, punctuation, common, non-printing, rare
- Fallback heuristics for unmapped codes
Why: QMK has 500+ keycodes; manual curation ensures accuracy for commonly used ones
Data Source
layout.yaml as Single Source:
- Parse layout.yaml to extract all layers and key definitions
- Show all symbol access methods defined in the YAML
- Symbol tables comprehensive: show every way to type each character
- NO parsing of keymap.c (implementation detail)
Theming Implementation
Approach: CSS custom properties with prefers-color-scheme media query
:root { --color-bg: #ffffff; }
@media (prefers-color-scheme: dark) {
:root { --color-bg: #1a1a1a; }
}
SVG theming: Override CSS classes in generated SVGs or use CSS filters
Implementation Sequence (Incremental with Checkpoints)
Milestone 1: Foundation & SVG Generation
Goal: Generate visual layer diagrams
1. Create website/ directory structure
2. Initialize package.json with Eleventy + js-yaml
3. Create basic .eleventy.js config
4. Implement scripts/generate-svgs.js
5. Install keymap-drawer (Python: pip install keymap-drawer)
6. Generate SVGs for existing qwerty keymap
Output: SVG files in website/public/generated/ferris-sweep-qwerty/
CHECKPOINT: Review SVG diagrams before proceeding
Milestone 2: Data Processing
Goal: Parse layout.yaml into structured data
1. Create symbolMapping.json with initial 50-100 keycodes
2. Implement scripts/discover-keymaps.js to find all layout.yaml files
3. Implement src/_data/keymaps.js to parse layout.yaml
4. Build data structure: array of keymaps with layers and accessible symbols
Output: Data available to templates via keymaps global
CHECKPOINT: Review data structure and symbol extraction
Milestone 3: Templates & Symbol Tables
Goal: Create web pages with comprehensive symbol tables
1. Create base.njk HTML layout
2. Create index.njk landing page (list keyboards)
3. Create keymap.njk page (embed SVGs + symbol tables)
4. Implement symbol-table.njk partial with category grouping
5. Add filters: formatKeyCombination, keycodeDesc
Output: Functional site with diagrams and comprehensive symbol tables
CHECKPOINT: Review symbol table accuracy and completeness
Milestone 4: Theming
Goal: Light/dark mode support
1. Create public/css/theme.css with light/dark variables
2. Style base layout, tables, and typography
3. Test SVG rendering in both modes
4. Implement SVG theming (CSS classes or filters)
Output: Working light/dark mode with system preference detection
CHECKPOINT: Review visual design in both themes
Milestone 5: GitHub Actions & Deployment
Goal: Automated publishing to GitHub Pages
1. Create .github/workflows/build_website.yaml
2. Configure workflow to install Python + Node.js
3. Run SVG generation → Eleventy build → deploy to Pages
4. Create .nojekyll file in root
5. Enable GitHub Pages in repo settings
Output: Live website automatically deployed on push
CHECKPOINT: Verify live deployment works
Critical Files
Existing (to read):
- keyboards/ferris/sweep/keymaps/qwerty/layout.yaml - Source data
- keyboards/ferris/sweep/keymaps/qwerty/keymap.c - Key override definitions
To create:
- website/.eleventy.js - Core configuration
- website/package.json - Dependencies
- website/src/_data/keymaps.js - Data processing pipeline (layout.yaml only)
- website/src/_data/symbolMapping.json - Keycode mappings
- scripts/generate-svgs.js - SVG generation orchestration
- scripts/discover-keymaps.js - Find all layout.yaml files
- .github/workflows/build_website.yaml - CI/CD pipeline
Local Development Workflow
# First-time setup
cd website
npm install
pip install keymap-drawer # or: pipx install keymap-drawer
# Daily development
npm run dev # Runs prebuild (SVG gen) + Eleventy dev server
# Open http://localhost:8080
# Production build
npm run build
Note: Implementation will be incremental with checkpoints after each milestone for review.
Challenges & Solutions
Challenge 1: Complex Keycode Syntax
- layout.yaml uses {h: Gui, t: d}, OSM Shift, TO(NAV), custom labels
- Solution: Parser function handles each syntax type differently
Challenge 2: Multiple Symbol Access Methods
- Some symbols accessible via multiple layers or modifier combinations
- Solution: Show all methods in table - comprehensive from user perspective
Challenge 3: Symbol Categorization
- Need to group 200+ symbols into meaningful categories
- Solution: Manual curation (letters/numbers/punctuation/common/non-printing/rare) with fallback heuristics
Challenge 4: Keeping layout.yaml as Source of Truth
- Must ensure layout.yaml is complete and doesn't require parsing keymap.c
- Solution: Document only what's in layout.yaml; update YAML if needed to be comprehensive
Success Criteria
Implementation complete when:
1. ✓ SVG diagrams display for all 5 layers
2. ✓ Symbol table shows 100+ categorized symbols
3. ✓ "Which keys to press" is accurate (spot-check 20 symbols)
4. ✓ Light/dark mode works via system preference
5. ✓ Local dev server runs with hot reload
6. ✓ GitHub Actions deploys to Pages automatically
7. ✓ Documentation includes setup instructions
Dependencies
Node.js (package.json):
- @11ty/eleventy - Static site generator
- js-yaml - YAML parsing
Python (pip/pipx):
- keymap-drawer - SVG generation tool
GitHub Actions:
- Python 3.12+ for keymap-drawer
- Node.js 20 for Eleventy
- GitHub Pages enabled in repo settings

View file

@ -0,0 +1,88 @@
#!/usr/bin/env node
const fs = require('fs');
const path = require('path');
/**
* Recursively find all layout.yaml files in a directory
*/
function findLayoutYamls(dir) {
const results = [];
const entries = fs.readdirSync(dir, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(dir, entry.name);
if (entry.isDirectory()) {
results.push(...findLayoutYamls(fullPath));
} else if (entry.name === 'layout.yaml' || entry.name === 'keymap.yaml') {
results.push(fullPath);
}
}
return results;
}
/**
* Extract keyboard/keymap identifier from path
* Example: keyboards/ferris/sweep/keymaps/qwerty/layout.yaml
* Returns: { id: 'ferris-sweep-qwerty', keyboard: 'ferris/sweep', keymap: 'qwerty' }
*/
function parseKeymapPath(layoutPath) {
const parts = layoutPath.split(path.sep);
const keyboardsIdx = parts.indexOf('keyboards');
if (keyboardsIdx === -1) {
throw new Error(`Invalid path: ${layoutPath} (no 'keyboards' directory found)`);
}
// Find 'keymaps' directory
const keymapsIdx = parts.indexOf('keymaps');
if (keymapsIdx === -1) {
throw new Error(`Invalid path: ${layoutPath} (no 'keymaps' directory found)`);
}
// Extract keyboard parts (between keyboards/ and keymaps/)
const keyboardParts = parts.slice(keyboardsIdx + 1, keymapsIdx);
const keyboard = keyboardParts.join('/');
// Extract keymap name
const keymapName = parts[keymapsIdx + 1];
// Create ID: keyboard-keymap (with dashes)
const id = [...keyboardParts, keymapName].join('-');
return {
id,
keyboard,
keymap: keymapName,
path: layoutPath
};
}
/**
* Discover all keymaps in the keyboards directory
*/
function discoverKeymaps(keyboardsDir) {
const layoutFiles = findLayoutYamls(keyboardsDir);
return layoutFiles.map(parseKeymapPath);
}
// Run if executed directly
if (require.main === module) {
const rootDir = path.join(__dirname, '..');
const keyboardsDir = path.join(rootDir, 'keyboards');
console.log('Discovering keymaps...\n');
const keymaps = discoverKeymaps(keyboardsDir);
console.log(`Found ${keymaps.length} keymap(s):\n`);
keymaps.forEach(km => {
console.log(` ${km.id}`);
console.log(` Keyboard: ${km.keyboard}`);
console.log(` Keymap: ${km.keymap}`);
console.log(` Path: ${km.path}\n`);
});
}
module.exports = { findLayoutYamls, parseKeymapPath, discoverKeymaps };

188
scripts/generate-svgs.js Normal file
View file

@ -0,0 +1,188 @@
#!/usr/bin/env node
const fs = require('fs');
const path = require('path');
const { execSync } = require('child_process');
// Try to require js-yaml from website/node_modules when run from website/
let yaml;
try {
yaml = require('js-yaml');
} catch (error) {
// If not found, try from website/node_modules (when run from root)
const yamlPath = path.join(__dirname, '..', 'website', 'node_modules', 'js-yaml');
yaml = require(yamlPath);
}
/**
* Recursively find all layout.yaml files in keyboards/ directory
*/
function findLayoutYamls(dir) {
const results = [];
const entries = fs.readdirSync(dir, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(dir, entry.name);
if (entry.isDirectory()) {
results.push(...findLayoutYamls(fullPath));
} else if (entry.name === 'layout.yaml' || entry.name === 'keymap.yaml') {
results.push(fullPath);
}
}
return results;
}
/**
* Extract keyboard/keymap identifier from path
* Example: keyboards/ferris/sweep/keymaps/qwerty/layout.yaml
* Returns: ferris-sweep-qwerty
*/
function extractKeymapId(layoutPath) {
const parts = layoutPath.split(path.sep);
const keyboardsIdx = parts.indexOf('keyboards');
if (keyboardsIdx === -1) {
throw new Error(`Invalid path: ${layoutPath} (no 'keyboards' directory found)`);
}
// Find 'keymaps' directory
const keymapsIdx = parts.indexOf('keymaps');
if (keymapsIdx === -1) {
throw new Error(`Invalid path: ${layoutPath} (no 'keymaps' directory found)`);
}
// Extract keyboard parts (between keyboards/ and keymaps/)
const keyboardParts = parts.slice(keyboardsIdx + 1, keymapsIdx);
// Extract keymap name
const keymapName = parts[keymapsIdx + 1];
// Create ID: keyboard-keymap
return [...keyboardParts, keymapName].join('-');
}
/**
* Extract layer names from layout.yaml
*/
function getLayerNames(layoutPath) {
try {
const fileContents = fs.readFileSync(layoutPath, 'utf8');
const data = yaml.load(fileContents);
if (!data.layers) {
return [];
}
return Object.keys(data.layers);
} catch (error) {
console.error(`Error parsing ${layoutPath}: ${error.message}`);
return [];
}
}
/**
* Generate SVGs for a layout.yaml file using keymap-drawer
*/
function generateSvgs(layoutPath, outputDir) {
const keymapId = extractKeymapId(layoutPath);
const svgOutputDir = path.join(outputDir, keymapId);
// Create output directory
if (!fs.existsSync(svgOutputDir)) {
fs.mkdirSync(svgOutputDir, { recursive: true });
}
console.log(`Generating SVGs for: ${keymapId}`);
console.log(` Input: ${layoutPath}`);
console.log(` Output: ${svgOutputDir}`);
// Get layer names from layout.yaml
const layerNames = getLayerNames(layoutPath);
if (layerNames.length === 0) {
console.error(` ✗ No layers found in ${layoutPath}\n`);
throw new Error('No layers found');
}
console.log(` Layers: ${layerNames.join(', ')}`);
// Generate SVG for each layer
let generatedCount = 0;
for (const layerName of layerNames) {
const outputFile = path.join(svgOutputDir, `${layerName.toLowerCase()}.svg`);
try {
// Run keymap draw command for this layer
const cmd = `keymap draw "${layoutPath}" -s ${layerName} -o "${outputFile}"`;
execSync(cmd, { stdio: 'pipe' });
generatedCount++;
} catch (error) {
console.error(` ✗ Error generating ${layerName}: ${error.message}`);
throw error;
}
}
console.log(` ✓ Generated ${generatedCount} SVG(s) successfully\n`);
}
/**
* Main function
*/
function main() {
const rootDir = path.join(__dirname, '..');
const keyboardsDir = path.join(rootDir, 'keyboards');
const outputDir = path.join(rootDir, 'website', 'public', 'generated');
console.log('=== Keymap SVG Generator ===\n');
// Check if keymap is installed (from keymap-drawer package)
try {
execSync('keymap --version', { stdio: 'pipe' });
} catch (error) {
console.error('ERROR: keymap (from keymap-drawer) is not installed!');
console.error('Install it with: pip install keymap-drawer');
console.error('Or with pipx: pipx install keymap-drawer\n');
process.exit(1);
}
// Find all layout.yaml files
console.log(`Searching for layout.yaml files in: ${keyboardsDir}\n`);
const layoutFiles = findLayoutYamls(keyboardsDir);
if (layoutFiles.length === 0) {
console.log('No layout.yaml files found.');
return;
}
console.log(`Found ${layoutFiles.length} layout file(s):\n`);
// Generate SVGs for each layout
let successCount = 0;
let errorCount = 0;
for (const layoutPath of layoutFiles) {
try {
generateSvgs(layoutPath, outputDir);
successCount++;
} catch (error) {
errorCount++;
}
}
console.log('=== Summary ===');
console.log(`✓ Success: ${successCount}`);
console.log(`✗ Errors: ${errorCount}`);
if (errorCount > 0) {
process.exit(1);
}
}
// Run if executed directly
if (require.main === module) {
main();
}
module.exports = { findLayoutYamls, extractKeymapId, generateSvgs };

69
website/.eleventy.js Normal file
View file

@ -0,0 +1,69 @@
module.exports = function(eleventyConfig) {
// Pass through static assets
eleventyConfig.addPassthroughCopy("public");
// Watch for changes in keyboard configuration files during dev
eleventyConfig.addWatchTarget("../keyboards/**/*.yaml");
// Custom filter: Get keycode description from symbol mapping
eleventyConfig.addFilter("keycodeDesc", function(keycode) {
const mapping = this.ctx.symbolMapping || {};
return mapping[keycode] || {
symbol: null,
description: keycode,
category: 'unknown'
};
});
// Custom filter: Find item in array by property
eleventyConfig.addFilter("find", function(array, property, value) {
if (!Array.isArray(array)) return null;
return array.find(item => item[property] === value);
});
// Custom filter: Format key combination for display
eleventyConfig.addFilter("formatKeyCombination", function(combo) {
if (!combo) return '—';
let parts = [];
// Add modifiers
if (combo.mods && combo.mods.length > 0) {
parts.push(...combo.mods);
}
// Add base key
if (combo.baseKey) {
parts.push(combo.baseKey);
}
// Handle special cases
if (combo.modTapHold) {
return `Hold ${combo.modTapHold}${combo.withKey ? ' + ' + combo.withKey : ''}`;
}
if (combo.layerSwitch) {
return `Layer ${combo.layerSwitch}${combo.baseKey ? ' → ' + combo.baseKey : ''}`;
}
return parts.join(' + ') || '—';
});
// Custom filter: Replace text
eleventyConfig.addFilter("replace", function(str, search, replace) {
if (typeof str !== 'string') return str;
return str.replace(new RegExp(search, 'g'), replace);
});
return {
dir: {
input: "src",
output: "_site",
includes: "_includes",
data: "_data"
},
templateFormats: ["njk", "md", "html"],
htmlTemplateEngine: "njk",
markdownTemplateEngine: "njk"
};
};

16
website/.gitignore vendored Normal file
View file

@ -0,0 +1,16 @@
# Build output
_site/
# Generated SVG files (built from layout.yaml via scripts/generate-svgs.js)
public/generated/
# Dependencies
node_modules/
# Logs
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# OS files
.DS_Store

1713
website/package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

18
website/package.json Normal file
View file

@ -0,0 +1,18 @@
{
"name": "qmk-keymap-docs",
"version": "1.0.0",
"description": "Documentation website for QMK keymaps",
"scripts": {
"prebuild": "node ../scripts/generate-svgs.js",
"build": "eleventy",
"dev": "npm run prebuild && eleventy --serve",
"clean": "rm -rf _site public/generated/*"
},
"keywords": ["qmk", "keyboard", "keymap", "documentation"],
"author": "",
"license": "MIT",
"devDependencies": {
"@11ty/eleventy": "^3.1.0",
"js-yaml": "^4.1.0"
}
}

View file

@ -0,0 +1,103 @@
/**
* QMK Keymap Documentation Theme
* Light/Dark mode support using CSS custom properties
*/
:root {
/* Light mode colors (default) */
--color-bg: #ffffff;
--color-text: #1a1a1a;
--color-heading: #2c3e50;
--color-muted: #6c757d;
--color-border: #dee2e6;
/* Links and accents */
--color-link: #007bff;
--color-link-hover: #0056b3;
--color-accent: #007bff;
/* Buttons */
--color-primary: #007bff;
--color-primary-hover: #0056b3;
--color-secondary: #6c757d;
--color-secondary-hover: #5a6268;
/* Cards and surfaces */
--color-card-bg: #f8f9fa;
--color-layer-bg: #ffffff;
/* Tables */
--color-table-bg: #ffffff;
--color-table-header-bg: #e9ecef;
--color-table-border: #dee2e6;
--color-table-row-hover: #f8f9fa;
/* Code blocks */
--color-code-bg: #f8f9fa;
--color-code-text: #e83e8c;
/* SVG diagram colors */
--svg-filter: none;
}
@media (prefers-color-scheme: dark) {
:root {
/* Dark mode colors */
--color-bg: #1a1a1a;
--color-text: #e9ecef;
--color-heading: #f8f9fa;
--color-muted: #adb5bd;
--color-border: #495057;
/* Links and accents */
--color-link: #66b3ff;
--color-link-hover: #99ccff;
--color-accent: #66b3ff;
/* Buttons */
--color-primary: #0d6efd;
--color-primary-hover: #0a58ca;
--color-secondary: #6c757d;
--color-secondary-hover: #5a6268;
/* Cards and surfaces */
--color-card-bg: #212529;
--color-layer-bg: #2b3035;
/* Tables */
--color-table-bg: #212529;
--color-table-header-bg: #343a40;
--color-table-border: #495057;
--color-table-row-hover: #2c3136;
/* Code blocks */
--color-code-bg: #2d3238;
--color-code-text: #ff6b9d;
/* SVG diagram theming - invert colors for dark mode */
--svg-filter: invert(0.9) hue-rotate(180deg);
}
}
/* Force dark mode for testing - uncomment to test */
/*
:root {
color-scheme: dark;
}
*/
/* SVG theming */
svg,
object[type="image/svg+xml"] {
filter: var(--svg-filter);
}
/* Ensure smooth transitions when switching themes */
* {
transition: background-color 0.2s ease, color 0.2s ease, border-color 0.2s ease;
}
/* Prevent transition on page load */
.preload * {
transition: none !important;
}

View file

@ -0,0 +1,590 @@
const fs = require('fs');
const path = require('path');
const yaml = require('js-yaml');
// Import discovery utilities
const discoverPath = path.join(__dirname, '../../../scripts/discover-keymaps.js');
const { discoverKeymaps } = require(discoverPath);
// Load symbol mapping
const symbolMapping = require('./symbolMapping.json');
// Position-to-coordinate mapping for LAYOUT_split_3x5_2 (Ferris Sweep)
// Layout is stored as rows: [L L L L L R R R R R] × 3 rows + [thumb thumb thumb thumb]
// 34 keys total: 3 rows × 10 keys (5L + 5R) + 4 thumb keys (2L + 2R)
const POSITION_TO_COORDINATE = [
// Row 1 (top): positions 0-9
'LPT', 'LRT', 'LMT', 'LIT', 'LET', // Left hand (0-4): q w e r t
'RET', 'RIT', 'RMT', 'RRT', 'RPT', // Right hand (5-9): y u i o p
// Row 2 (home): positions 10-19
'LPH', 'LRH', 'LMH', 'LIH', 'LEH', // Left hand (10-14): a s d f g
'REH', 'RIH', 'RMH', 'RRH', 'RPH', // Right hand (15-19): h j k l ;
// Row 3 (bottom): positions 20-29
'LPB', 'LRB', 'LMB', 'LIB', 'LEB', // Left hand (20-24): z x c v b
'REB', 'RIB', 'RMB', 'RRB', 'RPB', // Right hand (25-29): n m Repeat . /
// Thumbs: positions 30-33
'LTO', 'LTI', // Left thumbs (30-31): TO(NAV), OSM Shift
'RTI', 'RTO' // Right thumbs (32-33): OSM Alt, Space
];
// Layer type classification
const LAYER_TYPES = {
real: ['BASE', 'NAV', 'ALT_NAV', 'SYMBOL', 'SHIFT_SYMBOL'], // Physical layers accessed via layer-switch keys
ghost: ['BASE_SHIFT', 'BASE_ALT', 'BASE_SHIFT_ALT'] // Virtual layers accessed via modifiers
};
/**
* Parse a key definition from layout.yaml
* Handles: simple keys, labeled keys, mod-tap, one-shot mods, layer switches
*/
function parseKeyDefinition(keyDef) {
if (keyDef === null) {
return { type: 'empty', keycode: null, label: '—' };
}
// Handle string definitions
if (typeof keyDef === 'string') {
// Custom label (quoted in YAML)
if (keyDef.includes('+') || keyDef.includes(' ')) {
return { type: 'labeled', keycode: null, label: keyDef };
}
// Map common key names to keycodes (check this first, before pattern matching)
const keycodeMap = {
'Space': 'KC_SPACE',
'Esc': 'KC_ESC',
'Tab': 'KC_TAB',
'Enter': 'KC_ENTER',
'BSpace': 'KC_BSPC',
'Repeat': 'QK_REPEAT_KEY',
'Trans': 'KC_TRNS',
'Left': 'KC_LEFT',
'Right': 'KC_RIGHT',
'Up': 'KC_UP',
'Down': 'KC_DOWN'
};
if (keycodeMap[keyDef]) {
return { type: 'keycode', keycode: keycodeMap[keyDef], label: keyDef };
}
// Check if it's a known keycode pattern (all uppercase)
if (keyDef.match(/^[A-Z_]+$/)) {
const mappedKeycode = `KC_${keyDef.toUpperCase()}`;
return { type: 'keycode', keycode: mappedKeycode, label: keyDef };
}
// Single character
if (keyDef.length === 1) {
const keycode = `KC_${keyDef.toUpperCase()}`;
return { type: 'char', keycode, label: keyDef };
}
// Fallback: treat as label
return { type: 'labeled', keycode: null, label: keyDef };
}
// Handle object definitions (mod-tap, one-shot, layer switches, held keys)
if (typeof keyDef === 'object') {
// Check for "held" type (modifier keys that enable the current layer)
if (keyDef.type === 'held') {
return {
type: 'held',
keycode: null,
label: '(held)'
};
}
// Handle tap behavior (t: value)
if (keyDef.t !== undefined) {
const tapValue = keyDef.t;
// Mod-tap with hold modifier
if (keyDef.h !== undefined) {
const holdMod = keyDef.h;
const tapKey = parseKeyDefinition(tapValue);
return {
type: 'mod-tap',
holdMod,
tapKey,
label: `${tapValue} (${holdMod} hold)`
};
}
// One-shot modifier or layer switch (just tap behavior)
if (tapValue.startsWith('OSM ')) {
const modifier = tapValue.substring(4);
return {
type: 'one-shot-mod',
modifier,
keycode: `OSM(MOD_L${modifier.toUpperCase()})`,
label: `OSM ${modifier}`
};
}
if (tapValue.startsWith('TO(')) {
const layerName = tapValue.substring(3, tapValue.length - 1);
return {
type: 'layer-switch',
targetLayer: layerName,
keycode: tapValue,
label: `${layerName}`
};
}
// Simple tap value
return parseKeyDefinition(tapValue);
}
}
return { type: 'unknown', keycode: null, label: String(keyDef) };
}
/**
* Get human-readable description for a shifted symbol
*/
function getShiftedDescription(symbol, originalDescription) {
// For uppercase letters, use "Uppercase X" instead of "Shifted Lowercase x"
if (symbol && symbol.length === 1 && symbol >= 'A' && symbol <= 'Z') {
return `Uppercase ${symbol}`;
}
// For other symbols, use the symbol itself as description
return symbol;
}
/**
* Get the modifier needed to access a layer
*/
function getLayerModifier(layerName) {
if (layerName === 'BASE_SHIFT') return 'LTI'; // OSM Shift on left thumb inside
if (layerName === 'BASE_ALT') return 'RTI'; // OSM Alt on right thumb inside
if (layerName === 'BASE_SHIFT_ALT') return 'LTI+RTI'; // Both modifiers
if (layerName === 'SHIFT_SYMBOL') return 'LTI'; // Shift on SYMBOL layer
if (layerName === 'ALT_NAV') return 'RTI'; // Alt on NAV layer
return null;
}
/**
* Categorize a labeled symbol that doesn't have a keycode mapping
*/
function categorizeLabeledSymbol(label, layerName) {
// Symbol descriptions map
const symbolDescriptions = {
'`': 'Backtick',
'@': 'At sign',
'#': 'Hash',
'$': 'Dollar sign',
'%': 'Percent sign',
'^': 'Caret',
'&': 'Ampersand',
'*': 'Asterisk',
'-': 'Hyphen',
'+': 'Plus sign',
'=': 'Equals sign',
'_': 'Underscore',
'~': 'Tilde',
"'": 'Single quote',
'"': 'Double quote',
'(': 'Left parenthesis',
')': 'Right parenthesis',
'[': 'Left bracket',
']': 'Right bracket',
'{': 'Left brace',
'}': 'Right brace',
'<': 'Less than',
'>': 'Greater than',
'/': 'Forward slash',
'\\': 'Backslash',
'|': 'Pipe',
',': 'Comma',
'.': 'Period',
':': 'Colon',
';': 'Semicolon',
'?': 'Question mark',
'!': 'Exclamation mark',
'€': 'Euro sign',
'£': 'Pound sterling',
'\u201C': 'Opening double quote',
'\u201D': 'Closing double quote',
'\u2018': 'Opening single quote',
'\u2019': 'Closing single quote',
// macOS Alt+letter symbols (SYMBOL layer)
'œ': 'Latin ligature oe',
'∑': 'Summation',
'´': 'Acute accent',
'®': 'Registered trademark',
'†': 'Dagger',
'¥': 'Yen sign',
'¨': 'Diaeresis',
'ˆ': 'Circumflex',
'ø': 'O with stroke',
'π': 'Pi',
'å': 'A with ring',
'ß': 'Sharp S (eszett)',
'∂': 'Partial derivative',
'ƒ': 'Function (florin)',
'©': 'Copyright',
'˙': 'Dot above',
'∆': 'Delta',
'˚': 'Ring above',
'¬': 'Not sign',
'…': 'Ellipsis',
'Ω': 'Omega',
'≈': 'Approximately equal',
'ç': 'C cedilla',
'√': 'Square root',
'∫': 'Integral',
'˜': 'Small tilde',
'~': 'Tilde',
'µ': 'Micro sign',
'≤': 'Less than or equal',
'≥': 'Greater than or equal',
'÷': 'Division sign',
// macOS Shift+Alt+letter symbols (SHIFT_SYMBOL layer)
'Œ': 'Latin ligature OE',
'„': 'Double low quote',
'‰': 'Per mille',
'‡': 'Double dagger',
'ˇ': 'Caron',
'Á': 'A acute',
'Â': 'A circumflex',
'Ê': 'E circumflex',
'Ë': 'E diaeresis',
'¯': 'Macron',
'ˆ': 'Circumflex',
'Ø': 'O with stroke',
'∏': 'Product',
'Å': 'A with ring',
'Í': 'I acute',
'Î': 'I circumflex',
'Ï': 'I diaeresis',
'Ì': 'I grave',
'˝': 'Double acute',
'Ó': 'O acute',
'Ô': 'O circumflex',
'Ò': 'O grave',
'Ú': 'U acute',
'Û': 'U circumflex',
'Ù': 'U grave',
'Æ': 'Latin ligature AE',
'¸': 'Cedilla',
'': 'Fraction slash',
'Ç': 'C cedilla',
'◊': 'Lozenge',
'ı': 'Dotless i',
'˘': 'Breve',
'¿': 'Inverted question mark'
};
// Keypad numbers (already formatted as "KP_X")
if (/^KP_\d+$/.test(label)) {
return { category: 'numbers', priority: 1, description: `Keypad ${label.substring(3)}` };
}
// Function keys
if (/^F\d+$/.test(label)) {
return { category: 'non-printing', priority: 3, description: `Function key ${label}` };
}
// Command combinations
if (label.includes('Cmd+') || label.includes('Ctrl+') || label.includes('Alt+') || label.includes('Shift+')) {
return { category: 'commands', priority: 4, description: label };
}
// Special words/labels
if (label === 'Repeat') {
return { category: 'special', priority: 3, description: 'Repeat last key' };
}
if (label === 'Alt Repeat') {
return { category: 'special', priority: 3, description: 'Alternate repeat' };
}
if (label.includes('Word')) {
return { category: 'special', priority: 3, description: label };
}
// Symbols from SYMBOL and SHIFT_SYMBOL layers are always rare
if (layerName === 'SYMBOL' || layerName === 'SHIFT_SYMBOL') {
const description = symbolDescriptions[label] || label;
return { category: 'rare', priority: 4, description };
}
// £ and € from BASE_ALT or BASE_SHIFT_ALT layers are common (not rare)
const baseLayerSymbols = ['€', '£'];
const isBaseLayer = layerName === 'BASE_ALT' || layerName === 'BASE_SHIFT_ALT' || layerName === 'BASE' || layerName === 'BASE_SHIFT';
const isCurlyQuote = ['\u201C', '\u201D', '\u2018', '\u2019'].includes(label);
// Check if we have a description for this symbol
if (symbolDescriptions[label]) {
let category = 'common';
let priority = 2;
// Curly quotes are always rare
if (isCurlyQuote) {
category = 'rare';
priority = 4;
}
// If £ or € are on base layers (BASE_ALT/BASE_SHIFT_ALT), they're common
// Otherwise (like if they appear on SYMBOL layer), they're rare
else if (baseLayerSymbols.includes(label) && !isBaseLayer) {
category = 'rare';
priority = 4;
}
return { category, priority, description: symbolDescriptions[label] };
}
// Default to common with the label as description
return { category: 'common', priority: 2, description: label };
}
/**
* Extract all symbols that can be typed from a parsed key
*/
function extractSymbolsFromKey(parsedKey, layerName, position) {
const symbols = [];
// Don't extract symbols from empty keys or held modifier keys
if (parsedKey.type === 'empty' || parsedKey.type === 'held') {
return symbols;
}
// For mod-tap keys, extract symbols from the tap behavior
if (parsedKey.type === 'mod-tap' && parsedKey.tapKey) {
return extractSymbolsFromKey(parsedKey.tapKey, layerName, position);
}
// Get position coordinate
const coordinate = POSITION_TO_COORDINATE[position] || `pos${position}`;
// Get layer modifier (for ghost layers)
const layerModifier = getLayerModifier(layerName);
// Get symbol mapping if available
const mapping = parsedKey.keycode ? symbolMapping[parsedKey.keycode] : null;
if (mapping) {
// Determine which symbol to extract based on the key label
// If label is uppercase (e.g., 'A'), extract only the shifted symbol
// If label is lowercase (e.g., 'a'), extract only the unshifted symbol
const isUppercase = parsedKey.label && parsedKey.label.length === 1 &&
parsedKey.label === parsedKey.label.toUpperCase() &&
parsedKey.label !== parsedKey.label.toLowerCase();
if (isUppercase && mapping.shiftedSymbol) {
// Extract shifted symbol (uppercase letter)
const shiftedDescription = getShiftedDescription(mapping.shiftedSymbol, mapping.description);
const notation = layerModifier ? `${layerModifier}+${coordinate}` : coordinate;
symbols.push({
symbol: mapping.shiftedSymbol,
description: shiftedDescription,
category: mapping.category,
priority: mapping.priority,
layer: layerName,
position,
coordinate,
notation,
method: layerModifier ? 'modifier' : 'direct',
keyLabel: parsedKey.label
});
} else if (!isUppercase && mapping.symbol) {
// Extract unshifted symbol (lowercase letter or other)
const notation = layerModifier ? `${layerModifier}+${coordinate}` : coordinate;
symbols.push({
symbol: mapping.symbol,
description: mapping.description,
category: mapping.category,
priority: mapping.priority,
layer: layerName,
position,
coordinate,
notation,
method: layerModifier ? 'modifier' : 'direct',
keyLabel: parsedKey.label
});
}
} else if ((parsedKey.type === 'labeled' || parsedKey.type === 'char') && parsedKey.label && parsedKey.label !== '—') {
// For labeled keys or char keys without keycode mapping, use the label itself
const notation = layerModifier ? `${layerModifier}+${coordinate}` : coordinate;
const { category, priority, description } = categorizeLabeledSymbol(parsedKey.label, layerName);
symbols.push({
symbol: parsedKey.label,
description,
category,
priority,
layer: layerName,
position,
coordinate,
notation,
method: layerModifier ? 'modifier' : 'direct',
keyLabel: parsedKey.label
});
}
return symbols;
}
/**
* Parse layer data from layout.yaml
*/
function parseLayerData(layoutData) {
const layers = [];
for (const [layerName, layerKeys] of Object.entries(layoutData.layers || {})) {
const keys = [];
const allSymbols = [];
let position = 0;
// Flatten the layer key array (it's a 2D array representing rows)
for (const row of layerKeys) {
for (const keyDef of row) {
const parsedKey = parseKeyDefinition(keyDef);
keys.push(parsedKey);
// Extract symbols from this key
const symbols = extractSymbolsFromKey(parsedKey, layerName, position);
allSymbols.push(...symbols);
position++;
}
}
layers.push({
name: layerName,
keys,
symbols: allSymbols
});
}
return layers;
}
/**
* Get the real layer that a ghost layer belongs to
*/
function getRealLayer(layerName) {
if (LAYER_TYPES.ghost.includes(layerName)) {
return 'Layer 0 (Base)'; // Ghost layers belong to Layer 0
}
if (layerName === 'BASE') return 'Layer 0 (Base)';
if (layerName === 'NAV') return 'Layer 1 (Nav)';
if (layerName === 'ALT_NAV') return 'Layer 1 (Nav)'; // Alt on NAV layer
if (layerName === 'SYMBOL') return 'Layer 2 (Symbol)';
if (layerName === 'SHIFT_SYMBOL') return 'Layer 2 (Symbol)'; // Shift on SYMBOL layer
return layerName;
}
/**
* Build keymap data structure
*/
function buildKeymapData(keymapInfo) {
try {
// Read and parse layout.yaml
const fileContents = fs.readFileSync(keymapInfo.path, 'utf8');
const layoutData = yaml.load(fileContents);
// Parse layers
const layers = parseLayerData(layoutData);
// Collect all unique symbols grouped by real layers
const allSymbolsMap = new Map();
for (const layer of layers) {
for (const symbol of layer.symbols) {
const key = `${symbol.symbol}|${symbol.description}`;
if (!allSymbolsMap.has(key)) {
allSymbolsMap.set(key, {
symbol: symbol.symbol,
description: symbol.description,
category: symbol.category,
priority: symbol.priority,
accessMethods: {} // Keyed by real layer name
});
}
const symbolData = allSymbolsMap.get(key);
const realLayer = getRealLayer(symbol.layer);
// Initialize array for this real layer if needed
if (!symbolData.accessMethods[realLayer]) {
symbolData.accessMethods[realLayer] = [];
}
// Add this access method
symbolData.accessMethods[realLayer].push({
layer: symbol.layer,
position: symbol.position,
coordinate: symbol.coordinate,
notation: symbol.notation,
method: symbol.method,
keyLabel: symbol.keyLabel
});
}
}
// Convert map to sorted array
const allSymbols = Array.from(allSymbolsMap.values()).sort((a, b) => {
// Sort by: priority, then category, then symbol
if (a.priority !== b.priority) return a.priority - b.priority;
if (a.category !== b.category) return a.category.localeCompare(b.category);
return (a.symbol || '').localeCompare(b.symbol || '');
});
// Define available real layers
const realLayers = [
{ id: 'layer0', name: 'Layer 0 (Base)' },
{ id: 'layer1', name: 'Layer 1 (Nav)' },
{ id: 'layer2', name: 'Layer 2 (Symbol)' }
];
return {
id: keymapInfo.id,
keyboard: keymapInfo.keyboard,
keymapName: keymapInfo.keymap,
layoutFile: keymapInfo.path,
layers,
allSymbols,
realLayers,
svgPaths: layers.map(layer => ({
name: layer.name,
path: `/public/generated/${keymapInfo.id}/${layer.name.toLowerCase()}.svg`
}))
};
} catch (error) {
console.error(`Error parsing keymap ${keymapInfo.id}:`, error.message);
return null;
}
}
/**
* Main data export for Eleventy
*/
module.exports = function() {
const rootDir = path.join(__dirname, '../../..');
const keyboardsDir = path.join(rootDir, 'keyboards');
console.log('\n[Eleventy Data] Discovering keymaps...');
// Discover all keymaps
const keymapInfos = discoverKeymaps(keyboardsDir);
console.log(`[Eleventy Data] Found ${keymapInfos.length} keymap(s)`);
// Build data for each keymap
const keymaps = keymapInfos
.map(buildKeymapData)
.filter(km => km !== null);
console.log(`[Eleventy Data] Processed ${keymaps.length} keymap(s)`);
// Log some stats
keymaps.forEach(km => {
console.log(` ${km.id}: ${km.layers.length} layers, ${km.allSymbols.length} unique symbols`);
});
return keymaps;
};

View file

@ -0,0 +1,117 @@
{
"_comment": "Maps QMK keycodes to human-readable descriptions and categories",
"KC_A": { "symbol": "a", "shiftedSymbol": "A", "description": "Lowercase a", "category": "letters", "priority": 1 },
"KC_B": { "symbol": "b", "shiftedSymbol": "B", "description": "Lowercase b", "category": "letters", "priority": 1 },
"KC_C": { "symbol": "c", "shiftedSymbol": "C", "description": "Lowercase c", "category": "letters", "priority": 1 },
"KC_D": { "symbol": "d", "shiftedSymbol": "D", "description": "Lowercase d", "category": "letters", "priority": 1 },
"KC_E": { "symbol": "e", "shiftedSymbol": "E", "description": "Lowercase e", "category": "letters", "priority": 1 },
"KC_F": { "symbol": "f", "shiftedSymbol": "F", "description": "Lowercase f", "category": "letters", "priority": 1 },
"KC_G": { "symbol": "g", "shiftedSymbol": "G", "description": "Lowercase g", "category": "letters", "priority": 1 },
"KC_H": { "symbol": "h", "shiftedSymbol": "H", "description": "Lowercase h", "category": "letters", "priority": 1 },
"KC_I": { "symbol": "i", "shiftedSymbol": "I", "description": "Lowercase i", "category": "letters", "priority": 1 },
"KC_J": { "symbol": "j", "shiftedSymbol": "J", "description": "Lowercase j", "category": "letters", "priority": 1 },
"KC_K": { "symbol": "k", "shiftedSymbol": "K", "description": "Lowercase k", "category": "letters", "priority": 1 },
"KC_L": { "symbol": "l", "shiftedSymbol": "L", "description": "Lowercase l", "category": "letters", "priority": 1 },
"KC_M": { "symbol": "m", "shiftedSymbol": "M", "description": "Lowercase m", "category": "letters", "priority": 1 },
"KC_N": { "symbol": "n", "shiftedSymbol": "N", "description": "Lowercase n", "category": "letters", "priority": 1 },
"KC_O": { "symbol": "o", "shiftedSymbol": "O", "description": "Lowercase o", "category": "letters", "priority": 1 },
"KC_P": { "symbol": "p", "shiftedSymbol": "P", "description": "Lowercase p", "category": "letters", "priority": 1 },
"KC_Q": { "symbol": "q", "shiftedSymbol": "Q", "description": "Lowercase q", "category": "letters", "priority": 1 },
"KC_R": { "symbol": "r", "shiftedSymbol": "R", "description": "Lowercase r", "category": "letters", "priority": 1 },
"KC_S": { "symbol": "s", "shiftedSymbol": "S", "description": "Lowercase s", "category": "letters", "priority": 1 },
"KC_T": { "symbol": "t", "shiftedSymbol": "T", "description": "Lowercase t", "category": "letters", "priority": 1 },
"KC_U": { "symbol": "u", "shiftedSymbol": "U", "description": "Lowercase u", "category": "letters", "priority": 1 },
"KC_V": { "symbol": "v", "shiftedSymbol": "V", "description": "Lowercase v", "category": "letters", "priority": 1 },
"KC_W": { "symbol": "w", "shiftedSymbol": "W", "description": "Lowercase w", "category": "letters", "priority": 1 },
"KC_X": { "symbol": "x", "shiftedSymbol": "X", "description": "Lowercase x", "category": "letters", "priority": 1 },
"KC_Y": { "symbol": "y", "shiftedSymbol": "Y", "description": "Lowercase y", "category": "letters", "priority": 1 },
"KC_Z": { "symbol": "z", "shiftedSymbol": "Z", "description": "Lowercase z", "category": "letters", "priority": 1 },
"KC_1": { "symbol": "1", "shiftedSymbol": "!", "description": "Number 1", "category": "numbers", "priority": 1 },
"KC_2": { "symbol": "2", "shiftedSymbol": "@", "description": "Number 2", "category": "numbers", "priority": 1 },
"KC_3": { "symbol": "3", "shiftedSymbol": "#", "description": "Number 3", "category": "numbers", "priority": 1 },
"KC_4": { "symbol": "4", "shiftedSymbol": "$", "description": "Number 4", "category": "numbers", "priority": 1 },
"KC_5": { "symbol": "5", "shiftedSymbol": "%", "description": "Number 5", "category": "numbers", "priority": 1 },
"KC_6": { "symbol": "6", "shiftedSymbol": "^", "description": "Number 6", "category": "numbers", "priority": 1 },
"KC_7": { "symbol": "7", "shiftedSymbol": "&", "description": "Number 7", "category": "numbers", "priority": 1 },
"KC_8": { "symbol": "8", "shiftedSymbol": "*", "description": "Number 8", "category": "numbers", "priority": 1 },
"KC_9": { "symbol": "9", "shiftedSymbol": "(", "description": "Number 9", "category": "numbers", "priority": 1 },
"KC_0": { "symbol": "0", "shiftedSymbol": ")", "description": "Number 0", "category": "numbers", "priority": 1 },
"KC_KP_0": { "symbol": "0", "description": "Keypad 0", "category": "numbers", "priority": 1 },
"KC_KP_1": { "symbol": "1", "description": "Keypad 1", "category": "numbers", "priority": 1 },
"KC_KP_2": { "symbol": "2", "description": "Keypad 2", "category": "numbers", "priority": 1 },
"KC_KP_3": { "symbol": "3", "description": "Keypad 3", "category": "numbers", "priority": 1 },
"KC_KP_4": { "symbol": "4", "description": "Keypad 4", "category": "numbers", "priority": 1 },
"KC_KP_5": { "symbol": "5", "description": "Keypad 5", "category": "numbers", "priority": 1 },
"KC_KP_6": { "symbol": "6", "description": "Keypad 6", "category": "numbers", "priority": 1 },
"KC_KP_7": { "symbol": "7", "description": "Keypad 7", "category": "numbers", "priority": 1 },
"KC_KP_8": { "symbol": "8", "description": "Keypad 8", "category": "numbers", "priority": 1 },
"KC_KP_9": { "symbol": "9", "description": "Keypad 9", "category": "numbers", "priority": 1 },
"KC_DOT": { "symbol": ".", "shiftedSymbol": ">", "description": "Period", "category": "punctuation", "priority": 2 },
"KC_COMMA": { "symbol": ",", "shiftedSymbol": "<", "description": "Comma", "category": "punctuation", "priority": 2 },
"KC_SCLN": { "symbol": ";", "shiftedSymbol": ":", "description": "Semicolon", "category": "punctuation", "priority": 2 },
"KC_SLSH": { "symbol": "/", "shiftedSymbol": "?", "description": "Forward slash", "category": "punctuation", "priority": 2 },
"KC_QUOTE": { "symbol": "'", "shiftedSymbol": "\"", "description": "Single quote", "category": "punctuation", "priority": 2 },
"KC_GRAVE": { "symbol": "`", "shiftedSymbol": "~", "description": "Backtick", "category": "punctuation", "priority": 2 },
"KC_MINUS": { "symbol": "-", "shiftedSymbol": "_", "description": "Minus/Underscore", "category": "punctuation", "priority": 2 },
"KC_EQUAL": { "symbol": "=", "shiftedSymbol": "+", "description": "Equals/Plus", "category": "punctuation", "priority": 2 },
"KC_LBRC": { "symbol": "[", "shiftedSymbol": "{", "description": "Left bracket", "category": "punctuation", "priority": 2 },
"KC_RBRC": { "symbol": "]", "shiftedSymbol": "}", "description": "Right bracket", "category": "punctuation", "priority": 2 },
"KC_BACKSLASH": { "symbol": "\\", "shiftedSymbol": "|", "description": "Backslash/Pipe", "category": "punctuation", "priority": 2 },
"KC_AT": { "symbol": "@", "description": "At sign", "category": "common", "priority": 2 },
"KC_HASH": { "symbol": "#", "description": "Hash/Pound", "category": "common", "priority": 2 },
"KC_DOLLAR": { "symbol": "$", "description": "Dollar sign", "category": "common", "priority": 2 },
"KC_PERCENT": { "symbol": "%", "description": "Percent", "category": "common", "priority": 2 },
"KC_CIRCUMFLEX": { "symbol": "^", "description": "Caret", "category": "common", "priority": 2 },
"KC_AMPERSAND": { "symbol": "&", "description": "Ampersand", "category": "common", "priority": 2 },
"KC_ASTERISK": { "symbol": "*", "description": "Asterisk", "category": "common", "priority": 2 },
"KC_LPRN": { "symbol": "(", "description": "Left parenthesis", "category": "common", "priority": 2 },
"KC_RPRN": { "symbol": ")", "description": "Right parenthesis", "category": "common", "priority": 2 },
"KC_UNDERSCORE": { "symbol": "_", "description": "Underscore", "category": "common", "priority": 2 },
"KC_PLUS": { "symbol": "+", "description": "Plus", "category": "common", "priority": 2 },
"KC_LCBR": { "symbol": "{", "description": "Left curly brace", "category": "common", "priority": 2 },
"KC_RCBR": { "symbol": "}", "description": "Right curly brace", "category": "common", "priority": 2 },
"KC_PIPE": { "symbol": "|", "description": "Pipe", "category": "common", "priority": 2 },
"KC_COLON": { "symbol": ":", "description": "Colon", "category": "common", "priority": 2 },
"KC_DQUO": { "symbol": "\"", "description": "Double quote", "category": "common", "priority": 2 },
"KC_TILDE": { "symbol": "~", "description": "Tilde", "category": "common", "priority": 2 },
"KC_LABK": { "symbol": "<", "description": "Less than", "category": "common", "priority": 2 },
"KC_RABK": { "symbol": ">", "description": "Greater than", "category": "common", "priority": 2 },
"KC_QUESTION": { "symbol": "?", "description": "Question mark", "category": "common", "priority": 2 },
"KC_EXCLAIM": { "symbol": "!", "description": "Exclamation mark", "category": "common", "priority": 2 },
"KC_SPACE": { "symbol": "␣", "description": "Space", "category": "invisible", "priority": 2 },
"KC_ENTER": { "symbol": "↵", "description": "Enter", "category": "invisible", "priority": 2 },
"KC_ESC": { "symbol": "⎋", "description": "Escape", "category": "invisible", "priority": 2 },
"KC_BSPC": { "symbol": "⌫", "description": "Backspace", "category": "invisible", "priority": 3 },
"KC_TAB": { "symbol": "⇥", "description": "Tab", "category": "invisible", "priority": 2 },
"KC_DELETE": { "symbol": "⌦", "description": "Delete", "category": "invisible", "priority": 3 },
"KC_LEFT": { "symbol": "←", "description": "Left arrow", "category": "navigation", "priority": 3 },
"KC_RIGHT": { "symbol": "→", "description": "Right arrow", "category": "navigation", "priority": 3 },
"KC_UP": { "symbol": "↑", "description": "Up arrow", "category": "navigation", "priority": 3 },
"KC_DOWN": { "symbol": "↓", "description": "Down arrow", "category": "navigation", "priority": 3 },
"KC_F1": { "symbol": null, "description": "Function key F1", "category": "non-printing", "priority": 3 },
"KC_F2": { "symbol": null, "description": "Function key F2", "category": "non-printing", "priority": 3 },
"KC_F3": { "symbol": null, "description": "Function key F3", "category": "non-printing", "priority": 3 },
"KC_F4": { "symbol": null, "description": "Function key F4", "category": "non-printing", "priority": 3 },
"KC_F5": { "symbol": null, "description": "Function key F5", "category": "non-printing", "priority": 3 },
"KC_F6": { "symbol": null, "description": "Function key F6", "category": "non-printing", "priority": 3 },
"KC_F7": { "symbol": null, "description": "Function key F7", "category": "non-printing", "priority": 3 },
"KC_F8": { "symbol": null, "description": "Function key F8", "category": "non-printing", "priority": 3 },
"KC_F9": { "symbol": null, "description": "Function key F9", "category": "non-printing", "priority": 3 },
"KC_F10": { "symbol": null, "description": "Function key F10", "category": "non-printing", "priority": 3 },
"KC_F11": { "symbol": null, "description": "Function key F11", "category": "non-printing", "priority": 3 },
"KC_F12": { "symbol": null, "description": "Function key F12", "category": "non-printing", "priority": 3 },
"QK_REPEAT_KEY": { "symbol": null, "description": "Repeat last key", "category": "non-printing", "priority": 3 },
"QK_ALT_REPEAT_KEY": { "symbol": null, "description": "Alt repeat", "category": "non-printing", "priority": 3 },
"KC_EURO": { "symbol": "€", "description": "Euro sign", "category": "rare", "priority": 4 },
"KC_UK_POUND": { "symbol": "£", "description": "Pound sterling", "category": "rare", "priority": 4 }
}

View file

@ -0,0 +1,206 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ title }} | QMK Keymap Documentation</title>
<link rel="stylesheet" href="/public/css/theme.css">
<style>
* {
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
max-width: 1400px;
margin: 0 auto;
padding: 2rem;
line-height: 1.6;
background: var(--color-bg, #fff);
color: var(--color-text, #1a1a1a);
}
header {
border-bottom: 2px solid var(--color-border, #e0e0e0);
margin-bottom: 2rem;
padding-bottom: 1rem;
}
h1 {
margin: 0 0 0.5rem 0;
color: var(--color-heading, #333);
}
nav {
margin-top: 1rem;
}
nav a {
color: var(--color-link, #007bff);
text-decoration: none;
margin-right: 1rem;
}
nav a:hover {
text-decoration: underline;
}
main {
min-height: 60vh;
}
footer {
margin-top: 4rem;
padding-top: 2rem;
border-top: 1px solid var(--color-border, #e0e0e0);
text-align: center;
color: var(--color-muted, #666);
font-size: 0.9rem;
}
.keyboard-group {
margin: 2rem 0;
padding: 1.5rem;
border: 1px solid var(--color-border, #ddd);
border-radius: 8px;
background: var(--color-card-bg, #f9f9f9);
}
.keyboard-group h2, .keyboard-group h3 {
margin-top: 0;
color: var(--color-heading, #333);
}
.button {
display: inline-block;
padding: 0.5rem 1rem;
margin: 0.5rem 0.5rem 0.5rem 0;
background: var(--color-primary, #007bff);
color: white;
text-decoration: none;
border-radius: 4px;
font-weight: 500;
transition: background 0.2s;
}
.button:hover {
background: var(--color-primary-hover, #0056b3);
}
.button.secondary {
background: var(--color-secondary, #6c757d);
}
.button.secondary:hover {
background: var(--color-secondary-hover, #5a6268);
}
.stats {
color: var(--color-muted, #666);
font-size: 0.95rem;
margin-top: 1rem;
}
.layer-diagrams {
margin: 3rem 0;
}
.layer {
margin: 2rem 0;
padding: 1.5rem;
background: var(--color-layer-bg, #fff);
border: 1px solid var(--color-border, #ddd);
border-radius: 8px;
}
.layer h3 {
margin-top: 0;
color: var(--color-heading, #333);
}
.layer svg {
width: 100%;
height: auto;
max-width: 1200px;
display: block;
margin: 1rem auto;
}
table {
width: 100%;
border-collapse: collapse;
margin: 1.5rem 0;
font-size: 0.95rem;
background: var(--color-table-bg, #fff);
}
thead {
background: var(--color-table-header-bg, #f0f0f0);
position: sticky;
top: 0;
}
th, td {
padding: 0.75rem;
text-align: left;
border: 1px solid var(--color-table-border, #ddd);
}
th {
font-weight: 600;
color: var(--color-heading, #333);
}
tbody tr:hover {
background: var(--color-table-row-hover, #f5f5f5);
}
.symbol-char {
font-size: 1.2rem;
font-weight: 600;
text-align: center;
font-family: "SF Mono", Monaco, "Cascadia Code", "Roboto Mono", Consolas, monospace;
}
.not-available {
color: var(--color-muted, #999);
text-align: center;
}
.category-section {
margin: 3rem 0;
}
.category-section h3 {
color: var(--color-heading, #333);
border-bottom: 2px solid var(--color-accent, #007bff);
padding-bottom: 0.5rem;
margin-bottom: 1rem;
}
code {
background: var(--color-code-bg, #f5f5f5);
padding: 0.2rem 0.4rem;
border-radius: 3px;
font-family: "SF Mono", Monaco, "Cascadia Code", "Roboto Mono", Consolas, monospace;
font-size: 0.9em;
}
</style>
</head>
<body>
<header>
<h1>QMK Keymap Documentation</h1>
<nav>
<a href="/">Home</a>
</nav>
</header>
<main>
{{ content | safe }}
</main>
<footer>
<p>Generated with <a href="https://www.11ty.dev/">Eleventy</a> and <a href="https://github.com/caksoylar/keymap-drawer">keymap-drawer</a></p>
</footer>
</body>
</html>

View file

@ -0,0 +1,77 @@
{# Symbol table component - displays all symbols grouped by category #}
{% set categories = {
'letters': 'Letters (a-z, A-Z)',
'numbers': 'Numbers (0-9)',
'punctuation': 'Punctuation',
'common': 'Common Symbols',
'invisible': 'Invisible Characters',
'navigation': 'Navigation',
'commands': 'Common Commands',
'special': 'Special Keys',
'non-printing': 'Function Keys & Non-Printing',
'rare': 'Rare & Unicode Symbols'
} %}
{% for categoryKey, categoryName in categories %}
{# Filter symbols by this category #}
{% set categorySymbols = [] %}
{% for symbol in keymap.allSymbols %}
{% if symbol.category == categoryKey %}
{% set categorySymbols = (categorySymbols.push(symbol), categorySymbols) %}
{% endif %}
{% endfor %}
{% if categorySymbols.length > 0 %}
<div class="category-section">
<h4>{{ categoryName }}</h4>
<table>
<thead>
<tr>
<th style="width: 80px;">Symbol</th>
<th style="width: 200px;">Description</th>
{% for realLayer in keymap.realLayers %}
<th>{{ realLayer.name }}</th>
{% endfor %}
</tr>
</thead>
<tbody>
{% for symbol in categorySymbols %}
<tr>
<td class="symbol-char">
{% if symbol.symbol %}
{{ symbol.symbol }}
{% else %}
<span class="not-available">—</span>
{% endif %}
</td>
<td>{{ symbol.description }}</td>
{# Show how to access this symbol from each real layer #}
{% for realLayer in keymap.realLayers %}
<td>
{% set accessMethods = symbol.accessMethods[realLayer.name] %}
{% if accessMethods and accessMethods.length > 0 %}
{% for method in accessMethods %}
<div style="margin: 0.25rem 0;">
<code>{{ method.notation }}</code>
</div>
{% endfor %}
{% else %}
<span class="not-available">—</span>
{% endif %}
</td>
{% endfor %}
</tr>
{% endfor %}
</tbody>
</table>
<p style="color: var(--color-muted, #666); font-size: 0.9rem; margin-top: 0.5rem;">
{{ categorySymbols.length }} {{ 'symbol' if categorySymbols.length == 1 else 'symbols' }} in this category
</p>
</div>
{% endif %}
{% endfor %}

34
website/src/index.njk Normal file
View file

@ -0,0 +1,34 @@
---
layout: layouts/base.njk
title: Home
---
<h2>QMK Keymaps</h2>
<p>Browse visual documentation and comprehensive symbol tables for all configured QMK keymaps.</p>
{% if keymaps.length > 0 %}
<h2>Available Keymaps</h2>
{% for keymap in keymaps %}
<div class="keyboard-group">
<h3>{{ keymap.keyboard }}</h3>
<p><strong>Keymap:</strong> {{ keymap.keymapName }}</p>
<a href="/keymaps/{{ keymap.id }}/index.html" class="button">View Keymap Documentation</a>
<div class="stats">
<p>
<strong>Layers:</strong> {{ keymap.layers | length }}
({% for layer in keymap.layers %}{{ layer.name }}{% if not loop.last %}, {% endif %}{% endfor %})
</p>
<p>
<strong>Unique Symbols:</strong> {{ keymap.allSymbols.length }} symbols and keys documented
</p>
</div>
</div>
{% endfor %}
{% else %}
<p><em>No keymaps found. Make sure you have layout.yaml files in your keyboards/ directory.</em></p>
{% endif %}

55
website/src/keymaps.njk Normal file
View file

@ -0,0 +1,55 @@
---
layout: layouts/base.njk
pagination:
data: keymaps
size: 1
alias: keymap
permalink: "/keymaps/{{ keymap.id }}/index.html"
eleventyComputed:
title: "{{ keymap.keyboard }} - {{ keymap.keymapName }}"
---
<h2>{{ keymap.keyboard }} - {{ keymap.keymapName }}</h2>
<p class="stats">
<strong>{{ keymap.layers.length }} layers</strong> with <strong>{{ keymap.allSymbols.length }} unique symbols</strong>
</p>
<nav style="margin: 2rem 0;">
<a href="#diagrams" class="button">Layer Diagrams</a>
<a href="#symbols" class="button">Symbol Index</a>
<a href="/" class="button secondary">← Back to Home</a>
</nav>
<section id="diagrams" class="layer-diagrams">
<h3>Layer Diagrams</h3>
{% for layer in keymap.layers %}
<div class="layer">
<h4>Layer: {{ layer.name }}</h4>
{% set svgPath = keymap.svgPaths | find('name', layer.name) %}
{% if svgPath %}
<div class="svg-container">
<object data="{{ svgPath.path }}" type="image/svg+xml" style="width: 100%; max-width: 1200px;">
<img src="{{ svgPath.path }}" alt="{{ layer.name }} layer diagram" />
</object>
</div>
{% else %}
<p><em>SVG diagram not available for this layer.</em></p>
{% endif %}
<details style="margin-top: 1rem;">
<summary>Layer details ({{ layer.keys.length }} keys, {{ layer.symbols.length }} symbols)</summary>
<p style="margin-top: 1rem;">This layer defines {{ layer.symbols.length }} accessible symbols/keys.</p>
</details>
</div>
{% endfor %}
</section>
<section id="symbols" class="symbol-tables">
<h3>Symbol Index</h3>
<p>Find any symbol and see how to type it on this keyboard. Symbols are organized by category for easy reference.</p>
{% include "partials/symbol-table.njk" %}
</section>