diff --git a/docs/docs/codes/keymap-upgrader.mdx b/docs/docs/codes/keymap-upgrader.mdx new file mode 100644 index 00000000..bcee82b5 --- /dev/null +++ b/docs/docs/codes/keymap-upgrader.mdx @@ -0,0 +1,21 @@ +--- +title: Keymap Upgrader +sidebar_label: Keymap Upgrader +hide_title: true +hide_table_of_contents: true +--- + +# Keymap Upgrader + +Many codes have been renamed to be more consistent with each other. +Paste the contents of a `.keymap` file below to upgrade all deprecated codes to their replacements. + +Hover your mouse over the upgraded keymap and click the `Copy` button to copy it to your clipboard. + +You will likely need to realign columns in the upgraded keymap. The upgrader also does not handle +codes inside a `#define`, so you will need to update those manually using +[this list of deprecated codes and replacements](https://github.com/zmkfirmware/zmk/blob/main/docs/src/data/keymap-upgrade.js). + +import KeymapUpgrader from "@site/src/components/KeymapUpgrader/index"; + + diff --git a/docs/docusaurus.config.js b/docs/docusaurus.config.js index 2716039c..acf51f4a 100644 --- a/docs/docusaurus.config.js +++ b/docs/docusaurus.config.js @@ -1,3 +1,5 @@ +const path = require("path"); + module.exports = { title: "ZMK Firmware", tagline: "Modern, open source keyboard firmware", @@ -6,6 +8,7 @@ module.exports = { favicon: "img/favicon.ico", organizationName: "zmkfirmware", // Usually your GitHub org/user name. projectName: "zmk", // Usually your repo name. + plugins: [path.resolve(__dirname, "src/docusaurus-tree-sitter-plugin")], themeConfig: { googleAnalytics: { trackingID: "UA-145201102-2", diff --git a/docs/package-lock.json b/docs/package-lock.json index 757beb04..0e48a97b 100644 --- a/docs/package-lock.json +++ b/docs/package-lock.json @@ -14,16 +14,20 @@ "@fortawesome/react-fontawesome": "^0.1.12", "classnames": "^2.2.6", "react": "^16.8.4", + "react-async": "^10.0.1", "react-copy-to-clipboard": "^5.0.2", "react-dom": "^16.8.4", - "react-toastify": "^6.0.9" + "react-toastify": "^6.0.9", + "web-tree-sitter": "^0.17.1" }, "devDependencies": { "eslint": "^7.12.0", "eslint-config-prettier": "^6.14.0", "eslint-plugin-mdx": "^1.8.2", "eslint-plugin-react": "^7.21.5", - "prettier": "2.1.2" + "null-loader": "^3.0.0", + "prettier": "2.1.2", + "string-replace-loader": "2.3" } }, "node_modules/@algolia/cache-browser-local-storage": { @@ -11321,6 +11325,14 @@ "prop-types": "15.7.2" } }, + "node_modules/react-async": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/react-async/-/react-async-10.0.1.tgz", + "integrity": "sha512-ORUz5ca0B57QgBIzEZM5SuhJ6xFjkvEEs0gylLNlWf06vuVcLZsjIw3wx58jJkZG38p+0nUAxRgFW2b7mnVZzA==", + "peerDependencies": { + "react": ">=16.3.1" + } + }, "node_modules/react-base16-styling": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/react-base16-styling/-/react-base16-styling-0.6.0.tgz", @@ -13334,6 +13346,45 @@ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" }, + "node_modules/string-replace-loader": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/string-replace-loader/-/string-replace-loader-2.3.0.tgz", + "integrity": "sha512-HYBIHStViMKLZC/Lehxy42OuwsBaPzX/LjcF5mkJlE2SnHXmW6SW6eiHABTXnY8ZCm/REbdJ8qnA0ptmIzN0Ng==", + "dev": true, + "dependencies": { + "loader-utils": "^1.2.3", + "schema-utils": "^2.6.5" + }, + "peerDependencies": { + "webpack": "1 || 2 || 3 || 4" + } + }, + "node_modules/string-replace-loader/node_modules/json5": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", + "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", + "dev": true, + "dependencies": { + "minimist": "^1.2.0" + }, + "bin": { + "json5": "lib/cli.js" + } + }, + "node_modules/string-replace-loader/node_modules/loader-utils": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.0.tgz", + "integrity": "sha512-qH0WSMBtn/oHuwjy/NucEgbx5dbxxnxup9s4PVXJUDHZBQY+s0NWA9rJf53RBnQZxfch7euUui7hpoAPvALZdA==", + "dev": true, + "dependencies": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^1.0.1" + }, + "engines": { + "node": ">=4.0.0" + } + }, "node_modules/string-width": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", @@ -14794,6 +14845,7 @@ "version": "1.2.13", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.13.tgz", "integrity": "sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==", + "hasInstallScript": true, "optional": true, "dependencies": { "bindings": "1.5.0", @@ -14929,6 +14981,11 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/web-tree-sitter": { + "version": "0.17.1", + "resolved": "https://registry.npmjs.org/web-tree-sitter/-/web-tree-sitter-0.17.1.tgz", + "integrity": "sha512-QgaeV+wmlB1Qaw9rS5a0ZDBt8GRcKkF+hGNSVxQ/HLm1lPCow3BKOhoILaXkYm7YozCcL7TjppRADBwFJugbuA==" + }, "node_modules/webidl-conversions": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz", @@ -15202,6 +15259,7 @@ "version": "1.2.13", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.13.tgz", "integrity": "sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==", + "hasInstallScript": true, "optional": true, "dependencies": { "bindings": "1.5.0", @@ -26900,6 +26958,12 @@ "prop-types": "15.7.2" } }, + "react-async": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/react-async/-/react-async-10.0.1.tgz", + "integrity": "sha512-ORUz5ca0B57QgBIzEZM5SuhJ6xFjkvEEs0gylLNlWf06vuVcLZsjIw3wx58jJkZG38p+0nUAxRgFW2b7mnVZzA==", + "requires": {} + }, "react-base16-styling": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/react-base16-styling/-/react-base16-styling-0.6.0.tgz", @@ -28878,6 +28942,38 @@ } } }, + "string-replace-loader": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/string-replace-loader/-/string-replace-loader-2.3.0.tgz", + "integrity": "sha512-HYBIHStViMKLZC/Lehxy42OuwsBaPzX/LjcF5mkJlE2SnHXmW6SW6eiHABTXnY8ZCm/REbdJ8qnA0ptmIzN0Ng==", + "dev": true, + "requires": { + "loader-utils": "^1.2.3", + "schema-utils": "^2.6.5" + }, + "dependencies": { + "json5": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", + "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", + "dev": true, + "requires": { + "minimist": "^1.2.0" + } + }, + "loader-utils": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.0.tgz", + "integrity": "sha512-qH0WSMBtn/oHuwjy/NucEgbx5dbxxnxup9s4PVXJUDHZBQY+s0NWA9rJf53RBnQZxfch7euUui7hpoAPvALZdA==", + "dev": true, + "requires": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^1.0.1" + } + } + } + }, "string-width": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", @@ -30421,6 +30517,11 @@ "resolved": "https://registry.npmjs.org/web-namespaces/-/web-namespaces-1.1.4.tgz", "integrity": "sha512-wYxSGajtmoP4WxfejAPIr4l0fVh+jeMXZb08wNc0tMg6xsfZXj3cECqIK0G7ZAqUq0PP8WlMDtaOGVBTAWztNw==" }, + "web-tree-sitter": { + "version": "0.17.1", + "resolved": "https://registry.npmjs.org/web-tree-sitter/-/web-tree-sitter-0.17.1.tgz", + "integrity": "sha512-QgaeV+wmlB1Qaw9rS5a0ZDBt8GRcKkF+hGNSVxQ/HLm1lPCow3BKOhoILaXkYm7YozCcL7TjppRADBwFJugbuA==" + }, "webidl-conversions": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz", diff --git a/docs/package.json b/docs/package.json index 00dc6275..5249bcff 100644 --- a/docs/package.json +++ b/docs/package.json @@ -21,9 +21,11 @@ "@fortawesome/react-fontawesome": "^0.1.12", "classnames": "^2.2.6", "react": "^16.8.4", + "react-async": "^10.0.1", "react-copy-to-clipboard": "^5.0.2", "react-dom": "^16.8.4", - "react-toastify": "^6.0.9" + "react-toastify": "^6.0.9", + "web-tree-sitter": "^0.17.1" }, "browserslist": { "production": [ @@ -42,6 +44,8 @@ "eslint-config-prettier": "^6.14.0", "eslint-plugin-mdx": "^1.8.2", "eslint-plugin-react": "^7.21.5", - "prettier": "2.1.2" + "null-loader": "^3.0.0", + "prettier": "2.1.2", + "string-replace-loader": "2.3" } } diff --git a/docs/sidebars.js b/docs/sidebars.js index 7d124cc6..8fc1dc54 100644 --- a/docs/sidebars.js +++ b/docs/sidebars.js @@ -35,6 +35,7 @@ module.exports = { "codes/applications", "codes/input-assist", "codes/power", + "codes/keymap-upgrader", ], Development: [ "development/clean-room", diff --git a/docs/src/components/KeymapUpgrader/index.jsx b/docs/src/components/KeymapUpgrader/index.jsx new file mode 100644 index 00000000..8d3a60b2 --- /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

Loading...

; + } + + if (error) { + return

Error: {error.message}

; + } + + return ; +} + +function Editor() { + const [keymap, setKeymap] = React.useState(""); + const upgraded = upgradeKeymap(keymap); + + return ( +
+ +
+ {upgraded} +
+
+ ); +} diff --git a/docs/src/components/KeymapUpgrader/styles.module.css b/docs/src/components/KeymapUpgrader/styles.module.css new file mode 100644 index 00000000..31e06b97 --- /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 00000000..bc83c951 --- /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 00000000..7803a9e9 --- /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 00000000..19a5d8e3 --- /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} 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(""); +} diff --git a/docs/static/tree-sitter-devicetree.wasm b/docs/static/tree-sitter-devicetree.wasm new file mode 100644 index 00000000..b0729e4d Binary files /dev/null and b/docs/static/tree-sitter-devicetree.wasm differ diff --git a/docs/static/tree-sitter.wasm b/docs/static/tree-sitter.wasm new file mode 100644 index 00000000..029b7ca0 Binary files /dev/null and b/docs/static/tree-sitter.wasm differ