├── .npmignore ├── .vscode ├── extensions.json └── settings.json ├── images ├── label-order.png └── oddly-shaped.png ├── .travis.yml ├── tsconfig.json ├── LICENSE ├── package.json ├── .gitignore ├── index.ts ├── README.md └── test └── test.js /.npmignore: -------------------------------------------------------------------------------- 1 | coverage/ 2 | .vscode/ 3 | .travis.yml 4 | *.tgz 5 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["esbenp.prettier-vscode"] 3 | } 4 | -------------------------------------------------------------------------------- /images/label-order.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ijprest/kle-serial/HEAD/images/label-order.png -------------------------------------------------------------------------------- /images/oddly-shaped.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ijprest/kle-serial/HEAD/images/oddly-shaped.png -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | node_js: 4 | - stable 5 | 6 | install: 7 | - npm install 8 | 9 | script: 10 | - npm run cover 11 | 12 | # Send coverage data to Coveralls 13 | after_script: "cat coverage/lcov.info | node_modules/coveralls/bin/coveralls.js" 14 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "commonjs", 5 | "declaration": true, 6 | "outDir": "./dist", 7 | "strict": true, 8 | "noImplicitAny": false, 9 | "plugins": [ 10 | { "transform": "ts-transformer-keys/transformer" } 11 | ] 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.tabSize": 2, 3 | 4 | "files.trimFinalNewlines": false, 5 | "files.insertFinalNewline": true, 6 | "files.trimTrailingWhitespace": true, 7 | "files.eol": "\n", 8 | "files.encoding": "utf8", 9 | "files.exclude": { 10 | "**/.git": true, 11 | "**/.svn": true, 12 | "**/.hg": true, 13 | "**/CVS": true, 14 | "**/.DS_Store": true, 15 | "dist/**": true, 16 | "node_modules/**": true, 17 | "npm-debug.log": true, 18 | "coverage/**": true, 19 | "**/*.tgz": true 20 | }, 21 | 22 | "prettier.endOfLine": "crlf", 23 | "prettier.tabWidth": 2, 24 | "prettier.printWidth": 80, 25 | "prettier.quoteProps": "consistent", 26 | "prettier.proseWrap": "always" 27 | } 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2013-2019 Ian Prest 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@ijprest/kle-serial", 3 | "version": "0.15.1", 4 | "description": "Serialization library for keyboard-layout-editor.com", 5 | "main": "dist/index.js", 6 | "types": "dist/index.d.ts", 7 | "scripts": { 8 | "prepublish": "npm run build", 9 | "build": "tsc", 10 | "test": "mocha --reporter spec", 11 | "cover": "node_modules/.bin/istanbul cover ./node_modules/mocha/bin/_mocha -- -R spec" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/ijprest/kle-serial.git" 16 | }, 17 | "keywords": [ 18 | "kle", 19 | "keyboard-layout-editor", 20 | "serialization", 21 | "json" 22 | ], 23 | "author": "Ian Prest ", 24 | "license": "MIT", 25 | "bugs": { 26 | "url": "https://github.com/ijprest/kle-serial/issues" 27 | }, 28 | "homepage": "https://github.com/ijprest/kle-serial#readme", 29 | "devDependencies": { 30 | "chai": "^4.2.0", 31 | "coveralls": "^3.0.5", 32 | "istanbul": "^0.4.5", 33 | "mocha": "^6.1.4", 34 | "mocha-lcov-reporter": "^1.3.0", 35 | "typescript": "^3.5.3" 36 | }, 37 | "dependencies": { 38 | "json5": "^2.1.0" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # TypeScript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # next.js build output 61 | .next 62 | 63 | # dist folder 64 | dist/ -------------------------------------------------------------------------------- /index.ts: -------------------------------------------------------------------------------- 1 | import * as JSON5 from "json5"; 2 | 3 | export class Key { 4 | color: string = "#cccccc"; 5 | labels: string[] = []; 6 | textColor: Array = []; 7 | textSize: Array = []; 8 | default: { textColor: string; textSize: number } = { 9 | textColor: "#000000", 10 | textSize: 3 11 | }; 12 | x: number = 0; 13 | y: number = 0; 14 | width: number = 1; 15 | height: number = 1; 16 | x2: number = 0; 17 | y2: number = 0; 18 | width2: number = 1; 19 | height2: number = 1; 20 | rotation_x: number = 0; 21 | rotation_y: number = 0; 22 | rotation_angle: number = 0; 23 | decal: boolean = false; 24 | ghost: boolean = false; 25 | stepped: boolean = false; 26 | nub: false = false; 27 | profile: string = ""; 28 | sm: string = ""; // switch mount 29 | sb: string = ""; // switch brand 30 | st: string = ""; // switch type 31 | } 32 | 33 | export class KeyboardMetadata { 34 | author: string = ""; 35 | backcolor: string = "#eeeeee"; 36 | background: { name: string; style: string } | null = null; 37 | name: string = ""; 38 | notes: string = ""; 39 | radii: string = ""; 40 | switchBrand: string = ""; 41 | switchMount: string = ""; 42 | switchType: string = ""; 43 | } 44 | 45 | export class Keyboard { 46 | meta: KeyboardMetadata = new KeyboardMetadata(); 47 | keys: Key[] = []; 48 | } 49 | 50 | export module Serial { 51 | // Helper to copy an object; doesn't handle loops/circular refs, etc. 52 | function copy(o: any): any { 53 | if (typeof o !== "object") { 54 | return o; // primitive value 55 | } else if (o instanceof Array) { 56 | var result: any[] = []; 57 | for (var i = 0; i < o.length; i++) { 58 | result[i] = copy(o[i]); 59 | } 60 | return result; 61 | } else { 62 | var oresult: object = Object.create(Object.getPrototypeOf(o)); 63 | oresult.constructor(); 64 | for (var prop in o) { 65 | oresult[prop] = copy(o[prop]); 66 | } 67 | return oresult; 68 | } 69 | } 70 | 71 | // Map from serialized label position to normalized position, 72 | // depending on the alignment flags. 73 | // prettier-ignore 74 | let labelMap: Array> = [ 75 | //0 1 2 3 4 5 6 7 8 9 10 11 // align flags 76 | [ 0, 6, 2, 8, 9,11, 3, 5, 1, 4, 7,10], // 0 = no centering 77 | [ 1, 7,-1,-1, 9,11, 4,-1,-1,-1,-1,10], // 1 = center x 78 | [ 3,-1, 5,-1, 9,11,-1,-1, 4,-1,-1,10], // 2 = center y 79 | [ 4,-1,-1,-1, 9,11,-1,-1,-1,-1,-1,10], // 3 = center x & y 80 | [ 0, 6, 2, 8,10,-1, 3, 5, 1, 4, 7,-1], // 4 = center front (default) 81 | [ 1, 7,-1,-1,10,-1, 4,-1,-1,-1,-1,-1], // 5 = center front & x 82 | [ 3,-1, 5,-1,10,-1,-1,-1, 4,-1,-1,-1], // 6 = center front & y 83 | [ 4,-1,-1,-1,10,-1,-1,-1,-1,-1,-1,-1], // 7 = center front & x & y 84 | ]; 85 | 86 | function reorderLabelsIn(labels, align) { 87 | var ret: Array = []; 88 | for (var i = 0; i < labels.length; ++i) { 89 | if (labels[i]) ret[labelMap[align][i]] = labels[i]; 90 | } 91 | return ret; 92 | } 93 | 94 | function deserializeError(msg, data?) { 95 | throw "Error: " + msg + (data ? ":\n " + JSON5.stringify(data) : ""); 96 | } 97 | 98 | export function deserialize(rows: Array): Keyboard { 99 | if (!(rows instanceof Array)) 100 | deserializeError("expected an array of objects"); 101 | 102 | // Initialize with defaults 103 | let current: Key = new Key(); 104 | let kbd = new Keyboard(); 105 | var align = 4; 106 | 107 | for (var r = 0; r < rows.length; ++r) { 108 | if (rows[r] instanceof Array) { 109 | for (var k = 0; k < rows[r].length; ++k) { 110 | var item = rows[r][k]; 111 | if (typeof item === "string") { 112 | var newKey: Key = copy(current); 113 | 114 | // Calculate some generated values 115 | newKey.width2 = 116 | newKey.width2 === 0 ? current.width : current.width2; 117 | newKey.height2 = 118 | newKey.height2 === 0 ? current.height : current.height2; 119 | newKey.labels = reorderLabelsIn(item.split("\n"), align); 120 | newKey.textSize = reorderLabelsIn(newKey.textSize, align); 121 | 122 | // Clean up the data 123 | for (var i = 0; i < 12; ++i) { 124 | if (!newKey.labels[i]) { 125 | delete newKey.textSize[i]; 126 | delete newKey.textColor[i]; 127 | } 128 | if (newKey.textSize[i] == newKey.default.textSize) 129 | delete newKey.textSize[i]; 130 | if (newKey.textColor[i] == newKey.default.textColor) 131 | delete newKey.textColor[i]; 132 | } 133 | 134 | // Add the key! 135 | kbd.keys.push(newKey); 136 | 137 | // Set up for the next key 138 | current.x += current.width; 139 | current.width = current.height = 1; 140 | current.x2 = current.y2 = current.width2 = current.height2 = 0; 141 | current.nub = current.stepped = current.decal = false; 142 | } else { 143 | if ( 144 | k != 0 && 145 | (item.r != null || item.rx != null || item.ry != null) 146 | ) { 147 | deserializeError( 148 | "rotation can only be specified on the first key in a row", 149 | item 150 | ); 151 | } 152 | if (item.r != null) current.rotation_angle = item.r; 153 | if (item.rx != null) current.rotation_x = item.rx; 154 | if (item.ry != null) current.rotation_y = item.ry; 155 | if (item.a != null) align = item.a; 156 | if (item.f) { 157 | current.default.textSize = item.f; 158 | current.textSize = []; 159 | } 160 | if (item.f2) 161 | for (var i = 1; i < 12; ++i) current.textSize[i] = item.f2; 162 | if (item.fa) current.textSize = item.fa; 163 | if (item.p) current.profile = item.p; 164 | if (item.c) current.color = item.c; 165 | if (item.t) { 166 | var split = item.t.split("\n"); 167 | if (split[0] != "") current.default.textColor = split[0]; 168 | current.textColor = reorderLabelsIn(split, align); 169 | } 170 | if (item.x) current.x += item.x; 171 | if (item.y) current.y += item.y; 172 | if (item.w) current.width = current.width2 = item.w; 173 | if (item.h) current.height = current.height2 = item.h; 174 | if (item.x2) current.x2 = item.x2; 175 | if (item.y2) current.y2 = item.y2; 176 | if (item.w2) current.width2 = item.w2; 177 | if (item.h2) current.height2 = item.h2; 178 | if (item.n) current.nub = item.n; 179 | if (item.l) current.stepped = item.l; 180 | if (item.d) current.decal = item.d; 181 | if (item.g != null) current.ghost = item.g; 182 | if (item.sm) current.sm = item.sm; 183 | if (item.sb) current.sb = item.sb; 184 | if (item.st) current.st = item.st; 185 | } 186 | } 187 | 188 | // End of the row 189 | current.y++; 190 | current.x = current.rotation_x; 191 | } else if (typeof rows[r] === "object") { 192 | if (r != 0) { 193 | deserializeError( 194 | "keyboard metadata must the be first element", 195 | rows[r] 196 | ); 197 | } 198 | for (let prop in kbd.meta) { 199 | if (rows[r][prop]) kbd.meta[prop] = rows[r][prop]; 200 | } 201 | } else { 202 | deserializeError("unexpected", rows[r]); 203 | } 204 | } 205 | return kbd; 206 | } 207 | 208 | export function parse(json: string): Keyboard { 209 | return deserialize(JSON5.parse(json)); 210 | } 211 | } 212 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # kle-serial 2 | 3 | [![Build Status](https://travis-ci.org/ijprest/kle-serial.svg?branch=master)](https://travis-ci.org/ijprest/kle-serial) 4 | [![Coverage Status](https://coveralls.io/repos/github/ijprest/kle-serial/badge.svg?branch=master)](https://coveralls.io/github/ijprest/kle-serial?branch=master) 5 | [![npm version](https://badge.fury.io/js/%40ijprest%2Fkle-serial.svg)](https://badge.fury.io/js/%40ijprest%2Fkle-serial) 6 | [![Dependency Status](https://david-dm.org/ijprest/kle-serial.svg)](https://david-dm.org/ijprest/kle-serial) 7 | [![GitHub](https://img.shields.io/github/license/ijprest/kle-serial.svg)](LICENSE) 8 | 9 | This is a [MIT-licensed](LICENSE) javascript library for parsing the serialized 10 | format used on keyboard-layout-editor.com (KLE) and converting it into something 11 | that is easier to understand and use in third-party applications. 12 | 13 | KLE is frequently used to prototype and generate a rough keyboard layout, that 14 | is then used by other applications to create plates, circuit boards, etc. These 15 | third-party applications currently use their own parsing logic. 16 | 17 | Unfortunately, the KLE format was designed to be _compact_ (due to some original 18 | limitations), and the format has evolved considerably from its original 19 | versions. As a result, third-party parsing implementations aren't always 100% 20 | compatible with KLE itself, particularly with respect to certain corner-cases or 21 | older / deprecated properties. 22 | 23 | This library is the same code that KLE itself uses to parse serialized layouts, 24 | so by using it, you can be sure that you are 100% compatible with the editor. 25 | 26 | ## Installation 27 | 28 | Install the package via NPM: 29 | 30 | ```bash 31 | npm install @ijprest/kle-serial --save 32 | ``` 33 | 34 | ## Usage 35 | 36 | ```js 37 | var kle = require("@ijprest/kle-serial"); 38 | 39 | var keyboard = kle.Serial.deserialize([ 40 | { name: "Sample", author: "Your Name" }, 41 | ["Q", "W", "E", "R", "T", "Y"] 42 | ]); 43 | 44 | // or 45 | 46 | var keyboard = kle.Serial.parse(`[ 47 | { name: "Sample", author: "Your Name" }, 48 | ["Q", "W", "E", "R", "T", "Y"] 49 | ]`); 50 | ``` 51 | 52 | ## API 53 | 54 | ```ts 55 | kle.Serial.deserialize(rows: Array): Keyboard 56 | ``` 57 | 58 | - Given an array of keyboard rows, deserializes the result into a `Keyboard` 59 | object. 60 | - The first entry is optionally a keyboard metadata object. 61 | 62 | ```ts 63 | kle.Serial.parse(json5: string): Keyboard 64 | ``` 65 | 66 | - This function takes a JSON5-formatted string, parses it, then deserializes the 67 | result into a `Keyboard` object. 68 | - [JSON5](https://json5.org/) is a simplified / lenient version of JSON that is 69 | easier for humans to type; in particular, it doesn't require quotes around 70 | property names. Any valid JSON string should also be a valid JSON5 string. 71 | 72 | ### Keyboard Objects 73 | 74 | ```ts 75 | class Keyboard { 76 | meta: KeyboardMetadata; 77 | keys: Key[]; 78 | } 79 | ``` 80 | 81 | A `Keyboard` is an object containg keyboard metadata (`meta`) and an array of 82 | `keys`. 83 | 84 | ### Keyboard Metadata 85 | 86 | The `meta` object contains several fields: 87 | 88 | ```ts 89 | class KeyboardMetadata { 90 | author: string; 91 | backcolor: string; 92 | background: { name: string; style: string } | null; 93 | name: string; 94 | notes: string; 95 | radii: string; 96 | switchBrand: string; 97 | switchMount: string; 98 | switchType: string; 99 | } 100 | ``` 101 | 102 | - `author` — the name of the author 103 | - `backcolor` — the background color of the keyboard editor area (default 104 | `#eeeeee`) 105 | - `background` (optional) — a background image that overrides `backcolor` if 106 | specified. 107 | - The `name` identifies it from the list of backgrounds in the editor; other 108 | consumers can ignore this property. 109 | - The `style` is some custom CSS that will override the background color; it 110 | will have the form: `background-image: url(...)` 111 | - `name` — the name of the keyboard layout 112 | - Appears in the editor, below the keyboard. 113 | - Identifies the keyboard among your saved layouts. 114 | - Used to generate a filename when downloading or rendering the keyboard. 115 | - `notes` — notes about the keyboard layout, in 116 | [GitHub-flavored Markdown](https://github.github.com/gfm/). 117 | - `radii` — the radii of the keyboard corners, in 118 | [CSS `border-radius` format](https://developer.mozilla.org/en-US/docs/Web/CSS/border-radius), 119 | e.g., `20px`. 120 | - `switchBrand`, `switchMount`, `switchType` — the _default_ switch `mount`, 121 | `brand`, and `type` of switches on your keyboard. 122 | - Default can be overridden on individual keys. 123 | - See known values here: 124 | https://github.com/ijprest/keyboard-layout-editor/blob/master/switches.json 125 | 126 | ### Keys 127 | 128 | Each key in the `keys` array contains the following data: 129 | 130 | ```ts 131 | export class Key { 132 | color: string; 133 | labels: string[]; 134 | textColor: Array; 135 | textSize: Array; 136 | default: { textColor: string; textSize: number }; 137 | 138 | x: number; 139 | y: number; 140 | width: number; 141 | height: number; 142 | 143 | x2: number; 144 | y2: number; 145 | width2: number; 146 | height2: number; 147 | 148 | rotation_x: number; 149 | rotation_y: number; 150 | rotation_angle: number; 151 | 152 | decal: boolean; 153 | ghost: boolean; 154 | stepped: boolean; 155 | nub: boolean; 156 | 157 | profile: string; 158 | 159 | sm: string; // switch mount 160 | sb: string; // switch brand 161 | st: string; // switch type 162 | } 163 | ``` 164 | 165 | - `color` — the keycap color, e.g., `"#ff0000"` for red. 166 | - `labels` — an array of up to 12 text labels (sometimes referred to as 167 | 'legends'): 168 | - In reading order, i.e., left-to-right, top-to-bottom: 169 | - ![label order illustration](images/label-order.png) 170 | - The labels are user input, and may contain arbitrary HTML content; when 171 | rendering, input sanitization is recommended for security purposes. 172 | - `textColor` — an array of up to 12 colors (e.g., `"#ff0000"`), to be used for 173 | the text labels; if any entries are `null` or `undefined`, you should use the 174 | `default.textColor`. 175 | - `textSize` — an array of up to 12 sizes (integers 1-9), to be used for the 176 | text labels; if any entries are `null` or `undefined`, you should use the 177 | `default.textSize`. 178 | - Note that the sizes are relative and do not correspond to any fixed font 179 | size. 180 | - KLE uses the following formula when rendering on-screen: 181 | - (6px + 2px \* _textSize_) 182 | - `default.textColor` / `default.textSize` — the default text color / size. 183 | - `x` / `y` — the absolute position of the key in keyboard units (where _1u_ is 184 | the size of a standard 1x1 keycap). 185 | - `width` / `height` — the size of the key, in keyboard units. 186 | - `x2` / `y2` / `width2` / `height2` — the size & position of the _second_ 187 | rectangle that is used to define oddly-shaped keys (like an 188 | [ISO Enter or Big-ass Enter key](https://deskthority.net/wiki/Return_key) or 189 | [stepped keys](https://deskthority.net/wiki/Keycap#Stepped_keycaps)). 190 | - If the size is (0,0), then there is no second rectangle required. 191 | - The position is relative to (`x`, `y`). 192 | - The two rectangles can be thought of as overlapping, combining to create the 193 | desired key shape. 194 | - Note that labels are always positioned relative to the main rectangle. 195 | - If a key is `stepped`, the second rectangle is the lower part. 196 | - ![oddly-shapped key illustration](images/oddly-shaped.png) 197 | - In this example, the second rectangle is shown on top of the original 198 | rectangle, and (`x2`,`y2`) [`width` x `height`] = (-0.75, 1.0) [2.25 x 199 | 1.0]. 200 | - `rotation_x` / `rotation_y` — defines the center of rotation for the key. 201 | - `rotation_angle` — specifies the angle the key is rotated (about the center of 202 | rotation). 203 | - `decal` — specifies that the key is a 'decal', meaning that only the text 204 | labels should be rendered, not the keycap borders. 205 | - `ghost` — specifies that key key is 'ghosted', meaning that it is to be 206 | rendered unobtrusively; typically semi-transparent and without any labels. 207 | - `stepped` — specifies that the key is 208 | [stepped](https://deskthority.net/wiki/Keycap#Stepped_keycaps). 209 | - `nub` — specifies that the key has a homing nub / bump / dish; the exact 210 | rendering will depend on the key's `profile`. 211 | - `profile` — specifies the key's "profile" (and row, for those profiles that 212 | vary depending on the row), e.g., "`DCS R1`" or "`DSA`". 213 | - Currently supported / known profiles: `SA`, `DSA`, `DCS`, `OEM`, `CHICKLET`, 214 | `FLAT` 215 | - Currently supported / known rows: `R1`, `R2`, `R3`, `R4`, `R5`, `SPACE` 216 | - `sm` / `sb` / `st` — the switch _mount_, _brand_, and _type_, overriding the 217 | default values specified in the keyboard metadata. 218 | 219 | ## Future Work 220 | 221 | In rough order of priority: 222 | 223 | 1. This library is _based_ on the original KLE code, but it has been converted 224 | to a TypeScript and modularized to make it convenient for others to consume; 225 | the KLE site itself is not yet using this actual code. 226 | - So the first order of business is to update KLE to use this exact NPM 227 | module. 228 | - That will ensure that the code is correct, and that nothing has been 229 | missed, as well as guarantee that the two projects are kept in sync. 230 | 2. This library currently only handles _deserialization_; the serialization code 231 | still needs to be ported. 232 | 3. More tests (particularly on the serialization side, once it's ported; it's 233 | much more error-prone than deserialization). 234 | 4. Migrate some of the supporting data from KLE to this project, so you don't 235 | have to look it up elsewhere, e.g.: 236 | - Switch mount / brand / type definitions. 237 | - Color palettes. 238 | 5. Migrate HTML key rendering templates (and supporting stylesheets) from KLE to 239 | this project, so anyone can render a key identically to KLE. 240 | 241 | ## Tests 242 | 243 | ```bash 244 | npm test 245 | ``` 246 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var expect = require("chai").expect; 4 | var kbd = require("../dist/index"); 5 | 6 | describe("deserialization", function() { 7 | it("should fail on non-array", function() { 8 | var result = () => kbd.Serial.deserialize("test"); 9 | expect(result).to.throw(); 10 | }); 11 | 12 | it("should fail on non array/object data", function() { 13 | var result = () => kbd.Serial.deserialize(["test"]); 14 | expect(result).to.throw(); 15 | }); 16 | 17 | it("should return empty keyboard on empty array", function() { 18 | var result = kbd.Serial.deserialize([]); 19 | expect(result).to.be.an.instanceOf(kbd.Keyboard); 20 | expect(result.keys).to.be.empty; 21 | }); 22 | 23 | describe("of metadata", function() { 24 | it("should parse from first object if it exists", function() { 25 | var result = kbd.Serial.deserialize([{ name: "test" }]); 26 | expect(result).to.be.an.instanceOf(kbd.Keyboard); 27 | expect(result.meta.name).to.equal("test"); 28 | }); 29 | 30 | it("should throw an exception if found anywhere other than the start", function() { 31 | var result = () => kbd.Serial.deserialize([[], { name: "test" }]); 32 | expect(result).to.throw(); 33 | }); 34 | }); 35 | 36 | describe("of key positions", function() { 37 | it("should default to (0,0)", function() { 38 | var result = kbd.Serial.deserialize([["1"]]); 39 | expect(result).to.be.an.instanceOf(kbd.Keyboard); 40 | expect(result.keys).to.have.length(1); 41 | expect(result.keys[0].x).to.equal(0); 42 | expect(result.keys[0].y).to.equal(0); 43 | }); 44 | 45 | it("should increment x position by the width of the previous key", function() { 46 | var result = kbd.Serial.deserialize([[{ x: 1 }, "1", "2"]]); 47 | expect(result).to.be.an.instanceOf(kbd.Keyboard); 48 | expect(result.keys).to.have.length(2); 49 | expect(result.keys[0].x).to.equal(1); 50 | expect(result.keys[1].x).to.equal( 51 | result.keys[0].x + result.keys[0].width 52 | ); 53 | expect(result.keys[1].y).to.equal(result.keys[0].y); 54 | }); 55 | 56 | it("should increment y position whenever a new row starts, and reset x to zero", function() { 57 | var result = kbd.Serial.deserialize([[{ y: 1 }, "1"], ["2"]]); 58 | expect(result).to.be.an.instanceOf(kbd.Keyboard); 59 | expect(result.keys).to.have.length(2); 60 | expect(result.keys[0].y).to.equal(1); 61 | expect(result.keys[1].x).to.equal(0); 62 | expect(result.keys[1].y).to.equal(result.keys[0].y + 1); 63 | }); 64 | 65 | it("should add x and y to current position", function() { 66 | var result = kbd.Serial.deserialize([["1", { x: 1 }, "2"]]); 67 | expect(result).to.be.an.instanceOf(kbd.Keyboard); 68 | expect(result.keys).to.have.length(2); 69 | expect(result.keys[0].x).to.equal(0); 70 | expect(result.keys[1].x).to.equal(2); 71 | 72 | var result = kbd.Serial.deserialize([["1"], [{ y: 1 }, "2"]]); 73 | expect(result).to.be.an.instanceOf(kbd.Keyboard); 74 | expect(result.keys).to.have.length(2); 75 | expect(result.keys[0].y).to.equal(0); 76 | expect(result.keys[1].y).to.equal(2); 77 | }); 78 | 79 | it("should leave x2,y2 at (0,0) if not specified", function() { 80 | var result = kbd.Serial.deserialize([[{ x: 1, y: 1 }, "1"]]); 81 | expect(result).to.be.an.instanceOf(kbd.Keyboard); 82 | expect(result.keys).to.have.length(1); 83 | expect(result.keys[0].x).to.not.equal(0); 84 | expect(result.keys[0].y).to.not.equal(0); 85 | expect(result.keys[0].x2).to.equal(0); 86 | expect(result.keys[0].y2).to.equal(0); 87 | 88 | var result = kbd.Serial.deserialize([ 89 | [{ x: 1, y: 1, x2: 2, y2: 2 }, "1"] 90 | ]); 91 | expect(result).to.be.an.instanceOf(kbd.Keyboard); 92 | expect(result.keys).to.have.length(1); 93 | expect(result.keys[0].x).to.not.equal(0); 94 | expect(result.keys[0].y).to.not.equal(0); 95 | expect(result.keys[0].x2).to.not.equal(0); 96 | expect(result.keys[0].y2).to.not.equal(0); 97 | }); 98 | }); 99 | 100 | describe("of key sizes", function() { 101 | it("should reset width and height to 1", function() { 102 | var result = kbd.Serial.deserialize([[{ w: 5 }, "1", "2"]]); 103 | expect(result).to.be.an.instanceOf(kbd.Keyboard); 104 | expect(result.keys).to.have.length(2); 105 | expect(result.keys[0].width).to.equal(5); 106 | expect(result.keys[1].width).to.equal(1); 107 | 108 | var result = kbd.Serial.deserialize([[{ h: 5 }, "1", "2"]]); 109 | expect(result).to.be.an.instanceOf(kbd.Keyboard); 110 | expect(result.keys).to.have.length(2); 111 | expect(result.keys[0].height).to.equal(5); 112 | expect(result.keys[1].height).to.equal(1); 113 | }); 114 | 115 | it("should default width2/height2 if not specified", function() { 116 | var result = kbd.Serial.deserialize([ 117 | [{ w: 2, h: 2 }, "1", { w: 2, h: 2, w2: 4, h2: 4 }, "2"] 118 | ]); 119 | expect(result).to.be.an.instanceOf(kbd.Keyboard); 120 | expect(result.keys).to.have.length(2); 121 | expect(result.keys[0].width2).to.equal(result.keys[0].width); 122 | expect(result.keys[0].height2).to.equal(result.keys[0].height); 123 | expect(result.keys[1].width2).to.not.equal(result.keys[1].width); 124 | expect(result.keys[1].height2).to.not.equal(result.keys[1].width); 125 | }); 126 | }); 127 | 128 | describe("of other properties", function() { 129 | it("should reset stepped, homing, and decal flags to false", function() { 130 | var result = kbd.Serial.deserialize([ 131 | [{ l: true, n: true, d: true }, "1", "2"] 132 | ]); 133 | expect(result).to.be.an.instanceOf(kbd.Keyboard); 134 | expect(result.keys).to.have.length(2); 135 | expect(result.keys[0].stepped).to.be.true; 136 | expect(result.keys[0].nub).to.be.true; 137 | expect(result.keys[0].decal).to.be.true; 138 | expect(result.keys[1].stepped).to.be.false; 139 | expect(result.keys[1].nub).to.be.false; 140 | expect(result.keys[1].decal).to.be.false; 141 | }); 142 | 143 | it("should propagate the ghost flag", function() { 144 | var result = kbd.Serial.deserialize([["0", { g: true }, "1", "2"]]); 145 | expect(result).to.be.an.instanceOf(kbd.Keyboard); 146 | expect(result.keys).to.have.length(3); 147 | expect(result.keys[0].ghost).to.be.false; 148 | expect(result.keys[1].ghost).to.be.true; 149 | expect(result.keys[2].ghost).to.be.true; 150 | }); 151 | 152 | it("should propagate the profile flag", function() { 153 | var result = kbd.Serial.deserialize([["0", { p: "DSA" }, "1", "2"]]); 154 | expect(result).to.be.an.instanceOf(kbd.Keyboard); 155 | expect(result.keys).to.have.length(3); 156 | expect(result.keys[0].profile).to.be.empty; 157 | expect(result.keys[1].profile).to.equal("DSA"); 158 | expect(result.keys[2].profile).to.equal("DSA"); 159 | }); 160 | 161 | it("should propagate switch properties", function() { 162 | var result = kbd.Serial.deserialize([["1", { sm: "cherry" }, "2", "3"]]); 163 | expect(result, "sm").to.be.an.instanceOf(kbd.Keyboard); 164 | expect(result.keys, "sm").to.have.length(3); 165 | expect(result.keys[0].sm, "sm_0").to.equal(""); 166 | expect(result.keys[1].sm, "sm_1").to.equal("cherry"); 167 | expect(result.keys[2].sm, "sm_2").to.equal("cherry"); 168 | 169 | var result = kbd.Serial.deserialize([["1", { sb: "cherry" }, "2", "3"]]); 170 | expect(result, "sb").to.be.an.instanceOf(kbd.Keyboard); 171 | expect(result.keys, "sb").to.have.length(3); 172 | expect(result.keys[0].sb, "sb_0").to.equal(""); 173 | expect(result.keys[1].sb, "sb_1").to.equal("cherry"); 174 | expect(result.keys[2].sb, "sb_2").to.equal("cherry"); 175 | 176 | var result = kbd.Serial.deserialize([ 177 | ["1", { st: "MX1A-11Nx" }, "2", "3"] 178 | ]); 179 | expect(result, "st").to.be.an.instanceOf(kbd.Keyboard); 180 | expect(result.keys, "st").to.have.length(3); 181 | expect(result.keys[0].st, "st_0").to.equal(""); 182 | expect(result.keys[1].st, "st_1").to.equal("MX1A-11Nx"); 183 | expect(result.keys[2].st, "st_2").to.equal("MX1A-11Nx"); 184 | }); 185 | }); 186 | 187 | describe("of text color", function() { 188 | it("should apply colors to all subsequent keys", function() { 189 | var result = kbd.Serial.deserialize([ 190 | [{ c: "#ff0000", t: "#00ff00" }, "1", "2"] 191 | ]); 192 | expect(result).to.be.an.instanceOf(kbd.Keyboard); 193 | expect(result.keys).to.have.length(2); 194 | expect(result.keys[0].color).to.equal("#ff0000"); 195 | expect(result.keys[1].color).to.equal("#ff0000"); 196 | expect(result.keys[0].default.textColor).to.equal("#00ff00"); 197 | expect(result.keys[1].default.textColor).to.equal("#00ff00"); 198 | }); 199 | 200 | it("should apply `t` to all legends", function() { 201 | var result = kbd.Serial.deserialize([ 202 | [{ a: 0, t: "#444444" }, "0\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11"] 203 | ]); 204 | expect(result).to.be.an.instanceOf(kbd.Keyboard); 205 | expect(result.keys).to.have.length(1); 206 | expect(result.keys[0].default.textColor).to.equal("#444444"); 207 | for (var i = 0; i < 12; ++i) { 208 | expect(result.keys[0].textColor[i], `[${i}]`).to.be.undefined; 209 | } 210 | }); 211 | 212 | it("should handle generic case", function() { 213 | var labels = 214 | "#111111\n#222222\n#333333\n#444444\n" + 215 | "#555555\n#666666\n#777777\n#888888\n" + 216 | "#999999\n#aaaaaa\n#bbbbbb\n#cccccc"; 217 | var result = kbd.Serial.deserialize([ 218 | [{ a: 0, t: /*colors*/ labels }, /*labels*/ labels] 219 | ]); 220 | expect(result).to.be.an.instanceOf(kbd.Keyboard); 221 | expect(result.keys).to.have.length(1); 222 | expect(result.keys[0].default.textColor).to.equal("#111111"); 223 | for (var i = 0; i < 12; ++i) { 224 | expect( 225 | result.keys[0].textColor[i] || result.keys[0].default.textColor, 226 | `i=${i}` 227 | ).to.equal(result.keys[0].labels[i]); 228 | } 229 | }); 230 | 231 | it("should handle blanks", function() { 232 | var labels = 233 | "#111111\nXX\n#333333\n#444444\n" + 234 | "XX\n#666666\nXX\n#888888\n" + 235 | "#999999\n#aaaaaa\n#bbbbbb\n#cccccc"; 236 | var result = kbd.Serial.deserialize([ 237 | [{ a: 0, t: /*colors*/ labels.replace(/XX/g, "") }, /*labels*/ labels] 238 | ]); 239 | expect(result).to.be.an.instanceOf(kbd.Keyboard); 240 | expect(result.keys).to.have.length(1); 241 | expect(result.keys[0].default.textColor).to.equal("#111111"); 242 | for (var i = 0; i < 12; ++i) { 243 | // if blank, should be same as color[0] / default 244 | var color = 245 | result.keys[0].textColor[i] || result.keys[0].default.textColor; 246 | if (result.keys[0].labels[i] === "XX") 247 | expect(color, `i=${i}`).to.equal("#111111"); 248 | else expect(color, `i=${i}`).to.equal(result.keys[0].labels[i]); 249 | } 250 | }); 251 | 252 | it("should not reset default color if blank", function() { 253 | var result = kbd.Serial.deserialize([ 254 | [{ t: "#ff0000" }, "1", { t: "\n#00ff00" }, "2"] 255 | ]); 256 | expect(result).to.be.an.instanceOf(kbd.Keyboard); 257 | expect(result.keys).to.have.length(2); 258 | expect(result.keys[0].default.textColor, "[0]").to.equal("#ff0000"); 259 | expect(result.keys[1].default.textColor, "[1]").to.equal("#ff0000"); 260 | }); 261 | 262 | it("should delete values equal to the default", function() { 263 | var result = kbd.Serial.deserialize([ 264 | [ 265 | { t: "#ff0000" }, 266 | "1", 267 | { t: "\n#ff0000" }, 268 | "\n2", 269 | { t: "\n#00ff00" }, 270 | "\n3" 271 | ] 272 | ]); 273 | expect(result).to.be.an.instanceOf(kbd.Keyboard); 274 | expect(result.keys).to.have.length(3); 275 | expect(result.keys[1].labels[6]).to.equal("2"); 276 | expect(result.keys[1].textColor[6]).to.be.undefined; 277 | expect(result.keys[2].labels[6]).to.equal("3"); 278 | expect(result.keys[2].textColor[6]).to.equal("#00ff00"); 279 | }); 280 | }); 281 | 282 | describe("of rotation", function() { 283 | it("should not be allowed on anything but the first key in a row", function() { 284 | var r1 = () => kbd.Serial.deserialize([[{ r: 45 }, "1", "2"]]); 285 | expect(r1).to.not.throw(); 286 | var rx1 = () => kbd.Serial.deserialize([[{ rx: 45 }, "1", "2"]]); 287 | expect(rx1).to.not.throw(); 288 | var ry1 = () => kbd.Serial.deserialize([[{ ry: 45 }, "1", "2"]]); 289 | expect(ry1).to.not.throw(); 290 | 291 | var r2 = () => kbd.Serial.deserialize([["1", { r: 45 }, "2"]]); 292 | expect(r2).to.throw(); 293 | var rx2 = () => kbd.Serial.deserialize([["1", { rx: 45 }, "2"]]); 294 | expect(rx2).to.throw(); 295 | var ry2 = () => kbd.Serial.deserialize([["1", { ry: 45 }, "2"]]); 296 | expect(ry2).to.throw(); 297 | }); 298 | }); 299 | 300 | describe("of legends", function() { 301 | it("should align legend positions correctly", function() { 302 | // Some history, to make sense of this: 303 | // 1. Originally, you could only have top & botton legends, and they were 304 | // left-aligned. (top:0 & bottom:1) 305 | // 2. Next, we added right-aligned labels (top:2 & bottom:3). 306 | // 3. Next, we added front text (left:4, right:5). 307 | // 4. Next, we added the alignment flags that allowed you to move the 308 | // labels (0-5) to the centered positions (via checkboxes). 309 | // 5. Nobody understood the checkboxes. They were removed in favor of 310 | // twelve separate label editors, allowing text to be placed anywhere. 311 | // This introduced labels 6 through 11. 312 | // 6. The internal rendering is now Top->Bottom, Left->Right, but to keep 313 | // the file-format unchanged, the serialization code now translates 314 | // the array from the old layout to the new internal one. 315 | 316 | // prettier-ignore 317 | var expected = [ 318 | // top row /**/ middle row /**/ bottom row /**/ front 319 | ["0","8","2",/**/"6","9","7",/**/"1","10","3",/**/"4","11","5"], // a=0 320 | [ ,"0", ,/**/ ,"6", ,/**/ , "1", ,/**/"4","11","5"], // a=1 (center horz) 321 | [ , , ,/**/"0","8","2",/**/ , , ,/**/"4","11","5"], // a=2 (center vert) 322 | [ , , ,/**/ ,"0", ,/**/ , , ,/**/"4","11","5"], // a=3 (center both) 323 | 324 | ["0","8","2",/**/"6","9","7",/**/"1","10","3",/**/ , "4", ], // a=4 (center front) 325 | [ ,"0", ,/**/ ,"6", ,/**/ , "1", ,/**/ , "4", ], // a=5 (center front+horz) 326 | [ , , ,/**/"0","8","2",/**/ , , ,/**/ , "4", ], // a=6 (center front+vert) 327 | [ , , ,/**/ ,"0", ,/**/ , , ,/**/ , "4", ], // a=7 (center front+both) 328 | ]; 329 | 330 | for (var a = 0; a <= 7; ++a) { 331 | var name = `a=${a}`; 332 | var result = kbd.Serial.deserialize([ 333 | [{ a: a }, "0\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11"] 334 | ]); 335 | expect(expected[a], name).to.not.be.undefined; 336 | expect(result, name).to.be.an.instanceOf(kbd.Keyboard); 337 | expect(result.keys, name).to.have.length(1); 338 | expect(result.keys[0].labels, name).to.have.length(expected[a].length); 339 | expect(result.keys[0].labels, name).to.have.ordered.members( 340 | expected[a] 341 | ); 342 | } 343 | }); 344 | }); 345 | 346 | describe("of font sizes", function() { 347 | it("should handle `f` at all alignments", function() { 348 | for (var a = 0; a < 7; ++a) { 349 | var name = `a=${a}`; 350 | var result = kbd.Serial.deserialize([ 351 | [{ f: 1, a: a }, "0\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11"] 352 | ]); 353 | expect(result, name).to.be.an.instanceOf(kbd.Keyboard); 354 | expect(result.keys, name).to.have.length(1); 355 | expect(result.keys[0].default.textSize, name).to.equal(1); 356 | expect(result.keys[0].textSize, name).to.have.length(0); 357 | } 358 | }); 359 | 360 | it("should handle `f2` at all alignments", function() { 361 | for (var a = 0; a < 7; ++a) { 362 | var name = `a=${a}`; 363 | var result = kbd.Serial.deserialize([ 364 | [{ f: 1, f2: 2, a: a }, "0\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11"] 365 | ]); 366 | expect(result, name).to.be.an.instanceOf(kbd.Keyboard); 367 | expect(result.keys, name).to.have.length(1); 368 | // All labels should be 2, except the first one ('0') 369 | for (var i = 0; i < 12; ++i) { 370 | var name_i = `${name} [${i}]`; 371 | if (result.keys[0].labels[i]) { 372 | var expected = result.keys[0].labels[i] === "0" ? 1 : 2; 373 | if (result.keys[0].labels[i] === "0") { 374 | expect(result.keys[0].textSize[i], name_i).to.be.undefined; 375 | } else { 376 | expect(result.keys[0].textSize[i], name_i).to.equal(2); 377 | } 378 | } else { 379 | // no text at [i]; textSize should be undefined 380 | expect(result.keys[0].textSize[i], name_i).to.be.undefined; 381 | } 382 | } 383 | } 384 | }); 385 | 386 | it("should handle `fa` at all alignments", function() { 387 | for (var a = 0; a < 7; ++a) { 388 | var name = `a=${a}`; 389 | var result = kbd.Serial.deserialize([ 390 | [ 391 | { f: 1, fa: [2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13], a: a }, 392 | "2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13" 393 | ] 394 | ]); 395 | expect(result, name).to.be.an.instanceOf(kbd.Keyboard); 396 | expect(result.keys, name).to.have.length(1); 397 | 398 | for (var i = 0; i < 12; ++i) { 399 | var name_i = `${name} [${i}]`; 400 | if (result.keys[0].labels[i]) { 401 | expect(result.keys[0].textSize[i], name_i).to.equal( 402 | parseInt(result.keys[0].labels[i]) 403 | ); 404 | } 405 | } 406 | } 407 | }); 408 | 409 | it("should handle blanks in `fa`", function() { 410 | for (var a = 0; a < 7; ++a) { 411 | var name = `a=${a}`; 412 | var result = kbd.Serial.deserialize([ 413 | [ 414 | { f: 1, fa: [, 2, , 4, , 6, , 8, 9, 10, , 12], a: a }, 415 | "x\n2\nx\n4\nx\n6\nx\n8\n9\n10\nx\n12" 416 | ] 417 | ]); 418 | expect(result, name).to.be.an.instanceOf(kbd.Keyboard); 419 | expect(result.keys, name).to.have.length(1); 420 | 421 | for (var i = 0; i < 12; ++i) { 422 | var name_i = `${name} [${i}]`; 423 | if (result.keys[0].labels[i] === "x") { 424 | expect(result.keys[0].textSize[i], name_i).to.be.undefined; 425 | } 426 | } 427 | } 428 | }); 429 | 430 | it("should not reset default size if blank", function() { 431 | var result = kbd.Serial.deserialize([ 432 | [{ f: 1 }, "1", { fa: [, 2] }, "2"] 433 | ]); 434 | expect(result).to.be.an.instanceOf(kbd.Keyboard); 435 | expect(result.keys).to.have.length(2); 436 | expect(result.keys[0].default.textSize, "[0]").to.equal(1); 437 | expect(result.keys[1].default.textSize, "[1]").to.equal(1); 438 | }); 439 | 440 | it("should delete values equal to the default", function() { 441 | var result = kbd.Serial.deserialize([ 442 | [{ f: 1 }, "1", { fa: "\n1" }, "\n2", { fa: "\n2" }, "\n3"] 443 | ]); 444 | expect(result).to.be.an.instanceOf(kbd.Keyboard); 445 | expect(result.keys).to.have.length(3); 446 | expect(result.keys[1].labels[6]).to.equal("2"); 447 | expect(result.keys[1].textSize[6]).to.be.undefined; 448 | expect(result.keys[2].labels[6]).to.equal("3"); 449 | expect(result.keys[2].textSize[6]).to.equal("2"); 450 | }); 451 | }); 452 | 453 | describe("of strings", function() { 454 | it("should be lenient about quotes", function() { 455 | var result1 = () => 456 | kbd.Serial.parse(`[ 457 | { name: "Sample", author: "Your Name" }, 458 | ["Q", "W", "E", "R", "T", "Y"] 459 | ]`); 460 | 461 | var result2 = () => 462 | kbd.Serial.parse(`[ 463 | { "name": "Sample", "author": "Your Name" }, 464 | ["Q", "W", "E", "R", "T", "Y"] 465 | ]`); 466 | 467 | var result3 = () => 468 | kbd.Serial.deserialize([ 469 | { name: "Sample", author: "Your Name" }, 470 | ["Q", "W", "E", "R", "T", "Y"] 471 | ]); 472 | 473 | expect(result1).to.not.throw(); 474 | expect(result2).to.not.throw(); 475 | expect(result1(), "1<>2").to.deep.equal(result2()); 476 | expect(result1(), "1<>3").to.deep.equal(result3()); 477 | }); 478 | }); 479 | }); 480 | --------------------------------------------------------------------------------