├── .eslintrc.yml ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── index.js ├── package.json └── spec ├── .eslintrc.yml └── index.js /.eslintrc.yml: -------------------------------------------------------------------------------- 1 | extends: eslint:recommended 2 | env: 3 | node: true 4 | browser: true 5 | rules: 6 | no-constant-condition: 0 7 | no-shadow: 1 8 | block-scoped-var: 2 9 | callback-return: 2 10 | complexity: [2, 15] 11 | curly: [2, multi-or-nest, consistent] 12 | dot-location: [2, property] 13 | dot-notation: 2 14 | indent-legacy: [2, 2, SwitchCase: 1] 15 | linebreak-style: [2, unix] 16 | no-console: [2, allow: [warn, error]] 17 | no-else-return: 2 18 | no-eq-null: 2 19 | no-fallthrough: 2 20 | no-invalid-this: 2 21 | no-return-assign: 2 22 | no-trailing-spaces: 2 23 | no-use-before-define: [2, nofunc] 24 | quotes: [2, single, avoid-escape] 25 | semi: [2, always] 26 | strict: [2, global] 27 | valid-jsdoc: [2, requireReturn: false] 28 | globals: 29 | BigInt: false 30 | Map: true 31 | Set: true 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # nyc test coverage 18 | .nyc_output 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directories 30 | node_modules 31 | jspm_packages 32 | 33 | # Optional npm cache directory 34 | .npm 35 | 36 | # Optional REPL history 37 | .node_repl_history 38 | 39 | .DS_Store 40 | yarn.lock 41 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "12" 4 | after_script: 5 | - coveralls < coverage/lcov.info 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Evgeny Poberezkin 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # json-source-map 2 | Parse/stringify JSON and provide source-map for JSON-pointers to all nodes. 3 | 4 | NEW: supports BigInt, Maps, Sets and Typed arrays. 5 | 6 | [![Build Status](https://travis-ci.org/epoberezkin/json-source-map.svg?branch=master)](https://travis-ci.org/epoberezkin/json-source-map) 7 | [![npm version](https://badge.fury.io/js/json-source-map.svg)](https://www.npmjs.com/package/json-source-map) 8 | [![Coverage Status](https://coveralls.io/repos/github/epoberezkin/json-source-map/badge.svg?branch=master)](https://coveralls.io/github/epoberezkin/json-source-map?branch=master) 9 | 10 | 11 | ## Install 12 | 13 | ```bash 14 | npm install json-source-map 15 | ``` 16 | 17 | 18 | ## Possible use cases 19 | 20 | #### Source maps 21 | 22 | When a domain-specific language that compiles to JavaScript uses JSON as a format, this module can be used as a replacement for standard JSON to simplify generation of source maps. 23 | 24 | #### Editing forms/JSON 25 | 26 | When a form also allows to edit JSON representation of data on the same screen, this module can be used to sinchronise navigation in JSON and in the form. 27 | 28 | 29 | ## Usage 30 | 31 | #### Stringify 32 | 33 | ```javascript 34 | var jsonMap = require('json-source-map'); 35 | var result = jsonMap.stringify({ foo: 'bar' }, null, 2); 36 | console.log('json:'); 37 | console.log(result.json); 38 | console.log('\npointers:'); 39 | console.log(result.pointers); 40 | ``` 41 | 42 | output: 43 | 44 | ```text 45 | json: 46 | { 47 | "foo": "bar" 48 | } 49 | 50 | pointers: 51 | { '': 52 | { value: { line: 0, column: 0, pos: 0 }, 53 | valueEnd: { line: 2, column: 1, pos: 18 } }, 54 | '/foo': 55 | { key: { line: 1, column: 2, pos: 4 }, 56 | keyEnd: { line: 1, column: 7, pos: 9 }, 57 | value: { line: 1, column: 9, pos: 11 }, 58 | valueEnd: { line: 1, column: 14, pos: 16 } } } 59 | ``` 60 | 61 | 62 | #### Parse 63 | 64 | ```javascript 65 | var result = jsonMap.parse('{ "foo": "bar" }'); 66 | console.log('data:') 67 | console.log(result.data); 68 | console.log('\npointers:'); 69 | console.log(result.pointers); 70 | ``` 71 | 72 | output: 73 | ```text 74 | data: 75 | { foo: 'bar' } 76 | 77 | pointers: 78 | { '': 79 | { value: { line: 0, column: 0, pos: 0 }, 80 | valueEnd: { line: 0, column: 16, pos: 16 } }, 81 | '/foo': 82 | { key: { line: 0, column: 2, pos: 2 }, 83 | keyEnd: { line: 0, column: 7, pos: 7 }, 84 | value: { line: 0, column: 9, pos: 9 }, 85 | valueEnd: { line: 0, column: 14, pos: 14 } } } 86 | ``` 87 | 88 | 89 | ## API 90 | 91 | #### .parse(String json, Any _, Object options) -> Object; 92 | 93 | Parses JSON string. Returns object with properties: 94 | - _data_: parsed data. 95 | - _pointers_: an object where each key is a JSON pointer ([RFC 6901](https://tools.ietf.org/html/rfc6901)), each corresponding value is a mapping object. 96 | 97 | Mapping object has properties: 98 | - _key_: location object (see below) of the beginning of the key in JSON string. This property is only present if parent data is an object (rather than array). 99 | - _keyEnd_: location of the end of the key in JSON string. This property is only present if parent data is an object. 100 | - _value_: location of the beginning of the value in JSON string. 101 | - _valueEnd_: location of the end of the value in JSON string. 102 | 103 | Location object has properties (zero-based numbers): 104 | - _line_: line number in JSON file. 105 | - _column_: column number in JSON string (from the beginning of line). 106 | - _pos_: character position in JSON file (from the beginning of JSON string). 107 | 108 | Options: 109 | - _bigint_: parse large integers as [BigInt](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/BigInt). 110 | 111 | Whitespace: 112 | - the only character that increases line number in mappings is line feed ('\n'), so if your JSON string has '\r\n' sequence, it will still be counted as one line, 113 | - both '\r' and '\n' are counted as a character when determining `pos` (it is possible to slice sections of JSON string using `pos` property), but `column` counter is reset when `r` or `n` is encountered, 114 | - tabs ('\t') are counted as four spaces when determining `column` but as a single character for `pos`. 115 | 116 | Comparison with the standard `JSON.parse`: 117 | - when it is not possible to parse JSON, a SyntaxError exception with exactly the same message is thrown, 118 | - `reviver` parameter of [JSON.parse](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/parse#Using_the_reviver_parameter) is not supported, but its position is reserved. 119 | - supports parsing large integers as [BigInt](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/BigInt) (with the option `bigint: true`). 120 | 121 | 122 | #### .stringify(Any data, Any _, String|Number|Object space) -> Object; 123 | 124 | Stringifies JavaScript data. Returns object with properties: 125 | - _json_: JSON string - stringified data. 126 | - _pointers_: an object where each key is a JSON-pointer, each corresponding value is a mapping object (same format as in parse method). 127 | 128 | Comparison with the standard `JSON.stringify`: 129 | - `replacer` parameter of [JSON.stringify](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify#The_replacer_parameter) is not supported, but its position is reserved. 130 | - `space` parameter is supported, but if it is a string, it may only contain characters space, tab ('\t'), caret return ('\r') and line feed ('\n') - using any other caracter throws an exception. If this parameter is an object, it is options. 131 | 132 | Options: 133 | - _space_: same as `space` parameter. 134 | - _es6_: stringify ES6 Maps, Sets and Typed arrays (as JSON arrays). 135 | 136 | 137 | ## License 138 | 139 | [MIT](https://github.com/epoberezkin/json-source-map/blob/master/LICENSE) 140 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var escapedChars = { 4 | 'b': '\b', 5 | 'f': '\f', 6 | 'n': '\n', 7 | 'r': '\r', 8 | 't': '\t', 9 | '"': '"', 10 | '/': '/', 11 | '\\': '\\' 12 | }; 13 | 14 | var A_CODE = 'a'.charCodeAt(); 15 | 16 | 17 | exports.parse = function (source, _, options) { 18 | var pointers = {}; 19 | var line = 0; 20 | var column = 0; 21 | var pos = 0; 22 | var bigint = options && options.bigint && typeof BigInt != 'undefined'; 23 | return { 24 | data: _parse('', true), 25 | pointers: pointers 26 | }; 27 | 28 | function _parse(ptr, topLevel) { 29 | whitespace(); 30 | var data; 31 | map(ptr, 'value'); 32 | var char = getChar(); 33 | switch (char) { 34 | case 't': read('rue'); data = true; break; 35 | case 'f': read('alse'); data = false; break; 36 | case 'n': read('ull'); data = null; break; 37 | case '"': data = parseString(); break; 38 | case '[': data = parseArray(ptr); break; 39 | case '{': data = parseObject(ptr); break; 40 | default: 41 | backChar(); 42 | if ('-0123456789'.indexOf(char) >= 0) 43 | data = parseNumber(); 44 | else 45 | unexpectedToken(); 46 | } 47 | map(ptr, 'valueEnd'); 48 | whitespace(); 49 | if (topLevel && pos < source.length) unexpectedToken(); 50 | return data; 51 | } 52 | 53 | function whitespace() { 54 | loop: 55 | while (pos < source.length) { 56 | switch (source[pos]) { 57 | case ' ': column++; break; 58 | case '\t': column += 4; break; 59 | case '\r': column = 0; break; 60 | case '\n': column = 0; line++; break; 61 | default: break loop; 62 | } 63 | pos++; 64 | } 65 | } 66 | 67 | function parseString() { 68 | var str = ''; 69 | var char; 70 | while (true) { 71 | char = getChar(); 72 | if (char == '"') { 73 | break; 74 | } else if (char == '\\') { 75 | char = getChar(); 76 | if (char in escapedChars) 77 | str += escapedChars[char]; 78 | else if (char == 'u') 79 | str += getCharCode(); 80 | else 81 | wasUnexpectedToken(); 82 | } else { 83 | str += char; 84 | } 85 | } 86 | return str; 87 | } 88 | 89 | function parseNumber() { 90 | var numStr = ''; 91 | var integer = true; 92 | if (source[pos] == '-') numStr += getChar(); 93 | 94 | numStr += source[pos] == '0' 95 | ? getChar() 96 | : getDigits(); 97 | 98 | if (source[pos] == '.') { 99 | numStr += getChar() + getDigits(); 100 | integer = false; 101 | } 102 | 103 | if (source[pos] == 'e' || source[pos] == 'E') { 104 | numStr += getChar(); 105 | if (source[pos] == '+' || source[pos] == '-') numStr += getChar(); 106 | numStr += getDigits(); 107 | integer = false; 108 | } 109 | 110 | var result = +numStr; 111 | return bigint && integer && (result > Number.MAX_SAFE_INTEGER || result < Number.MIN_SAFE_INTEGER) 112 | ? BigInt(numStr) 113 | : result; 114 | } 115 | 116 | function parseArray(ptr) { 117 | whitespace(); 118 | var arr = []; 119 | var i = 0; 120 | if (getChar() == ']') return arr; 121 | backChar(); 122 | 123 | while (true) { 124 | var itemPtr = ptr + '/' + i; 125 | arr.push(_parse(itemPtr)); 126 | whitespace(); 127 | var char = getChar(); 128 | if (char == ']') break; 129 | if (char != ',') wasUnexpectedToken(); 130 | whitespace(); 131 | i++; 132 | } 133 | return arr; 134 | } 135 | 136 | function parseObject(ptr) { 137 | whitespace(); 138 | var obj = {}; 139 | if (getChar() == '}') return obj; 140 | backChar(); 141 | 142 | while (true) { 143 | var loc = getLoc(); 144 | if (getChar() != '"') wasUnexpectedToken(); 145 | var key = parseString(); 146 | var propPtr = ptr + '/' + escapeJsonPointer(key); 147 | mapLoc(propPtr, 'key', loc); 148 | map(propPtr, 'keyEnd'); 149 | whitespace(); 150 | if (getChar() != ':') wasUnexpectedToken(); 151 | whitespace(); 152 | obj[key] = _parse(propPtr); 153 | whitespace(); 154 | var char = getChar(); 155 | if (char == '}') break; 156 | if (char != ',') wasUnexpectedToken(); 157 | whitespace(); 158 | } 159 | return obj; 160 | } 161 | 162 | function read(str) { 163 | for (var i=0; i= 'a' && char <= 'f') 187 | code += char.charCodeAt() - A_CODE + 10; 188 | else if (char >= '0' && char <= '9') 189 | code += +char; 190 | else 191 | wasUnexpectedToken(); 192 | } 193 | return String.fromCharCode(code); 194 | } 195 | 196 | function getDigits() { 197 | var digits = ''; 198 | while (source[pos] >= '0' && source[pos] <= '9') 199 | digits += getChar(); 200 | 201 | if (digits.length) return digits; 202 | checkUnexpectedEnd(); 203 | unexpectedToken(); 204 | } 205 | 206 | function map(ptr, prop) { 207 | mapLoc(ptr, prop, getLoc()); 208 | } 209 | 210 | function mapLoc(ptr, prop, loc) { 211 | pointers[ptr] = pointers[ptr] || {}; 212 | pointers[ptr][prop] = loc; 213 | } 214 | 215 | function getLoc() { 216 | return { 217 | line: line, 218 | column: column, 219 | pos: pos 220 | }; 221 | } 222 | 223 | function unexpectedToken() { 224 | throw new SyntaxError('Unexpected token ' + source[pos] + ' in JSON at position ' + pos); 225 | } 226 | 227 | function wasUnexpectedToken() { 228 | backChar(); 229 | unexpectedToken(); 230 | } 231 | 232 | function checkUnexpectedEnd() { 233 | if (pos >= source.length) 234 | throw new SyntaxError('Unexpected end of JSON input'); 235 | } 236 | }; 237 | 238 | 239 | exports.stringify = function (data, _, options) { 240 | if (!validType(data)) return; 241 | var wsLine = 0; 242 | var wsPos, wsColumn; 243 | var whitespace = typeof options == 'object' 244 | ? options.space 245 | : options; 246 | switch (typeof whitespace) { 247 | case 'number': 248 | var len = whitespace > 10 249 | ? 10 250 | : whitespace < 0 251 | ? 0 252 | : Math.floor(whitespace); 253 | whitespace = len && repeat(len, ' '); 254 | wsPos = len; 255 | wsColumn = len; 256 | break; 257 | case 'string': 258 | whitespace = whitespace.slice(0, 10); 259 | wsPos = 0; 260 | wsColumn = 0; 261 | for (var j=0; j= 0; 440 | } 441 | 442 | 443 | var ESC_QUOTE = /"|\\/g; 444 | var ESC_B = /[\b]/g; 445 | var ESC_F = /\f/g; 446 | var ESC_N = /\n/g; 447 | var ESC_R = /\r/g; 448 | var ESC_T = /\t/g; 449 | function quoted(str) { 450 | str = str.replace(ESC_QUOTE, '\\$&') 451 | .replace(ESC_F, '\\f') 452 | .replace(ESC_B, '\\b') 453 | .replace(ESC_N, '\\n') 454 | .replace(ESC_R, '\\r') 455 | .replace(ESC_T, '\\t'); 456 | return '"' + str + '"'; 457 | } 458 | 459 | 460 | var ESC_0 = /~/g; 461 | var ESC_1 = /\//g; 462 | function escapeJsonPointer(str) { 463 | return str.replace(ESC_0, '~0') 464 | .replace(ESC_1, '~1'); 465 | } 466 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "json-source-map", 3 | "version": "0.6.1", 4 | "description": "Parse/stringify JSON and provide source-map for JSON-pointers to all nodes", 5 | "main": "index.js", 6 | "scripts": { 7 | "eslint": "eslint index.js spec", 8 | "test-spec": "mocha spec -R spec", 9 | "test-debug": "mocha spec -R spec --debug-brk", 10 | "test": "npm run eslint && nyc npm run test-spec" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/epoberezkin/json-source-map.git" 15 | }, 16 | "keywords": [ 17 | "JSON", 18 | "parse", 19 | "stringify", 20 | "json-pointer", 21 | "source-map", 22 | "BigInt", 23 | "ES6", 24 | "Map", 25 | "Set", 26 | "TypedArray" 27 | ], 28 | "author": "Evgeny Poberezkin", 29 | "license": "MIT", 30 | "bugs": { 31 | "url": "https://github.com/epoberezkin/json-source-map/issues" 32 | }, 33 | "homepage": "https://github.com/epoberezkin/json-source-map#readme", 34 | "devDependencies": { 35 | "coveralls": "^2.11.15", 36 | "eslint": "^6.1.0", 37 | "json-pointer": "^0.6.0", 38 | "mocha": "^3.2.0", 39 | "nyc": "^10.0.0", 40 | "pre-commit": "^1.2.2" 41 | }, 42 | "nyc": { 43 | "exclude": [ 44 | "spec" 45 | ], 46 | "reporter": [ 47 | "lcov", 48 | "text-summary" 49 | ] 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /spec/.eslintrc.yml: -------------------------------------------------------------------------------- 1 | parserOptions: 2 | ecmaVersion: 6 3 | rules: 4 | no-console: 0 5 | quotes: 0 6 | globals: 7 | describe: false 8 | it: false 9 | Symbol: false 10 | Int8Array: false 11 | -------------------------------------------------------------------------------- /spec/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var jsonMap = require('../index'); 4 | var assert = require('assert'); 5 | var jsonPointer = require('json-pointer'); 6 | 7 | 8 | describe('parse', function() { 9 | describe('mappings', function() { 10 | it('should parse JSON and generate mappings', function() { 11 | var json = '{\n\ 12 | "foo": [\n\ 13 | {\n\ 14 | "bar": true\n\ 15 | },\n\ 16 | {\n\ 17 | "baz": 123,\n\ 18 | "quux": "hello"\n\ 19 | }\n\ 20 | ]\n\ 21 | }'; 22 | 23 | var pointers = testParse(json, JSON.parse(json), null, 2); 24 | assert.deepStrictEqual(pointers, { 25 | '': { 26 | value: { line: 0, column: 0, pos: 0 }, 27 | valueEnd: { line: 10, column: 1, pos: 101 } 28 | }, 29 | '/foo': { 30 | key: { line: 1, column: 2, pos: 4 }, 31 | keyEnd: { line: 1, column: 7, pos: 9 }, 32 | value: { line: 1, column: 9, pos: 11 }, 33 | valueEnd: { line: 9, column: 3, pos: 99 } 34 | }, 35 | '/foo/0': { 36 | value: { line: 2, column: 4, pos: 17 }, 37 | valueEnd: { line: 4, column: 5, pos: 42 } 38 | }, 39 | '/foo/0/bar': { 40 | key: { line: 3, column: 6, pos: 25 }, 41 | keyEnd: { line: 3, column: 11, pos: 30 }, 42 | value: { line: 3, column: 13, pos: 32 }, 43 | valueEnd: { line: 3, column: 17, pos: 36 } 44 | }, 45 | '/foo/1': { 46 | value: { line: 5, column: 4, pos: 48 }, 47 | valueEnd: { line: 8, column: 5, pos: 95 } 48 | }, 49 | '/foo/1/baz': { 50 | key: { line: 6, column: 6, pos: 56 }, 51 | keyEnd: { line: 6, column: 11, pos: 61 }, 52 | value: { line: 6, column: 13, pos: 63 }, 53 | valueEnd: { line: 6, column: 16, pos: 66 } 54 | }, 55 | '/foo/1/quux': { 56 | key: { line: 7, column: 6, pos: 74 }, 57 | keyEnd: { line: 7, column: 12, pos: 80 }, 58 | value: { line: 7, column: 14, pos: 82 }, 59 | valueEnd: { line: 7, column: 21, pos: 89 } 60 | } 61 | }); 62 | }); 63 | 64 | it('should support whitespace with tabs', function () { 65 | var json = '{\n\ 66 | \t"foo": [\n\ 67 | \t\t{\n\ 68 | \t\t\t"bar": true\n\ 69 | \t\t}\n\ 70 | \t]\n\ 71 | }'; 72 | 73 | var pointers = testParse(json, JSON.parse(json), null, '\t'); 74 | assert.deepStrictEqual(pointers, { 75 | '': { 76 | value: { line: 0, column: 0, pos: 0 }, 77 | valueEnd: { line: 6, column: 1, pos: 39 } 78 | }, 79 | '/foo': { 80 | key: { line: 1, column: 4, pos: 3 }, 81 | keyEnd: { line: 1, column: 9, pos: 8 }, 82 | value: { line: 1, column: 11, pos: 10 }, 83 | valueEnd: { line: 5, column: 5, pos: 37 } 84 | }, 85 | '/foo/0': { 86 | value: { line: 2, column: 8, pos: 14 }, 87 | valueEnd: { line: 4, column: 9, pos: 34 } 88 | }, 89 | '/foo/0/bar': { 90 | key: { line: 3, column: 12, pos: 19 }, 91 | keyEnd: { line: 3, column: 17, pos: 24 }, 92 | value: { line: 3, column: 19, pos: 26 }, 93 | valueEnd: { line: 3, column: 23, pos: 30 } 94 | } 95 | }); 96 | }); 97 | 98 | it('should support whitespace with CRs', function () { 99 | var json = '{\r\n\ 100 | "foo": [\r\n\ 101 | {\r\n\ 102 | "bar": true\r\n\ 103 | }\r\n\ 104 | ]\r\n\ 105 | }'; 106 | 107 | var pointers = testParse(json, JSON.parse(json), true); 108 | assert.deepStrictEqual(pointers, { 109 | '': { 110 | value: { line: 0, column: 0, pos: 0 }, 111 | valueEnd: { line: 6, column: 1, pos: 54 } 112 | }, 113 | '/foo': { 114 | key: { line: 1, column: 2, pos: 5 }, 115 | keyEnd: { line: 1, column: 7, pos: 10 }, 116 | value: { line: 1, column: 9, pos: 12 }, 117 | valueEnd: { line: 5, column: 3, pos: 51 } 118 | }, 119 | '/foo/0': { 120 | value: { line: 2, column: 4, pos: 19 }, 121 | valueEnd: { line: 4, column: 5, pos: 46 } 122 | }, 123 | '/foo/0/bar': { 124 | key: { line: 3, column: 6, pos: 28 }, 125 | keyEnd: { line: 3, column: 11, pos: 33 }, 126 | value: { line: 3, column: 13, pos: 35 }, 127 | valueEnd: { line: 3, column: 17, pos: 39 } 128 | } 129 | }); 130 | }); 131 | }); 132 | 133 | 134 | describe('simple values', function() { 135 | it('should throw exception on empty line/whitespace', function() { 136 | testParseFailEnd(''); 137 | testParseFailEnd(' '); 138 | }); 139 | 140 | 141 | it('should parse true/false/null', function() { 142 | testParse('true', true); 143 | testParse('false', false); 144 | testParse('null', null); 145 | 146 | testParseFailToken('ture', 'u', 1); 147 | testParseFailToken('truz', 'z', 3); 148 | testParseFailToken('truetrue', 't', 4); 149 | testParseFailToken('true true', 't', 5); 150 | testParseFailToken('undefined', 'u', 0); 151 | testParseFailEnd('tru'); 152 | }); 153 | 154 | it('should parse strings', function() { 155 | testParse('"foo"', 'foo'); 156 | testParse('"foo\\bbar"', 'foo\bbar'); 157 | testParse('"foo\\fbar"', 'foo\fbar'); 158 | testParse('"foo\\nbar"', 'foo\nbar'); 159 | testParse('"foo\\rbar"', 'foo\rbar'); 160 | testParse('"foo\\tbar"', 'foo\tbar'); 161 | testParse('"foo\\"bar"', 'foo"bar'); 162 | testParse('"foo\\/bar"', 'foo/bar', true); // reverse check fails because '/' stringifies as '"/"' (backslach is optional) 163 | testParse('"foo\\\\bar"', 'foo\\bar'); 164 | testParse('"foo\\u000Abar"', 'foo\nbar', true); 165 | testParse('"foo\\u000abar"', 'foo\nbar', true); 166 | testParse('"foo\\u2028bar"', 'foo\u2028bar', true); 167 | 168 | testParseFailToken('"foo\\abar"', 'a', 5); 169 | testParseFailToken('"foo\\u000Xbar"', 'X', 9); 170 | testParseFailToken('"foo"true', 't', 5); 171 | testParseFailToken('"foo" "foo"', '"', 6); 172 | testParseFailEnd('"foo'); 173 | }); 174 | 175 | it('should parse numbers', function() { 176 | testParse('123', 123); 177 | testParse('123.45', 123.45); 178 | testParse('-123.45', -123.45); 179 | testParse('0', 0); 180 | testParse('0.45', 0.45); 181 | testParse('1e2', 100, true); 182 | testParse('1e+2', 100, true); 183 | testParse('1e-2', 0.01, true); 184 | testParse('1.23e2', 123, true); 185 | testParse('1.23e-2', 0.0123, true); 186 | testParse('1.23e12', 1230000000000, true); 187 | 188 | testParseFailToken('123a', 'a', 3); 189 | testParseFailToken('123.a', 'a', 4); 190 | testParseFailToken('--123', '-', 1); 191 | testParseFailToken('+123', '+', 0); 192 | testParseFailToken('01', '1', 1); 193 | testParseFailToken('00', '0', 1); 194 | testParseFailToken('1..', '.', 2); 195 | testParseFailToken('1.e2', 'e', 2); 196 | testParseFailToken('1.23ee', 'e', 5); 197 | testParseFailEnd('1.'); 198 | testParseFailEnd('1.23e'); 199 | }); 200 | 201 | describe('option "bigint"', function() { 202 | it('should parse large integers as BigInt with option bigint: true', function() { 203 | testParseBigInt('' + (Number.MAX_SAFE_INTEGER + 1)); 204 | testParseBigInt('' + (Number.MIN_SAFE_INTEGER - 1)); 205 | testParseBigInt('10000000000000000'); 206 | testParseBigInt('-10000000000000000'); 207 | }); 208 | 209 | it('should parse large integers as Number without option bigint', function() { 210 | testParseNumber('' + (Number.MAX_SAFE_INTEGER + 1), false); 211 | testParseNumber('' + (Number.MIN_SAFE_INTEGER - 1), false); 212 | testParseNumber('10000000000000000', false); 213 | testParseNumber('-10000000000000000', false); 214 | }); 215 | 216 | it('should parse small integers and non-integers as Number with option bigint: true', function() { 217 | testParseNumber('' + Number.MAX_SAFE_INTEGER); 218 | testParseNumber('' + Number.MIN_SAFE_INTEGER); 219 | testParseNumber('1e16'); 220 | testParseNumber('-1e16'); 221 | testParseNumber('10000000000000000.1'); 222 | testParseNumber('-10000000000000000.1'); 223 | testParseNumber('10000'); 224 | testParseNumber('-10000'); 225 | testParseNumber('1.1'); 226 | testParseNumber('-1.1'); 227 | }); 228 | 229 | function testParseBigInt(str) { 230 | var result = jsonMap.parse(str, null, {bigint: true}); 231 | assert.strictEqual(typeof result.data, 'bigint'); 232 | assert.strictEqual(result.data, BigInt(str)); 233 | } 234 | 235 | function testParseNumber(str, opt=true) { 236 | var result = jsonMap.parse(str, null, {bigint: opt}); 237 | assert.strictEqual(typeof result.data, 'number'); 238 | assert.strictEqual(result.data, +str); 239 | } 240 | }); 241 | }); 242 | 243 | 244 | describe('composite values', function() { 245 | it('should parse arrays', function() { 246 | testParse('[]', []); 247 | testParse('[1]', [1]); 248 | testParse('[1.23,"foo",true,false,null]', [1.23,"foo",true,false,null]); 249 | 250 | testParseFailToken('[1,]', ']', 3); 251 | testParseFailToken('[1;', ';', 2); 252 | testParseFailEnd('['); 253 | testParseFailEnd('[1'); 254 | testParseFailEnd('[1,'); 255 | }); 256 | 257 | it('should parse objects', function() { 258 | testParse('{}', {}); 259 | testParse('{"foo":"bar"}', {foo: 'bar'}); 260 | testParse('{"foo":1,"bar":2}', {foo: 1, bar: 2}); 261 | 262 | testParseFailToken('{\'', '\'', 1); 263 | testParseFailToken('{"foo";', ';', 6); 264 | testParseFailToken('{"foo":1;', ';', 8); 265 | 266 | testParseFailEnd('{'); 267 | testParseFailEnd('{"'); 268 | testParseFailEnd('{"foo'); 269 | testParseFailEnd('{"foo"'); 270 | testParseFailEnd('{"foo":'); 271 | testParseFailEnd('{"foo":"'); 272 | testParseFailEnd('{"foo":"bar'); 273 | testParseFailEnd('{"foo":"bar"'); 274 | testParseFailEnd('{"foo":"bar",'); 275 | }); 276 | 277 | it('should parse nested structures', function() { 278 | var data = { 279 | foo: [ 280 | { 281 | bar: true 282 | }, 283 | { 284 | baz: 123, 285 | quux: 'hello' 286 | } 287 | ] 288 | }; 289 | 290 | testParse(JSON.stringify(data), data); 291 | testParse(JSON.stringify(data, null, 2), data, null, 2); 292 | }); 293 | }); 294 | 295 | 296 | function testParse(json, expectedData, skipReverseCheck, whitespace) { 297 | var result = jsonMap.parse(json); 298 | var data = result.data; 299 | var pointers = result.pointers; 300 | assert.deepStrictEqual(data, expectedData); 301 | testResult(json, pointers, data); 302 | 303 | if (!skipReverseCheck) { 304 | var reverseResult = jsonMap.stringify(expectedData, null, whitespace); 305 | assert.strictEqual(json, reverseResult.json); 306 | assert.deepStrictEqual(pointers, reverseResult.pointers); 307 | } 308 | return pointers; 309 | } 310 | 311 | function testParseFailToken(json, token, pos) { 312 | testParseFail(json, 'Unexpected token ' + token + ' in JSON at position ' + pos); 313 | } 314 | 315 | function testParseFailEnd(json) { 316 | testParseFail(json, 'Unexpected end of JSON input'); 317 | } 318 | 319 | function testParseFail(json, expectedMessage) { 320 | try { 321 | jsonMap.parse(json); 322 | assert.fail('should have thrown exception'); 323 | } catch(e) { 324 | if (e instanceof assert.AssertionError) throw e; 325 | assert(e instanceof SyntaxError); 326 | assert.equal(e.message, expectedMessage); 327 | } 328 | } 329 | }); 330 | 331 | 332 | describe('stringify', function() { 333 | it('should stringify data and generate mappings', function() { 334 | var data = { 335 | "foo": [ 336 | { 337 | "bar": 1 338 | }, 339 | { 340 | "baz": 2, 341 | "quux": 3 342 | } 343 | ] 344 | }; 345 | 346 | var pointers = testStringify(data, data, null, 2); 347 | assert.deepEqual(pointers, { 348 | '': { 349 | value: { line: 0, column: 0, pos: 0 }, 350 | valueEnd: { line: 10, column: 1, pos: 90 } 351 | }, 352 | '/foo': { 353 | key: { line: 1, column: 2, pos: 4 }, 354 | keyEnd: { line: 1, column: 7, pos: 9 }, 355 | value: { line: 1, column: 9, pos: 11 }, 356 | valueEnd: { line: 9, column: 3, pos: 88 } 357 | }, 358 | '/foo/0': { 359 | value: { line: 2, column: 4, pos: 17 }, 360 | valueEnd: { line: 4, column: 5, pos: 39 } 361 | }, 362 | '/foo/0/bar': { 363 | key: { line: 3, column: 6, pos: 25 }, 364 | keyEnd: { line: 3, column: 11, pos: 30 }, 365 | value: { line: 3, column: 13, pos: 32 }, 366 | valueEnd: { line: 3, column: 14, pos: 33 } 367 | }, 368 | '/foo/1': { 369 | value: { line: 5, column: 4, pos: 45 }, 370 | valueEnd: { line: 8, column: 5, pos: 84 } 371 | }, 372 | '/foo/1/baz': { 373 | key: { line: 6, column: 6, pos: 53 }, 374 | keyEnd: { line: 6, column: 11, pos: 58 }, 375 | value: { line: 6, column: 13, pos: 60 }, 376 | valueEnd: { line: 6, column: 14, pos: 61 } 377 | }, 378 | "/foo/1/quux": { 379 | "key": { 380 | "column": 6, 381 | "line": 7, 382 | "pos": 69 383 | }, 384 | "keyEnd": { 385 | "column": 12, 386 | "line": 7, 387 | "pos": 75 388 | }, 389 | "value": { 390 | "column": 14, 391 | "line": 7, 392 | "pos": 77 393 | }, 394 | "valueEnd": { 395 | "column": 15, 396 | "line": 7, 397 | "pos": 78 398 | } 399 | } 400 | }); 401 | }); 402 | 403 | it('should stringify string, null, empty array, empty object, Date', function() { 404 | var data = { 405 | str: 'foo', 406 | null: null, 407 | arr: [], 408 | obj: {}, 409 | date: new Date('2017-01-09T08:50:13.064Z'), 410 | custom: { 411 | toJSON: function () { return 'custom'; } 412 | }, 413 | control: '"\f\b\n\r\t"', 414 | 'esc/aped~': true 415 | }; 416 | 417 | var reverseData = copy(data); 418 | reverseData.date = '2017-01-09T08:50:13.064Z'; 419 | reverseData.custom = 'custom'; 420 | 421 | var pointers = testStringify(data, reverseData, null, ' '); 422 | 423 | assert.deepEqual(pointers, { 424 | '': { 425 | value: { line: 0, column: 0, pos: 0 }, 426 | valueEnd: { line: 9, column: 1, pos: 172 } 427 | }, 428 | '/str': { 429 | key: { line: 1, column: 2, pos: 4 }, 430 | keyEnd: { line: 1, column: 7, pos: 9 }, 431 | value: { line: 1, column: 9, pos: 11 }, 432 | valueEnd: { line: 1, column: 14, pos: 16 } 433 | }, 434 | '/null': { 435 | key: { line: 2, column: 2, pos: 20 }, 436 | keyEnd: { line: 2, column: 8, pos: 26 }, 437 | value: { line: 2, column: 10, pos: 28 }, 438 | valueEnd: { line: 2, column: 14, pos: 32 } 439 | }, 440 | '/arr': { 441 | key: { line: 3, column: 2, pos: 36 }, 442 | keyEnd: { line: 3, column: 7, pos: 41 }, 443 | value: { line: 3, column: 9, pos: 43 }, 444 | valueEnd: { line: 3, column: 11, pos: 45 } 445 | }, 446 | '/obj': { 447 | key: { line: 4, column: 2, pos: 49 }, 448 | keyEnd: { line: 4, column: 7, pos: 54 }, 449 | value: { line: 4, column: 9, pos: 56 }, 450 | valueEnd: { line: 4, column: 11, pos: 58 } 451 | }, 452 | '/date': { 453 | key: { line: 5, column: 2, pos: 62 }, 454 | keyEnd: { line: 5, column: 8, pos: 68 }, 455 | value: { line: 5, column: 10, pos: 70 }, 456 | valueEnd: { line: 5, column: 36, pos: 96 } 457 | }, 458 | '/custom': { 459 | key: { line: 6, column: 2, pos: 100 }, 460 | keyEnd: { line: 6, column: 10, pos: 108 }, 461 | value: { line: 6, column: 12, pos: 110 }, 462 | valueEnd: { line: 6, column: 20, pos: 118 } 463 | }, 464 | '/control': { 465 | key: { column: 2, line: 7, pos: 122 }, 466 | keyEnd: { column: 11, line: 7, pos: 131 }, 467 | value: { column: 13, line: 7, pos: 133 }, 468 | valueEnd: { column: 29, line: 7, pos: 149 } 469 | }, 470 | '/esc~1aped~0': { 471 | key: { line: 8, column: 2, pos: 153 }, 472 | keyEnd: { line: 8, column: 13, pos: 164 }, 473 | value: { line: 8, column: 15, pos: 166 }, 474 | valueEnd: { line: 8, column: 19, pos: 170 } 475 | } 476 | }); 477 | }); 478 | 479 | it('should stringify BigInt', function() { 480 | testStringify(BigInt(100), 100); 481 | }); 482 | 483 | it('should return undefined if data is not a valid type', function() { 484 | assert.strictEqual(jsonMap.stringify(undefined), undefined); 485 | assert.strictEqual(jsonMap.stringify(function(){}), undefined); 486 | assert.strictEqual(jsonMap.stringify(Symbol()), undefined); 487 | }); 488 | 489 | it('should generate JSON without whitespace', function() { 490 | var data = { 491 | foo: [ 492 | { 493 | bar: 1 494 | } 495 | ] 496 | }; 497 | 498 | var pointers = testStringify(data); 499 | 500 | assert.deepStrictEqual(pointers, { 501 | '': { 502 | value: { line: 0, column: 0, pos: 0 }, 503 | valueEnd: { line: 0, column: 19, pos: 19 } 504 | }, 505 | '/foo': { 506 | key: { line: 0, column: 1, pos: 1 }, 507 | keyEnd: { line: 0, column: 6, pos: 6 }, 508 | value: { line: 0, column: 7, pos: 7 }, 509 | valueEnd: { line: 0, column: 18, pos: 18 } 510 | }, 511 | '/foo/0': { 512 | value: { line: 0, column: 8, pos: 8 }, 513 | valueEnd: { line: 0, column: 17, pos: 17 } 514 | }, 515 | '/foo/0/bar': { 516 | key: { line: 0, column: 9, pos: 9 }, 517 | keyEnd: { line: 0, column: 14, pos: 14 }, 518 | value: { line: 0, column: 15, pos: 15 }, 519 | valueEnd: { line: 0, column: 16, pos: 16 } 520 | } 521 | }); 522 | }); 523 | 524 | it('should skip properties with invalid types', function() { 525 | var data = { 526 | foo: { 527 | bar: null, 528 | baz: undefined, 529 | quux: function(){}, 530 | sym: Symbol() 531 | } 532 | }; 533 | 534 | assert.deepStrictEqual( 535 | jsonMap.stringify(data), 536 | jsonMap.stringify({foo: {bar: null}}) 537 | ); 538 | }); 539 | 540 | it('should stringify items with invalid types as null', function() { 541 | var data = { 542 | foo: [ 543 | null, 544 | undefined, 545 | function(){}, 546 | Symbol() 547 | ] 548 | }; 549 | 550 | assert.deepStrictEqual( 551 | jsonMap.stringify(data), 552 | jsonMap.stringify({foo: [null, null, null, null]}) 553 | ); 554 | }); 555 | 556 | it('should limit whitespace', function() { 557 | var data = { 558 | "foo": [ 559 | { 560 | "bar": 1 561 | }, 562 | { 563 | "baz": 2, 564 | "quux": 3 565 | } 566 | ] 567 | }; 568 | 569 | equal([ 570 | jsonMap.stringify(data), 571 | jsonMap.stringify(data, null, -1), 572 | jsonMap.stringify(data, null, 0), 573 | jsonMap.stringify(data, null, '') 574 | ]); 575 | 576 | equal([ 577 | jsonMap.stringify(data, null, 10), 578 | jsonMap.stringify(data, null, 20), 579 | jsonMap.stringify(data, null, Array(10 + 1).join(' ')), 580 | jsonMap.stringify(data, null, Array(20).join(' ')) 581 | ]); 582 | 583 | assert.notDeepStrictEqual( 584 | jsonMap.stringify(data, null, 10), 585 | jsonMap.stringify(data, null, Array(9 + 1).join(' ')) 586 | ); 587 | }); 588 | 589 | it('should stringify with CR/LF whitespace', function() { 590 | var data = { 591 | "foo": [ 592 | { 593 | "bar": 1 594 | }, 595 | { 596 | "baz": 2, 597 | "quux": 3 598 | } 599 | ] 600 | }; 601 | 602 | testStringify(data, data, null, '\r'); 603 | testStringify(data, data, null, '\n'); 604 | testStringify(data, data, null, '\r\n'); 605 | }); 606 | 607 | it('should throw if whitespace not allowed in JSON is used', function() { 608 | var data = { foo: 'bar' }; 609 | 610 | assert.throws(function() { 611 | jsonMap.stringify(data, null, '$$'); 612 | }); 613 | }); 614 | 615 | it('should support whitespace as option', function() { 616 | var data = { foo: 'bar' }; 617 | var result = jsonMap.stringify(data, null, {space: ' '}); 618 | assert.equal(result.json, '{\n "foo": "bar"\n}'); 619 | }); 620 | 621 | describe('option es6', function() { 622 | it('should strigify Maps', function() { 623 | var data = new Map; 624 | testStringify(data, {}, false, {es6: true}); 625 | 626 | data.set('foo', 1); 627 | data.set('bar', 2); 628 | testStringify(data, {foo: 1, bar: 2}, false, {es6: true}); 629 | testStringify(data, {foo: 1, bar: 2}, false, {es6: true, space: 2}); 630 | }); 631 | 632 | it('should strigify Sets', function() { 633 | var data = new Set; 634 | testStringify(data, {}, false, {es6: true}); 635 | 636 | data.add('foo'); 637 | data.add('bar'); 638 | testStringify(data, {foo: true, bar: true}, false, {es6: true}); 639 | testStringify(data, {foo: true, bar: true}, false, {es6: true, space: 2}); 640 | }); 641 | 642 | it('should strigify Typed arrays', function() { 643 | var data = new Int8Array(2); 644 | testStringify(data, [0, 0], false, {es6: true}); 645 | 646 | data[0] = 1; 647 | data[1] = 2; 648 | testStringify(data, [1, 2], false, {es6: true}); 649 | testStringify(data, [1, 2], false, {es6: true, space: 2}); 650 | }); 651 | 652 | it('should still strigify Objects', function() { 653 | testStringify({}, {}, false, {es6: true}); 654 | testStringify({foo: 1, bar: 2}, {foo: 1, bar: 2}, false, {es6: true}); 655 | }); 656 | }); 657 | 658 | function equal(objects) { 659 | for (var i=1; i