diff options
Diffstat (limited to 'docs/src')
-rw-r--r-- | docs/src/components/KeymapUpgrader/index.jsx | 47 | ||||
-rw-r--r-- | docs/src/components/KeymapUpgrader/styles.module.css | 26 | ||||
-rw-r--r-- | docs/src/data/keymap-upgrade.js | 84 | ||||
-rw-r--r-- | docs/src/docusaurus-tree-sitter-plugin/index.js | 39 | ||||
-rw-r--r-- | docs/src/keymap-upgrade.js | 232 |
5 files changed, 428 insertions, 0 deletions
diff --git a/docs/src/components/KeymapUpgrader/index.jsx b/docs/src/components/KeymapUpgrader/index.jsx new file mode 100644 index 0000000..8d3a60b --- /dev/null +++ b/docs/src/components/KeymapUpgrader/index.jsx @@ -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> + ); +} diff --git a/docs/src/components/KeymapUpgrader/styles.module.css b/docs/src/components/KeymapUpgrader/styles.module.css new file mode 100644 index 0000000..31e06b9 --- /dev/null +++ b/docs/src/components/KeymapUpgrader/styles.module.css @@ -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; +} diff --git a/docs/src/data/keymap-upgrade.js b/docs/src/data/keymap-upgrade.js new file mode 100644 index 0000000..bc83c95 --- /dev/null +++ b/docs/src/data/keymap-upgrade.js @@ -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", +}; diff --git a/docs/src/docusaurus-tree-sitter-plugin/index.js b/docs/src/docusaurus-tree-sitter-plugin/index.js new file mode 100644 index 0000000..7803a9e --- /dev/null +++ b/docs/src/docusaurus-tree-sitter-plugin/index.js @@ -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 }, + }; + }, + }; +}; diff --git a/docs/src/keymap-upgrade.js b/docs/src/keymap-upgrade.js new file mode 100644 index 0000000..19a5d8e --- /dev/null +++ b/docs/src/keymap-upgrade.js @@ -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(""); +} |