summaryrefslogtreecommitdiff
path: root/docs
diff options
context:
space:
mode:
Diffstat (limited to 'docs')
-rw-r--r--docs/docs/codes/keymap-upgrader.mdx21
-rw-r--r--docs/docusaurus.config.js3
-rw-r--r--docs/package-lock.json105
-rw-r--r--docs/package.json8
-rw-r--r--docs/sidebars.js1
-rw-r--r--docs/src/components/KeymapUpgrader/index.jsx47
-rw-r--r--docs/src/components/KeymapUpgrader/styles.module.css26
-rw-r--r--docs/src/data/keymap-upgrade.js84
-rw-r--r--docs/src/docusaurus-tree-sitter-plugin/index.js39
-rw-r--r--docs/src/keymap-upgrade.js232
-rw-r--r--docs/static/tree-sitter-devicetree.wasmbin0 -> 43042 bytes
-rw-r--r--docs/static/tree-sitter.wasmbin0 -> 202338 bytes
12 files changed, 562 insertions, 4 deletions
diff --git a/docs/docs/codes/keymap-upgrader.mdx b/docs/docs/codes/keymap-upgrader.mdx
new file mode 100644
index 0000000..bcee82b
--- /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";
+
+<KeymapUpgrader />
diff --git a/docs/docusaurus.config.js b/docs/docusaurus.config.js
index 2716039..acf51f4 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 757beb0..0e48a97 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 00dc627..5249bcf 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 7d124cc..8fc1dc5 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 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("");
+}
diff --git a/docs/static/tree-sitter-devicetree.wasm b/docs/static/tree-sitter-devicetree.wasm
new file mode 100644
index 0000000..b0729e4
--- /dev/null
+++ b/docs/static/tree-sitter-devicetree.wasm
Binary files differ
diff --git a/docs/static/tree-sitter.wasm b/docs/static/tree-sitter.wasm
new file mode 100644
index 0000000..029b7ca
--- /dev/null
+++ b/docs/static/tree-sitter.wasm
Binary files differ