feat(docs): add keymap upgrader

Added a documentation page with a script that upgrades deprecated key
codes and behaviors to their replacements.

Fixes #299
This commit is contained in:
Joel Spadin
2020-11-07 18:03:20 -06:00
committed by innovaker
parent 77c16b020e
commit 5aa8a07aa9
12 changed files with 562 additions and 4 deletions

View File

@@ -0,0 +1,47 @@
/*
* Copyright (c) 2020 The ZMK Contributors
*
* SPDX-License-Identifier: CC-BY-NC-SA-4.0
*/
import React from "react";
import { useAsync } from "react-async";
import { initParser, upgradeKeymap } from "@site/src/keymap-upgrade";
import CodeBlock from "@theme/CodeBlock";
import styles from "./styles.module.css";
export default function KeymapUpgrader() {
const { error, isPending } = useAsync(initParser);
if (isPending) {
return <p>Loading...</p>;
}
if (error) {
return <p className="error">Error: {error.message}</p>;
}
return <Editor />;
}
function Editor() {
const [keymap, setKeymap] = React.useState("");
const upgraded = upgradeKeymap(keymap);
return (
<div>
<textarea
className={styles.editor}
placeholder="Paste keymap here"
spellCheck={false}
value={keymap}
onChange={(e) => setKeymap(e.target.value)}
></textarea>
<div className={styles.result}>
<CodeBlock metastring={'title="Upgraded Keymap"'}>{upgraded}</CodeBlock>
</div>
</div>
);
}

View File

@@ -0,0 +1,26 @@
/*
* Copyright (c) 2020 The ZMK Contributors
*
* SPDX-License-Identifier: CC-BY-NC-SA-4.0
*/
.editor {
font-family: var(--ifm-font-family-monospace);
font-size: var(--ifm-font-size-base);
line-height: var(--ifm-pre-line-height);
tab-size: 4;
color: var(--ifm-pre-color);
background-color: var(--ifm-pre-background);
border: none;
border-radius: var(--ifm-pre-border-radius);
width: 100%;
min-height: 10em;
padding: var(--ifm-pre-padding);
}
.result {
tab-size: 4;
}

View File

@@ -0,0 +1,84 @@
/*
* Copyright (c) 2020 The ZMK Contributors
*
* SPDX-License-Identifier: CC-BY-NC-SA-4.0
*/
export const Codes = {
NUM_1: "N1",
NUM_2: "N2",
NUM_3: "N3",
NUM_4: "N4",
NUM_5: "N5",
NUM_6: "N6",
NUM_7: "N7",
NUM_8: "N8",
NUM_9: "N9",
NUM_0: "N0",
BKSP: "BSPC",
SPC: "SPACE",
EQL: "EQUAL",
TILD: "TILDE",
SCLN: "SEMI",
QUOT: "SQT",
GRAV: "GRAVE",
CMMA: "COMMA",
PRSC: "PSCRN",
SCLK: "SLCK",
PAUS: "PAUSE_BREAK",
PGUP: "PG_UP",
PGDN: "PG_DN",
RARW: "RIGHT",
LARW: "LEFT",
DARW: "DOWN",
UARW: "UP",
KDIV: "KP_DIVIDE",
KMLT: "KP_MULTIPLY",
KMIN: "KP_MINUS",
KPLS: "KP_PLUS",
UNDO: "K_UNDO",
CUT: "K_CUT",
COPY: "K_COPY",
PSTE: "K_PASTE",
VOLU: "K_VOL_UP",
VOLD: "K_VOL_DN",
CURU: "DLLR",
LPRN: "LPAR",
RPRN: "RPAR",
LCUR: "LBRC",
RCUR: "RBRC",
CRRT: "CARET",
PRCT: "PRCNT",
LABT: "LT",
RABT: "GT",
COLN: "COLON",
KSPC: null,
ATSN: "AT",
BANG: "EXCL",
LCTL: "LCTRL",
LSFT: "LSHFT",
RCTL: "RCTRL",
RSFT: "RSHFT",
M_NEXT: "C_NEXT",
M_PREV: "C_PREV",
M_STOP: "C_STOP",
M_EJCT: "C_EJECT",
M_PLAY: "C_PP",
M_MUTE: "C_MUTE",
M_VOLU: "C_VOL_UP",
M_VOLD: "C_VOL_DN",
GUI: "K_CMENU",
MOD_LCTL: "LCTRL",
MOD_LSFT: "LSHFT",
MOD_LALT: "LALT",
MOD_LGUI: "LGUI",
MOD_RCTL: "RCTRL",
MOD_RSFT: "RSHFT",
MOD_RALT: "RALT",
MOD_RGUI: "RGUI",
};
export const Behaviors = {
cp: "kp",
inc_dec_cp: "inc_dec_kp",
};

View File

@@ -0,0 +1,39 @@
module.exports = function () {
return {
configureWebpack(config, isServer) {
let rules = [];
// Tree-sitter is only used for client-side code.
// Don't try to load it on the server.
if (isServer) {
rules.push({
test: /web-tree-sitter/,
loader: "null-loader",
});
} else {
// web-tree-sitter has a hard-coded path to tree-sitter.wasm,
// (see https://github.com/tree-sitter/tree-sitter/issues/559)
// which some browsers treat as absolute and others as relative.
// This breaks everything. Rewrite it to always use an absolute path.
rules.push({
test: /tree-sitter\.js$/,
loader: "string-replace-loader",
options: {
search: '"tree-sitter.wasm"',
replace: '"/tree-sitter.wasm"',
strict: true,
},
});
}
return {
// web-tree-sitter tries to import "fs", which can be ignored.
// https://github.com/tree-sitter/tree-sitter/issues/466
node: {
fs: "empty",
},
module: { rules },
};
},
};
};

232
docs/src/keymap-upgrade.js Normal file
View File

@@ -0,0 +1,232 @@
import Parser from "web-tree-sitter";
import { Codes, Behaviors } from "./data/keymap-upgrade";
let Devicetree;
export async function initParser() {
await Parser.init();
Devicetree = await Parser.Language.load("/tree-sitter-devicetree.wasm");
}
function createParser() {
if (!Devicetree) {
throw new Error("Parser not loaded. Call initParser() first.");
}
const parser = new Parser();
parser.setLanguage(Devicetree);
return parser;
}
export function upgradeKeymap(text) {
const parser = createParser();
const tree = parser.parse(text);
const edits = [...upgradeBehaviors(tree), ...upgradeKeycodes(tree)];
return applyEdits(text, edits);
}
class TextEdit {
/**
* Creates a text edit to replace a range or node with new text.
* Construct with one of:
*
* * `Edit(startIndex, endIndex, newText)`
* * `Edit(node, newText)`
*/
constructor(startIndex, endIndex, newText) {
if (typeof startIndex !== "number") {
const node = startIndex;
newText = endIndex;
startIndex = node.startIndex;
endIndex = node.endIndex;
}
/** @type number */
this.startIndex = startIndex;
/** @type number */
this.endIndex = endIndex;
/** @type string */
this.newText = newText;
}
}
/**
* Upgrades deprecated behavior references.
* @param {Parser.Tree} tree
*/
function upgradeBehaviors(tree) {
/** @type TextEdit[] */
let edits = [];
const query = Devicetree.query("(reference label: (identifier) @ref)");
const matches = query.matches(tree.rootNode);
for (const { captures } of matches) {
const node = findCapture("ref", captures);
if (node) {
edits.push(...getUpgradeEdits(node, Behaviors));
}
}
return edits;
}
/**
* Upgrades deprecated key code identifiers.
* @param {Parser.Tree} tree
*/
function upgradeKeycodes(tree) {
/** @type TextEdit[] */
let edits = [];
// No need to filter to the bindings array. The C preprocessor would have
// replaced identifiers anywhere, so upgrading all identifiers preserves the
// original behavior of the keymap (even if that behavior wasn't intended).
const query = Devicetree.query("(identifier) @name");
const matches = query.matches(tree.rootNode);
for (const { captures } of matches) {
const node = findCapture("name", captures);
if (node) {
edits.push(...getUpgradeEdits(node, Codes, keycodeReplaceHandler));
}
}
return edits;
}
/**
* @param {Parser.SyntaxNode} node
* @param {string | null} replacement
* @returns TextEdit[]
*/
function keycodeReplaceHandler(node, replacement) {
if (replacement) {
return [new TextEdit(node, replacement)];
}
const nodes = findBehaviorNodes(node);
if (nodes.length === 0) {
console.warn(
`Found deprecated code "${node.text}" but it is not a parameter to a behavior`
);
return [new TextEdit(node, `/* "${node.text}" no longer exists */`)];
}
const oldText = nodes.map((n) => n.text).join(" ");
const newText = `&none /* "${oldText}" no longer exists */`;
const startIndex = nodes[0].startIndex;
const endIndex = nodes[nodes.length - 1].endIndex;
return [new TextEdit(startIndex, endIndex, newText)];
}
/**
* Returns the node for the named capture.
* @param {string} name
* @param {any[]} captures
* @returns {Parser.SyntaxNode | null}
*/
function findCapture(name, captures) {
for (const c of captures) {
if (c.name === name) {
return c.node;
}
}
return null;
}
/**
* Given a parameter to a keymap behavior, returns a list of nodes beginning
* with the behavior and including all parameters.
* Returns an empty array if no behavior was found.
* @param {Parser.SyntaxNode} paramNode
*/
function findBehaviorNodes(paramNode) {
// Walk backwards from the given parameter to find the behavior reference.
let behavior = paramNode.previousNamedSibling;
while (behavior && behavior.type !== "reference") {
behavior = behavior.previousNamedSibling;
}
if (!behavior) {
return [];
}
// Walk forward from the behavior to collect all its parameters.
let nodes = [behavior];
let param = behavior.nextNamedSibling;
while (param && param.type !== "reference") {
nodes.push(param);
param = param.nextNamedSibling;
}
return nodes;
}
/**
* Gets a list of text edits to apply based on a node and a map of text
* replacements.
*
* If replaceHandler is given, it will be called if the node matches a
* deprecated value and it should return the text edits to apply.
*
* @param {Parser.SyntaxNode} node
* @param {Map<string, string | null>} replacementMap
* @param {(node: Parser.SyntaxNode, replacement: string | null) => TextEdit[]} replaceHandler
*/
function getUpgradeEdits(node, replacementMap, replaceHandler = undefined) {
for (const [deprecated, replacement] of Object.entries(replacementMap)) {
if (node.text === deprecated) {
if (replaceHandler) {
return replaceHandler(node, replacement);
} else {
return [new TextEdit(node, replacement)];
}
}
}
return [];
}
/**
* Sorts a list of text edits in ascending order by position.
* @param {TextEdit[]} edits
*/
function sortEdits(edits) {
return edits.sort((a, b) => a.startIndex - b.startIndex);
}
/**
* Returns a string with text replacements applied.
* @param {string} text
* @param {TextEdit[]} edits
*/
function applyEdits(text, edits) {
edits = sortEdits(edits);
/** @type string[] */
const chunks = [];
let currentIndex = 0;
for (const edit of edits) {
if (edit.startIndex < currentIndex) {
console.warn("discarding overlapping edit", edit);
continue;
}
chunks.push(text.substring(currentIndex, edit.startIndex));
chunks.push(edit.newText);
currentIndex = edit.endIndex;
}
chunks.push(text.substring(currentIndex));
return chunks.join("");
}