├── .editorconfig ├── .gitignore ├── .npmignore ├── .prettierignore ├── .travis.yml ├── LICENSE ├── README.md ├── docs-src ├── demo.ts └── index.html ├── docs ├── default.40d73132.css ├── default.40d73132.css.map ├── demo.4d1dd13d.js ├── demo.4d1dd13d.js.map └── index.html ├── examples └── basic │ └── index.html ├── package-lock.json ├── package.json ├── src ├── big-json-viewer-dom.spec.ts ├── big-json-viewer-dom.ts ├── big-json-viewer-service.ts ├── browser-api.ts ├── helpers │ ├── utils.ts │ ├── worker-client.spec.ts │ ├── worker-client.ts │ └── worker-provider.ts ├── index.ts ├── model │ └── big-json-viewer.model.ts ├── parser │ ├── buffer-json-parser.spec.ts │ ├── buffer-json-parser.ts │ ├── js-parser.spec.ts │ ├── js-parser.ts │ ├── json-node-info.ts │ ├── json-node-search.spec.ts │ └── json-node-search.ts └── worker │ ├── big-json-viewer.worker.inline.ts │ └── big-json-viewer.worker.ts ├── styles └── default.css ├── tools └── build-inline-worker.js ├── tsconfig.json └── tslint.json /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | max_line_length = off 13 | trim_trailing_whitespace = false 14 | indent_size = 4 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | /.idea 3 | /.vscode 4 | /coverage 5 | /dist 6 | /.cache 7 | /demo-dist 8 | /docs/tmp 9 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | /src 2 | /demo 3 | node_modules 4 | /.idea 5 | /.vscode 6 | /coverage 7 | /.cache 8 | /*.tgz 9 | /demo-dist 10 | /docs 11 | /docs-src 12 | /tools 13 | 14 | 15 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | /docs 2 | /coverage 3 | /node_modules 4 | /.idea 5 | /.cache 6 | /src/worker/big-json-viewer.worker.inline.ts 7 | /dist 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - node 4 | 5 | install: 6 | - npm install 7 | - npm install codecov -g 8 | 9 | script: 10 | - npm test 11 | 12 | after_success: 13 | - codecov 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Dominik Herbst 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 | # Big JSON Viewer 2 | 3 | [![npm](https://img.shields.io/npm/v/big-json-viewer.svg)](https://www.npmjs.com/package/big-json-viewer) 4 | [![Travis](https://img.shields.io/travis/dhcode/big-json-viewer.svg)](https://travis-ci.org/dhcode/big-json-viewer) 5 | [![Codecov](https://img.shields.io/codecov/c/github/dhcode/big-json-viewer.svg)](https://codecov.io/gh/dhcode/big-json-viewer) 6 | [![code style: prettier](https://img.shields.io/badge/code_style-prettier-ff69b4.svg)](https://www.npmjs.com/package/big-json-viewer) 7 | 8 | A JavaScript library that enables efficient working with large JSON data in the browser. 9 | 10 | The JSON data is held as ArrayBuffer and only parsed for structural information. 11 | 12 | Information about the top level nodes is provided. Pagination enabled browsing of arrays and objects. 13 | 14 | No dependencies, works directly on the DOM API. Runs in any modern browser and IE11. 15 | 16 | [View the Demo](https://dhcode.github.io/big-json-viewer/) 17 | 18 | ## Usage 19 | 20 | npm install big-json-viewer 21 | 22 | ## Example usage 23 | 24 | test.ts 25 | 26 | ```typescript 27 | import { BigJsonViewerDom } from 'big-json-viewer'; 28 | 29 | BigJsonViewerDom.fromData(JSON.stringify({ test: 23 })).then(viewer => { 30 | const node = viewer.getRootElement(); 31 | document.body.appendChild(node); 32 | node.openAll(1); 33 | }); 34 | ``` 35 | 36 | index.html 37 | 38 | ```html 39 | 40 | 41 | 42 | 43 | Test 44 | 45 | 46 | 47 | 48 | 49 | 50 | ``` 51 | 52 | Example run with `parcel` (`npm install -D parcel-bundler`); 53 | 54 | parcel index.html 55 | 56 | ## More examples 57 | 58 | See [examples/basic/index.html](examples/basic/index.html) for plain javascript example. 59 | 60 | See [docs-src/demo.ts](docs-src/demo.ts) for a more advanced example. 61 | 62 | ## Getting started 63 | 64 | You can use the following static method to get a new viewer instance: 65 | 66 | ```typescript 67 | import { BigJsonViewerDom, BigJsonViewerOptions } from 'big-json-viewer'; 68 | BigJsonViewerDom.fromData(data: ArrayBuffer | string, options?: BigJsonViewerOptions): Promise 69 | ``` 70 | 71 | It returns a `BigJsonViewerDom` instance. Call `getRootElement()` on it to get a `JsonNodeElement`, that is an `HTMLDivElement` with some extras. You can insert it anywhere in your DOM. 72 | 73 | ## Options 74 | 75 | When calling `fromData`, you can provide an object matching the interface `BigJsonViewerOptions`. 76 | 77 | Example: 78 | 79 | ```javascript 80 | { 81 | objectNodesLimit: 50, // how many properties of an object should be shows before it gets paginatated with a pagination size of 50 82 | arrayNodesLimit: 50, // same as objectNodesLimit, but with array elements 83 | labelAsPath: false // if true the label for every node will show the full path to the element 84 | } 85 | ``` 86 | 87 | ## API 88 | 89 | ## `BigJsonViewerDom` static methods 90 | 91 | #### `fromData(data: ArrayBuffer | string, options?: BigJsonViewerOptions): Promise` 92 | 93 | Initilizes a new viewer with JSON encoded data 94 | 95 | #### `fromObject(data: string | object | null | number | boolean, options?: BigJsonViewerOptions): Promise` 96 | 97 | Initializes a new viewer with JavaScript data 98 | 99 | ## `BigJsonViewerDom` methods 100 | 101 | #### `getRootElement()` 102 | 103 | Returns the `JsonNodeElement` that can be appended to the DOM. 104 | 105 | #### `destroy()` 106 | 107 | Call this to free resources. It will terminate any by the instance started worker. 108 | 109 | #### `openBySearch(pattern: RegExp, openLimit?: number, searchArea?: TreeSearchAreaOption): TreeSearchCursor;` 110 | 111 | Searches the tree by the specified `pattern` and `searchArea`. Returns a `TreeSearchCursor`, which contains all matches and methods to jump the focus between the matches. 112 | 113 | * `openLimit` is `1` by default. But can be `Infinity` or any number. 114 | * `searchArea` describes where the pattern should be searched. Has the following options: 115 | * `'all'` search in keys and values (default) 116 | * `'keys'` search only in keys 117 | * `'values'` search only in values 118 | 119 | ## `JsonNodeElement` methods 120 | 121 | #### `openNode()` 122 | 123 | Opens the node in case it is an openable node. No event is fired. 124 | 125 | #### `closeNode()` 126 | 127 | Closes the node in case it is open. No event is fired. 128 | 129 | #### `toggleNode()` 130 | 131 | Toggles the open state of the node. Either opens or closes it. No event is fired. 132 | 133 | #### `openPath(path: string[]): JsonNodeElement` 134 | 135 | Opens the specified path and returns the opened node, in case it was found. 136 | 137 | #### `openAll(maxDepth?: number, paginated?: PaginatedOption): number` 138 | 139 | Opens all nodes until the defined depth. Returns the number of opened nodes. 140 | 141 | * `maxDepth` is `Infinity` by default 142 | * `paginated` is a string of the following options 143 | * `'first'` open only the first pagination stub (default) 144 | * `'all'` open all pagination stubs 145 | * `'none'` open no pagination stubs 146 | 147 | #### `getOpenPaths(withStubs?: boolean): string[][]` 148 | 149 | Returns a list of opened paths. 150 | `withStubs` is `true` by default. It makes sure, that paginated stubs that are opened are considered. 151 | 152 | When you have a limit of 50 nodes and you open the second stub `[50 ... 99]`, a path it retuned that contains the name of the first node in the stub. 153 | 154 | ### `JsonNodeElement` Events 155 | 156 | The following events are being fired on the visible DOM elements. The events bubble up, so you just need a listener to your root element. 157 | 158 | #### openNode 159 | 160 | Fires when a node is being opened by the user directly with a click. The target is a `JsonNodeElement`. 161 | 162 | Example logs the opened path: 163 | 164 | ```javascript 165 | rootNode.addEventListener('openNode', function(e) { 166 | console.log('openNode', e.target.jsonNode.path); 167 | }); 168 | ``` 169 | 170 | #### closeNode 171 | 172 | Fires when a node is being closed. The target is a `JsonNodeElement`. 173 | 174 | #### openedNodes 175 | 176 | Fires when multiple nodes have been opened. Target is the top level `JsonNodeElement` that was used to trigger the action. E.g. when the user clicks the _Expand all_ link. 177 | 178 | #### openStub 179 | 180 | Fires when a pagination stub is being opened directly by the user with a click. The target is a `JsonNodesStubElement`. 181 | 182 | #### closeStub 183 | 184 | Fires when a pagination stub is being closed. The target is a `JsonNodesStubElement`. 185 | 186 | #### copyPath 187 | 188 | Fires when the user clicks on the Copy Path link of a node. 189 | 190 | ## Contributing 191 | 192 | Anyone is welcome to contribute. 193 | 194 | If something has changed that affects the Docs/Demo page, run: 195 | 196 | npm run build-docs 197 | 198 | ### Future TODOs 199 | 200 | * Improve display of large strings. 201 | * Support JSON Schema. If provided show meta information from the schema definition. 202 | 203 | ## License 204 | 205 | [MIT](LICENSE) 206 | -------------------------------------------------------------------------------- /docs-src/demo.ts: -------------------------------------------------------------------------------- 1 | import 'babel-polyfill'; 2 | import { BigJsonViewerDom, JsonNodeElement } from '../src'; 3 | 4 | const demoData = { 5 | simpleData: { 6 | element1: 'str', 7 | element2: 1234, 8 | element3: [23, 43, true, false, null, { name: 'special' }, {}], 9 | element4: [], 10 | element5: 'this should be some long text\nwith line break', 11 | element6: { 12 | name: 'Hero', 13 | age: 32, 14 | birthday: { year: 1986, month: 4, day: 30 } 15 | }, 16 | element7: [1, 1, 1, 1, 1, 1, 1, 1, 1, 1] 17 | }, 18 | jsData: { 19 | element1: 'str', 20 | element2: 1234, 21 | element3: [23, 43, true, false, null, { name: 'special' }, {}], 22 | element4: [], 23 | element5: 'this should be some long text\nwith line break', 24 | element6: { 25 | name: 'Hero', 26 | age: 32, 27 | birthday: { year: 1986, month: 4, day: 30 } 28 | }, 29 | element7: [1, 1, 1, 1, 1, 1, 1, 1, 1, 1], 30 | element8: { un: undefined, nu: null } 31 | }, 32 | largeData: (function() { 33 | const list = new Array(Math.floor(Math.random() * 1000)); 34 | for (let i = 0; i < list.length; i++) { 35 | list[i] = Math.random(); 36 | if (list[i] < 0.2) { 37 | list[i] = 'hey ' + list[i]; 38 | } 39 | if (list[i] > 0.8) { 40 | list[i] = {}; 41 | const entries = Math.floor(Math.random() * 1000); 42 | for (let j = 0; j < entries; j++) { 43 | list[i]['entry-' + j] = Math.random(); 44 | } 45 | } 46 | } 47 | return list; 48 | })() 49 | }; 50 | 51 | const codeElement = document.getElementById('code') as HTMLTextAreaElement; 52 | const viewerElement = document.getElementById('viewer') as HTMLDivElement; 53 | const pathsElement = document.getElementById('paths') as HTMLTextAreaElement; 54 | const copiedElement = document.getElementById('copied') as HTMLInputElement; 55 | const searchElement = document.getElementById('search') as HTMLInputElement; 56 | const searchInfoElement = document.getElementById( 57 | 'searchInfo' 58 | ) as HTMLSpanElement; 59 | let viewer = null; 60 | let rootNode = document.getElementById('rootNode') as JsonNodeElement; 61 | 62 | querySelectorArray('[data-load]').forEach((link: any) => { 63 | const load = link.getAttribute('data-load'); 64 | if (demoData[load] && !link.loadListener) { 65 | link.loadListener = true; 66 | link.addEventListener('click', e => { 67 | e.preventDefault(); 68 | loadStructureData(demoData[load], load === 'jsData'); 69 | }); 70 | } 71 | }); 72 | 73 | codeElement.addEventListener('input', e => { 74 | console.log('show data based on input'); 75 | showData(codeElement.value); 76 | }); 77 | searchElement.addEventListener('input', async e => { 78 | if (searchElement.value.length >= 2) { 79 | const cursor = await viewer.openBySearch( 80 | new RegExp(searchElement.value, 'i') 81 | ); 82 | searchInfoElement.textContent = cursor.matches.length + ' matches'; 83 | 84 | searchInfoElement.appendChild(document.createTextNode(' ')); 85 | 86 | const prevBtn = searchInfoElement.appendChild(document.createElement('a')); 87 | prevBtn.href = 'javascript:'; 88 | prevBtn.addEventListener('click', e => { 89 | e.preventDefault(); 90 | cursor.previous(); 91 | }); 92 | prevBtn.textContent = 'Prev'; 93 | 94 | searchInfoElement.appendChild(document.createTextNode(' ')); 95 | 96 | const nextBtn = searchInfoElement.appendChild(document.createElement('a')); 97 | nextBtn.href = 'javascript:'; 98 | nextBtn.addEventListener('click', e => { 99 | e.preventDefault(); 100 | cursor.next(); 101 | }); 102 | nextBtn.textContent = 'Next'; 103 | } else { 104 | await rootNode.closeNode(); 105 | viewer.openBySearch(null); 106 | searchInfoElement.textContent = ''; 107 | } 108 | }); 109 | 110 | loadStructureData(demoData.simpleData); 111 | 112 | async function loadStructureData(structure, jsData = false) { 113 | if (jsData) { 114 | codeElement.style.display = 'none'; 115 | await showData(structure, jsData); 116 | } else { 117 | const text = JSON.stringify(structure, null, 2); 118 | codeElement.style.display = ''; 119 | codeElement.value = text; 120 | await showData(text, jsData); 121 | } 122 | 123 | showPaths(); 124 | } 125 | 126 | async function showData(data: any, jsData = false) { 127 | const index = 128 | 'showDataIndex' in viewerElement 129 | ? ++viewerElement['showDataIndex'] 130 | : (viewerElement['showDataIndex'] = 0); 131 | if (viewerElement.children.length) { 132 | viewerElement.removeChild(viewerElement.children[0]); 133 | } 134 | if (viewer) { 135 | viewer.destroy(); 136 | } 137 | try { 138 | let _viewer; 139 | if (jsData) { 140 | _viewer = await BigJsonViewerDom.fromObject(data); 141 | } else { 142 | _viewer = await BigJsonViewerDom.fromData(data); 143 | } 144 | if (viewerElement['showDataIndex'] !== index) { 145 | _viewer.destroy(); 146 | return; 147 | } 148 | viewer = _viewer; 149 | rootNode = viewer.getRootElement(); 150 | rootNode.id = 'rootNode'; 151 | viewerElement.appendChild(rootNode); 152 | await rootNode.openAll(1); 153 | setupRootNode(); 154 | } catch (e) { 155 | console.error('BigJsonViewer error', e); 156 | const errEl = document.createElement('div'); 157 | errEl.classList.add('alert', 'alert-danger'); 158 | errEl.appendChild(document.createTextNode(e.toString())); 159 | viewerElement.appendChild(errEl); 160 | } 161 | } 162 | 163 | function setupRootNode() { 164 | const listener = e => { 165 | console.log('event', e.type); 166 | showPaths(); 167 | }; 168 | rootNode.addEventListener('openNode', listener); 169 | rootNode.addEventListener('closeNode', listener); 170 | rootNode.addEventListener('openedNodes', listener); 171 | rootNode.addEventListener('openStub', listener); 172 | rootNode.addEventListener('closeStub', listener); 173 | rootNode.addEventListener('copyPath', e => { 174 | const node = e.target as JsonNodeElement; 175 | copiedElement.value = node.jsonNode.path.join('.'); 176 | }); 177 | } 178 | 179 | function showPaths() { 180 | if (!rootNode || !rootNode.getOpenPaths) { 181 | return; 182 | } 183 | 184 | pathsElement.value = rootNode 185 | .getOpenPaths() 186 | .map(path => path.join('.')) 187 | .join('\n'); 188 | } 189 | 190 | function querySelectorArray(selector: string) { 191 | const list = document.querySelectorAll(selector); 192 | const result = []; 193 | for (let i = 0; i < list.length; i++) { 194 | result.push(list[i]); 195 | } 196 | return result; 197 | } 198 | -------------------------------------------------------------------------------- /docs-src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Big JSON Viewer Demo 6 | 12 | 13 | 18 | 19 | 20 |
24 | 25 | 26 |
27 | 28 |
29 |

Big JSON Viewer Demo

30 | 31 |

32 | Watch large JSON structures in the Browser.
33 | On GitHub 34 |

35 | 36 |

JSON text

37 |
38 | Simple test data | 39 | Large data | 40 | JavaScript data 41 |
42 | 43 |
44 | 48 |
49 | 50 |

Big JSON Viewer output

51 | 52 |
53 | 54 |
Open paths
55 |
56 | 60 |
61 | 62 |
Copied paths
63 |
64 | 69 |
70 | 71 | 74 |
75 | 76 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /docs/default.40d73132.css: -------------------------------------------------------------------------------- 1 | .json-node-type{color:#7f7f7f}.json-node-stub-toggler,.json-node-toggler{text-decoration:none;color:inherit}.json-node-stub-toggler:hover,.json-node-toggler:hover{background:#e2e2e2}.json-node-children,.json-node-root{padding-left:1em}.json-node-header mark{background-color:rgba(199,193,0,.5);padding:0}.json-node-header mark.highlight-active{background-color:rgba(199,103,46,.5)}.json-node-label{color:#184183}.json-node-number .json-node-value{color:#00f}.json-node-string .json-node-value{color:green}.json-node-boolean .json-node-value{color:#95110f}.json-node-null .json-node-value{color:#959310}.json-node-boolean .json-node-type,.json-node-null .json-node-type,.json-node-number .json-node-type,.json-node-string .json-node-type,.json-node-undefined .json-node-type{display:none}.json-node-accessor{position:relative}.json-node-accessor:before{position:absolute;content:"▶";font-size:.6em;line-height:1.6em;right:.5em;transition:transform .1s ease-out}.json-node-open .json-node-accessor:before{transform:rotate(90deg)}.json-node-collapse,.json-node-stub-toggler .json-node-label{color:#7f7f7f}.json-node-collapse{font-size:.8em}@keyframes json-node-children-open{0%{transform:scaleY(0)}to{transform:scaleY(1)}}.json-node-link{display:none;padding-left:.5em;font-size:.8em;color:#7f7f7f;text-decoration:none}.json-node-link:hover{color:#000}.json-node-header:hover .json-node-link{display:inline} 2 | /*# sourceMappingURL=default.40d73132.css.map */ -------------------------------------------------------------------------------- /docs/default.40d73132.css.map: -------------------------------------------------------------------------------- 1 | {"version":3,"sources":["default.css"],"names":[],"mappings":"AAAA,gBACE,aACF,CAEA,2CAEE,oBAAqB,CACrB,aACF,CAEA,uDAEE,kBACF,CAEA,oCAEE,gBACF,CAEA,uBACE,mCAAwC,CACxC,SACF,CAEA,wCACE,oCACF,CAEA,iBACE,aACF,CAEA,mCACE,UACF,CAEA,mCACE,WACF,CAEA,oCACE,aACF,CAEA,iCACE,aACF,CAEA,4KAKE,YACF,CAEA,oBACE,iBACF,CAEA,2BACE,iBAAkB,CAClB,WAAY,CACZ,cAAgB,CAChB,iBAAkB,CAClB,UAAY,CACZ,iCACF,CAEA,2CACE,uBACF,CAQA,6DAEE,aACF,CACA,oBACE,cACF,CAEA,mCACE,GACE,mBACF,CACA,GACE,mBACF,CACF,CAEA,gBACE,YAAa,CACb,iBAAmB,CACnB,cAAgB,CAChB,aAAc,CACd,oBACF,CAEA,sBACE,UACF,CAEA,wCACE,cACF","file":"default.40d73132.css","sourceRoot":"..\\docs-src","sourcesContent":[".json-node-type {\n color: #7f7f7f;\n}\n\n.json-node-toggler,\n.json-node-stub-toggler {\n text-decoration: none;\n color: inherit;\n}\n\n.json-node-toggler:hover,\n.json-node-stub-toggler:hover {\n background: #e2e2e2;\n}\n\n.json-node-children,\n.json-node-root {\n padding-left: 1em;\n}\n\n.json-node-header mark {\n background-color: rgba(199, 193, 0, 0.5);\n padding: 0;\n}\n\n.json-node-header mark.highlight-active {\n background-color: rgba(199, 103, 46, 0.5);\n}\n\n.json-node-label {\n color: #184183;\n}\n\n.json-node-number .json-node-value {\n color: blue;\n}\n\n.json-node-string .json-node-value {\n color: green;\n}\n\n.json-node-boolean .json-node-value {\n color: #95110f;\n}\n\n.json-node-null .json-node-value {\n color: #959310;\n}\n\n.json-node-number .json-node-type,\n.json-node-string .json-node-type,\n.json-node-boolean .json-node-type,\n.json-node-undefined .json-node-type,\n.json-node-null .json-node-type {\n display: none;\n}\n\n.json-node-accessor {\n position: relative;\n}\n\n.json-node-accessor::before {\n position: absolute;\n content: '▶';\n font-size: 0.6em;\n line-height: 1.6em;\n right: 0.5em;\n transition: transform 100ms ease-out;\n}\n\n.json-node-open .json-node-accessor::before {\n transform: rotate(90deg);\n}\n\n.json-node-children {\n /*animation-duration: 500ms;*/\n /*animation-name: json-node-children-open;*/\n /*transform-origin: top;*/\n}\n\n.json-node-stub-toggler .json-node-label,\n.json-node-collapse {\n color: #7f7f7f;\n}\n.json-node-collapse {\n font-size: 0.8em;\n}\n\n@keyframes json-node-children-open {\n from {\n transform: scaleY(0);\n }\n to {\n transform: scaleY(1);\n }\n}\n\n.json-node-link {\n display: none;\n padding-left: 0.5em;\n font-size: 0.8em;\n color: #7f7f7f;\n text-decoration: none;\n}\n\n.json-node-link:hover {\n color: black;\n}\n\n.json-node-header:hover .json-node-link {\n display: inline;\n}\n"]} -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | Big JSON Viewer Demo

Big JSON Viewer Demo

Watch large JSON structures in the Browser.
On GitHub

JSON text

Simple test data | Large data | JavaScript data

Big JSON Viewer output

Open paths
Copied paths
-------------------------------------------------------------------------------- /examples/basic/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | JSON Viewer basic example 6 | 7 | 8 | 9 | 10 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "big-json-viewer", 3 | "version": "0.1.7", 4 | "description": "JavaScript Library to view big JSON structures.", 5 | "main": "./dist/index.js", 6 | "types": "./dist/index.d.ts", 7 | "scripts": { 8 | "clean-build": "del-cli dist && node tools/build-inline-worker.js && tsc && parcel build src/browser-api.ts", 9 | "build": "npm run clean-build && cpx styles/default.css dist", 10 | "prepack": "npm run build", 11 | "build-docs": "del-cli docs && parcel build docs-src/index.html --out-dir docs --public-url ./ && git add docs", 12 | "watch-docs": "parcel docs-src/index.html --out-dir docs/tmp --no-hmr", 13 | "lint": "tslint -c tslint.json -p tsconfig.json", 14 | "test": "jest", 15 | "publish-patch": "npm version patch && npm publish", 16 | "publish-minor": "npm version minor && npm publish", 17 | "precommit": "npm run build-docs && pretty-quick --staged" 18 | }, 19 | "keywords": [ 20 | "JSON", 21 | "JavaScript", 22 | "TypeScript", 23 | "big", 24 | "JSON", 25 | "viewer" 26 | ], 27 | "author": "Dominik Herbst", 28 | "license": "MIT", 29 | "repository": { 30 | "type": "git", 31 | "url": "https://github.com/dhcode/big-json-viewer.git" 32 | }, 33 | "devDependencies": { 34 | "@types/jest": "^22.2.3", 35 | "babel-polyfill": "^6.26.0", 36 | "cpx": "^1.5.0", 37 | "del-cli": "^1.1.0", 38 | "husky": "^0.14.3", 39 | "jasmine-core": "^2.99.1", 40 | "jest": "^22.4.4", 41 | "parcel-bundler": "^1.12.3", 42 | "prettier": "^1.16.4", 43 | "pretty-quick": "^1.10.0", 44 | "ts-jest": "^22.4.6", 45 | "tslint": "^5.14.0", 46 | "typescript": "^3.3.4000" 47 | }, 48 | "jest": { 49 | "collectCoverage": true, 50 | "coverageDirectory": "./coverage", 51 | "testURL": "http://localhost", 52 | "transform": { 53 | "^.+\\.tsx?$": "ts-jest" 54 | }, 55 | "testRegex": "(/__tests__/.*|(\\.|/)(test|spec))\\.(jsx?|tsx?)$", 56 | "moduleFileExtensions": [ 57 | "ts", 58 | "tsx", 59 | "js", 60 | "jsx", 61 | "json", 62 | "node" 63 | ] 64 | }, 65 | "prettier": { 66 | "singleQuote": true 67 | }, 68 | "husky": { 69 | "hooks": { 70 | "pre-commit": "npm run precommit" 71 | } 72 | }, 73 | "dependencies": {} 74 | } 75 | -------------------------------------------------------------------------------- /src/big-json-viewer-dom.spec.ts: -------------------------------------------------------------------------------- 1 | import { BigJsonViewerDom, JsonNodeElement } from './big-json-viewer-dom'; 2 | import { JsonNodesStubElement } from './model/big-json-viewer.model'; 3 | 4 | const wait = time => new Promise(resolve => setTimeout(() => resolve(), time)); 5 | 6 | describe('Big JSON Viewer', function() { 7 | it('should instantiate', async function() { 8 | const viewer = await BigJsonViewerDom.fromData('{}'); 9 | const root = viewer.getRootElement(); 10 | expect(root).toBeTruthy(); 11 | viewer.destroy(); 12 | }); 13 | 14 | it('should create DOM from simple object', async function() { 15 | const viewer = await BigJsonViewerDom.fromData('{"a":5, "b":true}'); 16 | const root = viewer.getRootElement(); 17 | expect(root).toBeTruthy(); 18 | await root.openAll(); 19 | expect(root.getOpenPaths()).toEqual([[]]); 20 | 21 | expect(root.childrenElement).toBeTruthy(); 22 | expect(root.childrenElement.children.length).toEqual(2); 23 | viewer.destroy(); 24 | }); 25 | 26 | it('should create DOM from JavaScript simple object', async function() { 27 | const viewer = await BigJsonViewerDom.fromObject({ a: 5, b: true }); 28 | const root = viewer.getRootElement(); 29 | expect(root).toBeTruthy(); 30 | await root.openAll(); 31 | expect(root.getOpenPaths()).toEqual([[]]); 32 | 33 | expect(root.childrenElement).toBeTruthy(); 34 | expect(root.childrenElement.children.length).toEqual(2); 35 | viewer.destroy(); 36 | }); 37 | 38 | it('should create DOM from more complex object', async function() { 39 | const data = 40 | '{"hello": "hello world, is a great world","test": [0,"old world",{"worldgame": true}]}'; 41 | const viewer = await BigJsonViewerDom.fromData(data); 42 | const root: JsonNodeElement = viewer.getRootElement(); 43 | expect(root).toBeTruthy(); 44 | await root.openAll(); 45 | expect(root.getOpenPaths()).toEqual([['test', '2']]); 46 | 47 | expect(root.childrenElement).toBeTruthy(); 48 | expect(root.childrenElement.children.length).toEqual(2); 49 | 50 | await root.closeNode(); 51 | 52 | expect(root.isNodeOpen()).toBeFalsy(); 53 | 54 | viewer.destroy(); 55 | }); 56 | 57 | it('should open by toggle', async function() { 58 | const data = 59 | '{"hello": "hello world, is a great world","test": [0,"old world",{"worldgame": true}]}'; 60 | const viewer = await BigJsonViewerDom.fromData(data); 61 | const root: JsonNodeElement = viewer.getRootElement(); 62 | expect(root).toBeTruthy(); 63 | 64 | expect(root.isNodeOpen()).toBeFalsy(); 65 | expect(await root.toggleNode()).toBeTruthy(); 66 | expect(root.isNodeOpen()).toBeTruthy(); 67 | 68 | expect(root.childrenElement).toBeTruthy(); 69 | expect(root.childrenElement.children.length).toEqual(2); 70 | 71 | expect(await root.toggleNode()).toBeTruthy(); 72 | expect(root.isNodeOpen()).toBeFalsy(); 73 | 74 | expect(root.childrenElement).toBeNull(); 75 | 76 | viewer.destroy(); 77 | }); 78 | 79 | it('should open by click', async function() { 80 | const data = 81 | '{"hello": "hello world, is a great world","test": [0,"old world",{"worldgame": true}]}'; 82 | const viewer = await BigJsonViewerDom.fromData(data); 83 | const root: JsonNodeElement = viewer.getRootElement(); 84 | let openCalls = 0; 85 | let closeCalls = 0; 86 | root.addEventListener('openNode', e => { 87 | openCalls++; 88 | }); 89 | root.addEventListener('closeNode', e => { 90 | closeCalls++; 91 | }); 92 | 93 | expect(root).toBeTruthy(); 94 | 95 | expect(root.isNodeOpen()).toBeFalsy(); 96 | root.querySelector('a').dispatchEvent(new MouseEvent('click')); 97 | await wait(1); 98 | expect(root.isNodeOpen()).toBeTruthy(); 99 | expect(openCalls).toBe(1); 100 | 101 | expect(root.childrenElement).toBeTruthy(); 102 | expect(root.childrenElement.children.length).toEqual(2); 103 | 104 | root.querySelector('a').dispatchEvent(new MouseEvent('click')); 105 | await wait(1); 106 | expect(root.isNodeOpen()).toBeFalsy(); 107 | expect(closeCalls).toBe(1); 108 | 109 | expect(root.childrenElement).toBeNull(); 110 | 111 | viewer.destroy(); 112 | }); 113 | 114 | it('should open by search', async function() { 115 | const data = 116 | '{"hello": "hello world, is a great world","test": [0,"old world",{"worldgame": true}]}'; 117 | const viewer = await BigJsonViewerDom.fromData(data); 118 | const root: JsonNodeElement = viewer.getRootElement(); 119 | expect(root).toBeTruthy(); 120 | 121 | const cursor = await viewer.openBySearch(/world/); 122 | expect(cursor).toBeTruthy(); 123 | expect(cursor.matches.length).toEqual(4); 124 | expect(root.getOpenPaths()).toEqual([[]]); 125 | 126 | await cursor.next(); 127 | expect(root.getOpenPaths()).toEqual([[]]); 128 | 129 | await cursor.next(); 130 | expect(root.getOpenPaths()).toEqual([['test']]); 131 | 132 | expect(await viewer.openBySearch(null)).toBeNull(); 133 | expect(root.isNodeOpen()).toBeFalsy(); 134 | 135 | const cursor2 = await viewer.openBySearch(/old/); 136 | expect(cursor2).toBeTruthy(); 137 | expect(cursor2.matches.length).toEqual(1); 138 | expect(root.getOpenPaths()).toEqual([['test']]); 139 | 140 | const cursor3 = await viewer.openBySearch(/notExisting/); 141 | expect(cursor3).toBeTruthy(); 142 | expect(cursor3.matches.length).toEqual(0); 143 | 144 | viewer.destroy(); 145 | }); 146 | 147 | it('should have working pagination', async function() { 148 | const data = new Array(120); 149 | data.fill(true); 150 | 151 | // default limit is 50 152 | const viewer = await BigJsonViewerDom.fromData(JSON.stringify(data), { 153 | collapseSameValue: Infinity 154 | }); 155 | const root: JsonNodeElement = viewer.getRootElement(); 156 | expect(root).toBeTruthy(); 157 | 158 | expect(root.isNodeOpen()).toBeFalsy(); 159 | expect(root.childrenElement).toBeUndefined(); 160 | 161 | expect(await root.openNode()).toBeTruthy(); 162 | expect(root.childrenElement).toBeTruthy(); 163 | expect(root.childrenElement.children.length).toEqual(3); 164 | expect(root.getOpenPaths()).toEqual([[]]); 165 | 166 | let stub = root.childrenElement.children[0] as JsonNodesStubElement; 167 | expect(stub.isNodeOpen()).toBeFalsy(); 168 | 169 | expect(await stub.openNode()).toBeTruthy(); 170 | expect(stub.childrenElement).toBeTruthy(); 171 | expect(stub.childrenElement.children.length).toEqual(50); 172 | expect(root.getOpenPaths()).toEqual([['0']]); 173 | 174 | expect(await stub.closeNode()).toBeTruthy(); 175 | expect(stub.childrenElement).toBeNull(); 176 | expect(root.getOpenPaths()).toEqual([[]]); 177 | 178 | stub = root.childrenElement.children[2] as JsonNodesStubElement; 179 | expect(stub.isNodeOpen()).toBeFalsy(); 180 | stub.querySelector('a').dispatchEvent(new MouseEvent('click')); 181 | await wait(1); 182 | expect(stub.childrenElement).toBeTruthy(); 183 | expect(stub.childrenElement.children.length).toEqual(20); 184 | expect(root.getOpenPaths()).toEqual([['100']]); 185 | 186 | await root.closeNode(); 187 | expect(root.getOpenPaths()).toEqual([]); 188 | 189 | const openedNode = await root.openPath(['61']); 190 | expect(openedNode).toBeTruthy(); 191 | expect(openedNode.jsonNode.type).toEqual('boolean'); 192 | expect(openedNode.jsonNode.path).toEqual(['61']); 193 | expect(root.getOpenPaths()).toEqual([['50']]); 194 | 195 | stub = root.childrenElement.children[0] as JsonNodesStubElement; 196 | expect(stub.isNodeOpen()).toBeFalsy(); 197 | 198 | stub = root.childrenElement.children[1] as JsonNodesStubElement; 199 | expect(stub.isNodeOpen()).toBeTruthy(); 200 | 201 | await root.closeNode(); 202 | expect(root.getOpenPaths()).toEqual([]); 203 | await root.openAll(2, 'first'); 204 | expect(root.getOpenPaths()).toEqual([['0']]); 205 | 206 | viewer.destroy(); 207 | }); 208 | 209 | it('should collapse same values in arrays', async function() { 210 | const data = new Array(10); 211 | data.fill(true); 212 | 213 | const viewer = await BigJsonViewerDom.fromObject(data); 214 | const root: JsonNodeElement = viewer.getRootElement(); 215 | expect(root).toBeTruthy(); 216 | 217 | await root.openNode(); 218 | 219 | expect(root.childrenElement.children.length).toBe(5 + 1 + 1); 220 | 221 | viewer.destroy(); 222 | }); 223 | 224 | it('should not collapse same values in objects', async function() { 225 | const data = {}; 226 | for (let i = 0; i < 10; i++) { 227 | data['node' + i] = true; 228 | } 229 | 230 | const viewer = await BigJsonViewerDom.fromObject(data); 231 | const root: JsonNodeElement = viewer.getRootElement(); 232 | expect(root).toBeTruthy(); 233 | 234 | await root.openNode(); 235 | 236 | expect(root.childrenElement.children.length).toBe(10); 237 | 238 | viewer.destroy(); 239 | }); 240 | 241 | it('should collapse same values in mixed arrays', async function() { 242 | const data = new Array(20); 243 | data.fill(true, 0, 9); 244 | data.fill(false, 9, 12); 245 | data.fill(true, 12, 20); 246 | 247 | const viewer = await BigJsonViewerDom.fromObject(data); 248 | const root: JsonNodeElement = viewer.getRootElement(); 249 | expect(root).toBeTruthy(); 250 | 251 | await root.openNode(); 252 | 253 | expect(root.childrenElement.children.length).toBe( 254 | 5 + 1 + 1 + (12 - 9) + 5 + 1 + 1 255 | ); 256 | 257 | viewer.destroy(); 258 | }); 259 | }); 260 | -------------------------------------------------------------------------------- /src/big-json-viewer-dom.ts: -------------------------------------------------------------------------------- 1 | import { 2 | BigJsonViewerEvent, 3 | BigJsonViewerNode, 4 | BigJsonViewerOptions, 5 | JsonNodesStubElement, 6 | PaginatedOption, 7 | TreeSearchAreaOption, 8 | TreeSearchCursor, 9 | TreeSearchMatch 10 | } from './model/big-json-viewer.model'; 11 | import { 12 | WorkerClient, 13 | WorkerClientApi, 14 | WorkerClientMock 15 | } from './helpers/worker-client'; 16 | import { forEachMatchFromString } from './parser/json-node-search'; 17 | import { initWorker } from './worker/big-json-viewer.worker.inline'; 18 | 19 | declare var require: any; 20 | 21 | export interface JsonNodeElement extends JsonNodesStubElement { 22 | jsonNode: BigJsonViewerNode; 23 | 24 | /** 25 | * Opens the given path and returns the JsonNodeElement if the path was found. 26 | */ 27 | openPath(path: string[]): Promise; 28 | 29 | /** 30 | * Opens all nodes with limits. 31 | * maxDepth is Infinity by default. 32 | * paginated is first by default. This opens only the first page. 33 | * all, would open all pages. 34 | * none would open no pages and just show the stubs. 35 | * Returns the number of opened nodes. 36 | */ 37 | openAll(maxDepth?: number, paginated?: PaginatedOption): Promise; 38 | 39 | /** 40 | * Get a list of all opened paths 41 | * withsStubs is true by default, it makes sure, that opened stubs are represented 42 | */ 43 | getOpenPaths(withStubs?: boolean): string[][]; 44 | 45 | openNode(dispatchEvent?: boolean): Promise; 46 | 47 | closeNode(dispatchEvent?: boolean): Promise; 48 | 49 | toggleNode(dispatchEvent?: boolean): Promise; 50 | } 51 | 52 | export class BigJsonViewerDom { 53 | private workerClient: WorkerClientApi; 54 | 55 | private options: BigJsonViewerOptions = { 56 | objectNodesLimit: 50, 57 | arrayNodesLimit: 50, 58 | labelAsPath: false, 59 | linkLabelCopyPath: 'Copy path', 60 | linkLabelExpandAll: 'Expand all', 61 | workerPath: null, 62 | collapseSameValue: 5 63 | }; 64 | 65 | private currentPattern: RegExp; 66 | private currentArea: TreeSearchAreaOption = 'all'; 67 | private currentMark = null; 68 | 69 | private rootElement: JsonNodeElement; 70 | 71 | private rootNode: BigJsonViewerNode; 72 | 73 | /** 74 | * Initialized the viewer with JSON encoded data 75 | */ 76 | public static async fromData( 77 | data: ArrayBuffer | string, 78 | options?: BigJsonViewerOptions 79 | ): Promise { 80 | const viewer = new BigJsonViewerDom(options); 81 | await viewer.setData(data); 82 | return viewer; 83 | } 84 | 85 | /** 86 | * Initializes the viewer with a JavaScript object 87 | */ 88 | public static async fromObject( 89 | data: string | object | null | number | boolean, 90 | options?: BigJsonViewerOptions 91 | ): Promise { 92 | const viewer = new BigJsonViewerDom(options); 93 | await viewer.setObject(data); 94 | return viewer; 95 | } 96 | 97 | protected constructor(options?: BigJsonViewerOptions) { 98 | if (options) { 99 | Object.assign(this.options, options); 100 | } 101 | } 102 | 103 | protected async getWorkerClient() { 104 | if (!this.workerClient) { 105 | try { 106 | const worker = this.options.workerPath 107 | ? new Worker(this.options.workerPath) 108 | : initWorker(); 109 | const client = new WorkerClient(worker); 110 | await client.initWorker(); 111 | this.workerClient = client; 112 | } catch (e) { 113 | console.warn( 114 | 'Could not instantiate Worker ' + 115 | this.options.workerPath + 116 | ', using mock', 117 | e 118 | ); 119 | const serviceModule = require('./big-json-viewer-service'); 120 | const service = new serviceModule.BigJsonViewerService(); 121 | this.workerClient = new WorkerClientMock(service); 122 | } 123 | } 124 | return this.workerClient; 125 | } 126 | 127 | protected async setData( 128 | data: ArrayBuffer | string 129 | ): Promise { 130 | const client = await this.getWorkerClient(); 131 | this.rootNode = await client.call('initWithData', data); 132 | return this.rootNode; 133 | } 134 | 135 | protected async setObject(data: any): Promise { 136 | const client = await this.getWorkerClient(); 137 | this.rootNode = await client.call('initWithJs', data); 138 | return this.rootNode; 139 | } 140 | 141 | protected async getChildNodes( 142 | path: string[], 143 | start: number, 144 | limit: number 145 | ): Promise { 146 | const client = await this.getWorkerClient(); 147 | return client.call('getNodes', path, start, limit); 148 | } 149 | 150 | protected async getSearchMatches( 151 | pattern: RegExp, 152 | searchArea: TreeSearchAreaOption 153 | ): Promise { 154 | const client = await this.getWorkerClient(); 155 | return await client.call('search', pattern, searchArea); 156 | } 157 | 158 | protected async getKeyIndex(path: string[], key: string): Promise { 159 | const client = await this.getWorkerClient(); 160 | return await client.call('getKeyIndex', path, key); 161 | } 162 | 163 | /** 164 | * Destroys the viewer and frees resources like the worker 165 | */ 166 | public destroy() { 167 | if (this.workerClient) { 168 | this.workerClient.destroy(); 169 | this.workerClient = null; 170 | } 171 | this.rootElement = null; 172 | this.currentPattern = null; 173 | } 174 | 175 | public getRootElement(): JsonNodeElement { 176 | if (this.rootElement) { 177 | return this.rootElement; 178 | } 179 | if (this.rootNode) { 180 | const nodeElement = this.getNodeElement(this.rootNode); 181 | nodeElement.classList.add('json-node-root'); 182 | this.rootElement = nodeElement; 183 | return nodeElement; 184 | } 185 | return null; 186 | } 187 | 188 | protected getNodeElement(node: BigJsonViewerNode): JsonNodeElement { 189 | const element = document.createElement('div') as JsonNodeElement; 190 | element.classList.add('json-node'); 191 | 192 | element.jsonNode = node; 193 | 194 | const header = this.getNodeHeader(node); 195 | element.headerElement = element.appendChild(header); 196 | 197 | this.attachInteractivity(element, node); 198 | 199 | return element; 200 | } 201 | 202 | protected attachInteractivity( 203 | nodeElement: JsonNodeElement, 204 | node: BigJsonViewerNode 205 | ) { 206 | nodeElement.isNodeOpen = (): boolean => { 207 | if (this.isOpenableNode(node)) { 208 | return nodeElement.headerElement.classList.contains('json-node-open'); 209 | } 210 | return false; 211 | }; 212 | nodeElement.openNode = async (dispatchEvent = false) => { 213 | if (this.isOpenableNode(node)) { 214 | return await this.openNode(nodeElement, dispatchEvent); 215 | } 216 | return false; 217 | }; 218 | nodeElement.closeNode = async (dispatchEvent = false) => { 219 | if (this.isOpenableNode(node)) { 220 | return this.closeNode(nodeElement, dispatchEvent); 221 | } 222 | return false; 223 | }; 224 | nodeElement.toggleNode = async (dispatchEvent = false) => { 225 | if (nodeElement.isNodeOpen()) { 226 | return await nodeElement.closeNode(dispatchEvent); 227 | } else { 228 | return await nodeElement.openNode(dispatchEvent); 229 | } 230 | }; 231 | 232 | nodeElement.openPath = async (path: string[]): Promise => { 233 | if (this.isOpenableNode(node)) { 234 | return await this.openPath(nodeElement, path, true); 235 | } 236 | return null; 237 | }; 238 | 239 | nodeElement.openAll = async ( 240 | maxDepth = Infinity, 241 | paginated = 'first' 242 | ): Promise => { 243 | if (this.isOpenableNode(node)) { 244 | return await this.openAll(nodeElement, maxDepth, paginated); 245 | } 246 | return 0; 247 | }; 248 | 249 | nodeElement.getOpenPaths = (withStubs = true): string[][] => { 250 | if (this.isOpenableNode(node)) { 251 | return this.getOpenPaths(nodeElement, withStubs); 252 | } 253 | return []; 254 | }; 255 | } 256 | 257 | protected attachClickToggleListener(anchor: HTMLAnchorElement) { 258 | anchor.addEventListener('click', e => { 259 | e.preventDefault(); 260 | const nodeElement = this.findNodeElement(anchor); 261 | if (nodeElement) { 262 | nodeElement.toggleNode(true); 263 | } 264 | }); 265 | } 266 | 267 | protected isOpenableNode(node: BigJsonViewerNode) { 268 | return (node.type === 'array' || node.type === 'object') && node.length; 269 | } 270 | 271 | protected closeNode(nodeElement: JsonNodeElement, dispatchEvent = false) { 272 | if (!nodeElement.isNodeOpen()) { 273 | return false; 274 | } 275 | if (nodeElement.childrenElement) { 276 | nodeElement.headerElement.classList.remove('json-node-open'); 277 | nodeElement.removeChild(nodeElement.childrenElement); 278 | nodeElement.childrenElement = null; 279 | if (dispatchEvent) { 280 | this.dispatchNodeEvent('closeNode', nodeElement); 281 | } 282 | return true; 283 | } 284 | return false; 285 | } 286 | 287 | protected refreshHeaders(nodeElement: JsonNodeElement) { 288 | const header = this.getNodeHeader(nodeElement.jsonNode); 289 | if (nodeElement.isNodeOpen()) { 290 | header.classList.add('json-node-open'); 291 | } 292 | nodeElement.headerElement.parentElement.replaceChild( 293 | header, 294 | nodeElement.headerElement 295 | ); 296 | nodeElement.headerElement = header; 297 | 298 | if (nodeElement.childrenElement) { 299 | this.getVisibleChildren(nodeElement.childrenElement.children).forEach( 300 | element => this.refreshHeaders(element) 301 | ); 302 | } 303 | } 304 | 305 | protected getHighlightedText( 306 | text: string, 307 | pattern: RegExp 308 | ): DocumentFragment { 309 | const fragment = document.createDocumentFragment(); 310 | if (!pattern) { 311 | fragment.appendChild(document.createTextNode(text)); 312 | return fragment; 313 | } 314 | let lastIndex = 0; 315 | forEachMatchFromString(pattern, text, (index, length) => { 316 | if (lastIndex < index) { 317 | fragment.appendChild( 318 | document.createTextNode(text.substring(lastIndex, index)) 319 | ); 320 | } 321 | const mark = document.createElement('mark'); 322 | mark.appendChild(document.createTextNode(text.substr(index, length))); 323 | fragment.appendChild(mark); 324 | lastIndex = index + length; 325 | }); 326 | if (lastIndex !== text.length) { 327 | fragment.appendChild( 328 | document.createTextNode(text.substring(lastIndex, text.length)) 329 | ); 330 | } 331 | return fragment; 332 | } 333 | 334 | /** 335 | * Opens the tree nodes based on a pattern 336 | * openLimit is 1 by default, you can provide Infinity for all 337 | * searchArea is 'all' by default 338 | */ 339 | public async openBySearch( 340 | pattern: RegExp, 341 | openLimit = 1, 342 | searchArea: TreeSearchAreaOption = 'all' 343 | ): Promise { 344 | const nodeElement = this.rootElement; 345 | this.currentPattern = pattern; 346 | this.currentArea = searchArea; 347 | if (!pattern) { 348 | this.closeNode(nodeElement, true); 349 | return null; 350 | } 351 | 352 | this.refreshHeaders(nodeElement); 353 | 354 | const viewer = this; 355 | const matches = await this.getSearchMatches(pattern, searchArea); 356 | const cursor: TreeSearchCursor = { 357 | matches: matches, 358 | index: 0, 359 | async navigateTo(index: number) { 360 | this.index = index; 361 | const match = this.matches[index]; 362 | if (!match) { 363 | console.warn( 364 | 'searchIndex does not exist on ' + this.matches.length, 365 | index 366 | ); 367 | return; 368 | } 369 | const openedElement = await viewer.openSearchMatch(nodeElement, match); 370 | if (openedElement) { 371 | if (openedElement.scrollIntoView) { 372 | openedElement.scrollIntoView({ block: 'center' }); 373 | } 374 | if (viewer.currentMark) { 375 | viewer.currentMark.classList.remove('highlight-active'); 376 | } 377 | viewer.currentMark = viewer.findMarkForMatch(openedElement, match); 378 | if (viewer.currentMark) { 379 | viewer.currentMark.classList.add('highlight-active'); 380 | } 381 | return true; 382 | } 383 | return false; 384 | }, 385 | next() { 386 | return this.navigateTo( 387 | this.index + 1 >= this.matches.length ? 0 : this.index + 1 388 | ); 389 | }, 390 | previous() { 391 | return this.navigateTo( 392 | this.index - 1 < 0 ? this.matches.length : this.index - 1 393 | ); 394 | } 395 | }; 396 | 397 | const length = Math.min(matches.length, openLimit); 398 | for (let i = 0; i < length && i < openLimit; i++) { 399 | await this.openSearchMatch(nodeElement, matches[i]); 400 | } 401 | 402 | this.dispatchNodeEvent('openedNodes', nodeElement); 403 | 404 | if (matches.length) { 405 | await cursor.navigateTo(0); 406 | } 407 | 408 | return cursor; 409 | } 410 | 411 | protected findMarkForMatch( 412 | nodeElement: JsonNodeElement, 413 | match: TreeSearchMatch 414 | ): HTMLElement { 415 | let children = null, 416 | expectIndex = 0; 417 | if (match.key !== undefined) { 418 | const label = nodeElement.headerElement.querySelector('.json-node-label'); 419 | if (label) { 420 | children = label.childNodes; 421 | expectIndex = match.key; 422 | } 423 | } 424 | 425 | if (match.value !== undefined) { 426 | const value = nodeElement.headerElement.querySelector('.json-node-value'); 427 | if (value) { 428 | children = value.childNodes; 429 | expectIndex = match.value; 430 | } 431 | } 432 | 433 | if (children) { 434 | let index = nodeElement.jsonNode.type === 'string' ? -1 : 0; 435 | for (let i = 0; i < children.length; i++) { 436 | const cn = children[i]; 437 | if (cn.nodeType === Node.TEXT_NODE) { 438 | index += cn.textContent.length; 439 | } 440 | if ( 441 | cn.nodeType === Node.ELEMENT_NODE && 442 | (cn as HTMLElement).tagName === 'MARK' && 443 | expectIndex === index 444 | ) { 445 | return cn as HTMLElement; 446 | } 447 | } 448 | } 449 | 450 | return null; 451 | } 452 | 453 | protected async openSearchMatch( 454 | nodeElement: JsonNodeElement, 455 | match: TreeSearchMatch 456 | ): Promise { 457 | if (match.key !== undefined && match.path.length) { 458 | if (match.path.length) { 459 | const matchNodeElementParent = await this.openPath( 460 | nodeElement, 461 | match.path.slice(0, -1) 462 | ); // open the parent 463 | if (matchNodeElementParent) { 464 | return await this.openKey( 465 | matchNodeElementParent, 466 | match.path[match.path.length - 1] 467 | ); // ensure the key is visible 468 | } 469 | } 470 | } else if (match.value !== undefined) { 471 | return await this.openPath(nodeElement, match.path); 472 | } 473 | return null; 474 | } 475 | 476 | protected getOpenPaths(nodeElement: JsonNodeElement, withSubs): string[][] { 477 | const result: string[][] = []; 478 | if (!nodeElement.isNodeOpen()) { 479 | return result; 480 | } 481 | 482 | const children = nodeElement.childrenElement.children; 483 | const nodeElements = this.getVisibleChildren(children); 484 | for (let i = 0; i < nodeElements.length; i++) { 485 | const element = nodeElements[i]; 486 | if (element.isNodeOpen()) { 487 | result.push(...this.getOpenPaths(element, withSubs)); 488 | } 489 | } 490 | 491 | const limit = this.getPaginationLimit(nodeElement.jsonNode); 492 | // find open stubs 493 | if (!result.length && limit) { 494 | for (let i = 0; i < children.length; i++) { 495 | const child = children[i] as JsonNodesStubElement; 496 | if ( 497 | child.isNodeOpen() && 498 | child.childrenElement && 499 | child.childrenElement.children.length 500 | ) { 501 | const first = child.childrenElement.children[0] as JsonNodeElement; 502 | if (first.jsonNode) { 503 | result.push(first.jsonNode.path); 504 | } 505 | } 506 | } 507 | } 508 | if (!result.length) { 509 | result.push(nodeElement.jsonNode.path); 510 | } 511 | return result; 512 | } 513 | 514 | protected async openNode( 515 | nodeElement: JsonNodeElement, 516 | dispatchEvent = false 517 | ): Promise { 518 | if (nodeElement.isNodeOpen()) { 519 | return false; 520 | } 521 | nodeElement.headerElement.classList.add('json-node-open'); 522 | 523 | const children = await this.getPaginatedNodeChildren(nodeElement); 524 | 525 | nodeElement.childrenElement = nodeElement.appendChild(children); 526 | 527 | if (dispatchEvent) { 528 | this.dispatchNodeEvent('openNode', nodeElement); 529 | } 530 | return true; 531 | } 532 | 533 | protected dispatchNodeEvent( 534 | type: BigJsonViewerEvent, 535 | nodeElement: JsonNodesStubElement 536 | ) { 537 | let event: Event; 538 | if (document.createEvent) { 539 | event = document.createEvent('Event'); 540 | event.initEvent(type, true, false); 541 | } else { 542 | event = new Event(type, { 543 | bubbles: true, 544 | cancelable: false 545 | }); 546 | } 547 | nodeElement.dispatchEvent(event); 548 | } 549 | 550 | protected async openKey( 551 | nodeElement: JsonNodeElement, 552 | key: string 553 | ): Promise { 554 | const node = nodeElement.jsonNode; 555 | let children: HTMLCollection = null; 556 | let index = -1; 557 | if (node.type === 'object') { 558 | index = await this.getKeyIndex(node.path, key); 559 | if (index === -1) { 560 | return null; 561 | } 562 | 563 | await nodeElement.openNode(); 564 | 565 | // find correct stub in pagination 566 | if (node.length > this.options.objectNodesLimit) { 567 | const stubIndex = Math.floor(index / this.options.objectNodesLimit); 568 | const stub = nodeElement.childrenElement.children[ 569 | stubIndex 570 | ] as JsonNodesStubElement; 571 | if (stub) { 572 | await stub.openNode(); 573 | index -= stubIndex * this.options.objectNodesLimit; 574 | children = stub.childrenElement.children; 575 | } 576 | } else { 577 | children = nodeElement.childrenElement.children; 578 | } 579 | } 580 | if (node.type === 'array') { 581 | index = parseInt(key); 582 | if (isNaN(index) || index >= node.length || index < 0) { 583 | return null; 584 | } 585 | 586 | await nodeElement.openNode(); 587 | // find correct stub in pagination 588 | if (node.length > this.options.arrayNodesLimit) { 589 | const stubIndex = Math.floor(index / this.options.arrayNodesLimit); 590 | const stub = nodeElement.childrenElement.children[ 591 | stubIndex 592 | ] as JsonNodesStubElement; 593 | if (stub) { 594 | await stub.openNode(); 595 | index -= stubIndex * this.options.arrayNodesLimit; 596 | children = stub.childrenElement.children; 597 | } 598 | } else { 599 | children = nodeElement.childrenElement.children; 600 | } 601 | } 602 | if (children && index >= 0 && index < children.length) { 603 | const childNodeElement = children[index] as JsonNodeElement; 604 | if (!childNodeElement.jsonNode) { 605 | return null; 606 | } 607 | return childNodeElement; 608 | } 609 | return null; 610 | } 611 | 612 | protected async openPath( 613 | nodeElement: JsonNodeElement, 614 | path: string[], 615 | dispatchEvent = false 616 | ): Promise { 617 | if (!path.length) { 618 | await this.openNode(nodeElement, dispatchEvent); 619 | return nodeElement; 620 | } 621 | 622 | let element = nodeElement; 623 | for (let i = 0; i < path.length; i++) { 624 | if (!element) { 625 | return null; 626 | } 627 | element = await this.openKey(element, path[i]); 628 | if (element) { 629 | await element.openNode(); 630 | } 631 | } 632 | if (dispatchEvent) { 633 | this.dispatchNodeEvent('openedNodes', nodeElement); 634 | } 635 | return element; 636 | } 637 | 638 | protected async openAll( 639 | nodeElement: JsonNodeElement, 640 | maxDepth: number, 641 | paginated: PaginatedOption, 642 | dispatchEvent = false 643 | ): Promise { 644 | await nodeElement.openNode(); 645 | let opened = 1; 646 | if (maxDepth <= 1 || !nodeElement.childrenElement) { 647 | return opened; 648 | } 649 | const newMaxDepth = maxDepth === Infinity ? Infinity : maxDepth - 1; 650 | 651 | opened += await this.openAllChildren( 652 | nodeElement.childrenElement.children, 653 | newMaxDepth, 654 | paginated 655 | ); 656 | 657 | if (dispatchEvent) { 658 | this.dispatchNodeEvent('openedNodes', nodeElement); 659 | } 660 | 661 | return opened; 662 | } 663 | 664 | protected async openAllChildren( 665 | children: HTMLCollection, 666 | maxDepth: number, 667 | paginated: PaginatedOption 668 | ): Promise { 669 | let opened = 0; 670 | for (let i = 0; i < children.length; i++) { 671 | const child = children[i] as JsonNodeElement; 672 | if (child.jsonNode) { 673 | // is a node 674 | opened += await child.openAll(maxDepth, paginated); 675 | } else if (child.openNode) { 676 | // is a stub 677 | if (paginated === 'none') { 678 | return opened; 679 | } 680 | await child.openNode(); 681 | if (child.childrenElement) { 682 | opened += await this.openAllChildren( 683 | child.childrenElement.children, 684 | maxDepth, 685 | paginated 686 | ); 687 | } 688 | if (paginated === 'first') { 689 | return opened; 690 | } 691 | } 692 | } 693 | return opened; 694 | } 695 | 696 | /** 697 | * Returns the pagination limit, if the node should have 698 | */ 699 | protected getPaginationLimit(node: BigJsonViewerNode): number { 700 | if (node.type === 'array' && node.length > this.options.arrayNodesLimit) { 701 | return this.options.arrayNodesLimit; 702 | } 703 | if (node.type === 'object' && node.length > this.options.objectNodesLimit) { 704 | return this.options.objectNodesLimit; 705 | } 706 | return 0; 707 | } 708 | 709 | protected getVisibleChildren(children: HTMLCollection): JsonNodeElement[] { 710 | const result: JsonNodeElement[] = []; 711 | for (let i = 0; i < children.length; i++) { 712 | const child = children[i] as JsonNodeElement; 713 | if (child.jsonNode) { 714 | // is a node 715 | result.push(child); 716 | } else if ( 717 | child.openNode && 718 | child.isNodeOpen() && 719 | child.childrenElement 720 | ) { 721 | // is a stub 722 | result.push(...this.getVisibleChildren(child.childrenElement.children)); 723 | } 724 | } 725 | return result; 726 | } 727 | 728 | protected async getPaginatedNodeChildren( 729 | nodeElement: JsonNodeElement 730 | ): Promise { 731 | const node = nodeElement.jsonNode; 732 | const element = document.createElement('div'); 733 | element.classList.add('json-node-children'); 734 | 735 | const limit = this.getPaginationLimit(node); 736 | if (limit) { 737 | for (let start = 0; start < node.length; start += limit) { 738 | element.appendChild(this.getPaginationStub(node, start, limit)); 739 | } 740 | } else { 741 | const nodes = await this.getChildNodes(node.path, 0, limit); 742 | this.addChildNodes( 743 | nodes, 744 | element, 745 | node.type === 'array' ? this.options.collapseSameValue : Infinity 746 | ); 747 | } 748 | return element; 749 | } 750 | 751 | protected getPaginationStub( 752 | node: BigJsonViewerNode, 753 | start: number, 754 | limit: number 755 | ): JsonNodesStubElement { 756 | const stubElement = document.createElement('div') as JsonNodesStubElement; 757 | stubElement.classList.add('json-node-stub'); 758 | 759 | const anchor = document.createElement('a'); 760 | anchor.href = 'javascript:'; 761 | anchor.classList.add('json-node-stub-toggler'); 762 | 763 | stubElement.headerElement = anchor; 764 | 765 | this.generateAccessor(anchor); 766 | 767 | const end = Math.min(node.length, start + limit) - 1; 768 | const label = document.createElement('span'); 769 | label.classList.add('json-node-label'); 770 | label.appendChild( 771 | document.createTextNode('[' + start + ' ... ' + end + ']') 772 | ); 773 | anchor.appendChild(label); 774 | 775 | stubElement.appendChild(anchor); 776 | 777 | anchor.addEventListener('click', async e => { 778 | e.preventDefault(); 779 | if (stubElement.isNodeOpen()) { 780 | this.closePaginationStub(stubElement, true); 781 | } else { 782 | this.openPaginationStub( 783 | stubElement, 784 | node, 785 | await this.getChildNodes(node.path, start, limit), 786 | true 787 | ); 788 | } 789 | }); 790 | 791 | stubElement.isNodeOpen = () => { 792 | return anchor.classList.contains('json-node-open'); 793 | }; 794 | 795 | stubElement.openNode = async () => { 796 | if (!stubElement.isNodeOpen()) { 797 | await this.openPaginationStub( 798 | stubElement, 799 | node, 800 | await this.getChildNodes(node.path, start, limit) 801 | ); 802 | return true; 803 | } 804 | return false; 805 | }; 806 | 807 | stubElement.closeNode = async () => { 808 | if (stubElement.isNodeOpen()) { 809 | this.closePaginationStub(stubElement); 810 | return true; 811 | } 812 | return false; 813 | }; 814 | 815 | stubElement.toggleNode = () => { 816 | if (stubElement.isNodeOpen()) { 817 | return stubElement.closeNode(); 818 | } else { 819 | return stubElement.openNode(); 820 | } 821 | }; 822 | 823 | return stubElement; 824 | } 825 | 826 | protected closePaginationStub( 827 | stubElement: JsonNodesStubElement, 828 | dispatchEvent = false 829 | ) { 830 | if (stubElement.childrenElement) { 831 | stubElement.headerElement.classList.remove('json-node-open'); 832 | stubElement.removeChild(stubElement.childrenElement); 833 | stubElement.childrenElement = null; 834 | if (dispatchEvent) { 835 | this.dispatchNodeEvent('closeStub', stubElement); 836 | } 837 | } 838 | } 839 | 840 | protected openPaginationStub( 841 | stubElement: JsonNodesStubElement, 842 | node: BigJsonViewerNode, 843 | nodes: BigJsonViewerNode[], 844 | dispatchEvent = false 845 | ) { 846 | stubElement.headerElement.classList.add('json-node-open'); 847 | const children = document.createElement('div'); 848 | children.classList.add('json-node-children'); 849 | stubElement.childrenElement = children; 850 | this.addChildNodes( 851 | nodes, 852 | children, 853 | node.type === 'array' ? this.options.collapseSameValue : Infinity 854 | ); 855 | stubElement.appendChild(children); 856 | if (dispatchEvent) { 857 | this.dispatchNodeEvent('openStub', stubElement); 858 | } 859 | } 860 | 861 | protected addChildNodes( 862 | nodes: BigJsonViewerNode[], 863 | parent: HTMLElement, 864 | collapseSameValue: number 865 | ) { 866 | let lastValue: any; 867 | let sameValueCount = 0; 868 | 869 | nodes.forEach((node, i) => { 870 | if ( 871 | node.type !== 'object' && 872 | node.type !== 'array' && 873 | lastValue === node.value 874 | ) { 875 | sameValueCount++; 876 | if (sameValueCount >= collapseSameValue) { 877 | return; 878 | } 879 | } else if (sameValueCount >= collapseSameValue) { 880 | parent.appendChild(this.getCollapseIndicator(sameValueCount)); 881 | parent.appendChild(this.getNodeElement(nodes[i - 1])); 882 | sameValueCount = 0; 883 | } else { 884 | sameValueCount = 0; 885 | } 886 | parent.appendChild(this.getNodeElement(node)); 887 | lastValue = node.value; 888 | }); 889 | if (sameValueCount >= collapseSameValue) { 890 | parent.appendChild( 891 | this.getCollapseIndicator(sameValueCount - collapseSameValue) 892 | ); 893 | parent.appendChild(this.getNodeElement(nodes[nodes.length - 1])); 894 | } 895 | } 896 | 897 | protected getCollapseIndicator(count): HTMLDivElement { 898 | const element = document.createElement('div'); 899 | element.classList.add('json-node-collapse'); 900 | element.appendChild(document.createTextNode('... [' + count + '] ...')); 901 | return element; 902 | } 903 | 904 | protected getNodeHeader(node: BigJsonViewerNode) { 905 | const element = document.createElement('div'); 906 | element.classList.add('json-node-header'); 907 | element.classList.add('json-node-' + node.type); 908 | 909 | const keyHighlightPattern = 910 | this.currentArea === 'all' || this.currentArea === 'keys' 911 | ? this.currentPattern 912 | : null; 913 | const valueHighlightPattern = 914 | this.currentArea === 'all' || this.currentArea === 'values' 915 | ? this.currentPattern 916 | : null; 917 | 918 | if (node.type === 'object' || node.type === 'array') { 919 | const anchor = document.createElement('a'); 920 | anchor.classList.add('json-node-toggler'); 921 | anchor.href = 'javascript:'; 922 | if (node.length) { 923 | this.attachClickToggleListener(anchor); 924 | this.generateAccessor(anchor); 925 | } 926 | this.generateLabel(anchor, node, keyHighlightPattern); 927 | this.generateTypeInfo(anchor, node); 928 | element.appendChild(anchor); 929 | } else { 930 | this.generateLabel(element, node, keyHighlightPattern); 931 | this.generateValue(element, node, valueHighlightPattern); 932 | this.generateTypeInfo(element, node); 933 | } 934 | 935 | this.generateLinks(element, node); 936 | 937 | return element; 938 | } 939 | 940 | protected generateAccessor(parent: HTMLElement) { 941 | const span = document.createElement('span'); 942 | span.classList.add('json-node-accessor'); 943 | parent.appendChild(span); 944 | } 945 | 946 | protected generateTypeInfo(parent: HTMLElement, node: BigJsonViewerNode) { 947 | const typeInfo = document.createElement('span'); 948 | typeInfo.classList.add('json-node-type'); 949 | if (node.type === 'object') { 950 | typeInfo.appendChild( 951 | document.createTextNode('Object(' + node.length + ')') 952 | ); 953 | } else if (node.type === 'array') { 954 | typeInfo.appendChild( 955 | document.createTextNode('Array[' + node.length + ']') 956 | ); 957 | } else { 958 | typeInfo.appendChild(document.createTextNode(node.type)); 959 | } 960 | parent.appendChild(typeInfo); 961 | } 962 | 963 | protected generateLabel( 964 | parent: HTMLElement, 965 | node: BigJsonViewerNode, 966 | highlightPattern: RegExp 967 | ) { 968 | if (!node.path.length) { 969 | return; 970 | } 971 | const label = document.createElement('span'); 972 | label.classList.add('json-node-label'); 973 | if (this.options.labelAsPath && node.path.length > 1) { 974 | const prefix = document.createElement('span'); 975 | prefix.classList.add('json-node-label-prefix'); 976 | prefix.appendChild( 977 | document.createTextNode( 978 | node.path.slice(0, node.path.length - 1).join('.') + '.' 979 | ) 980 | ); 981 | label.appendChild(prefix); 982 | } 983 | 984 | label.appendChild( 985 | this.getHighlightedText(node.path[node.path.length - 1], highlightPattern) 986 | ); 987 | 988 | parent.appendChild(label); 989 | parent.appendChild(document.createTextNode(': ')); 990 | } 991 | 992 | protected generateValue( 993 | parent: HTMLElement, 994 | node: BigJsonViewerNode, 995 | highlightPattern: RegExp 996 | ) { 997 | const valueElement = document.createElement('span'); 998 | valueElement.classList.add('json-node-value'); 999 | valueElement.appendChild( 1000 | this.getHighlightedText(JSON.stringify(node.value), highlightPattern) 1001 | ); 1002 | parent.appendChild(valueElement); 1003 | } 1004 | 1005 | protected getLabelNode(label: string | HTMLElement): Node { 1006 | if (label instanceof Node) { 1007 | return label; 1008 | } 1009 | return document.createTextNode(label); 1010 | } 1011 | 1012 | protected generateLinks(parent: HTMLElement, node: BigJsonViewerNode) { 1013 | if (this.isOpenableNode(node) && this.options.linkLabelExpandAll) { 1014 | const link = parent.appendChild(document.createElement('a')); 1015 | link.classList.add('json-node-link'); 1016 | link.href = 'javascript:'; 1017 | link.appendChild(this.getLabelNode(this.options.linkLabelExpandAll)); 1018 | link.addEventListener('click', e => { 1019 | e.preventDefault(); 1020 | const nodeElement = this.findNodeElement(parent); 1021 | if (nodeElement && this.isOpenableNode(nodeElement.jsonNode)) { 1022 | this.openAll(nodeElement, Infinity, 'first', true); 1023 | } 1024 | }); 1025 | } 1026 | 1027 | if (node.path.length && this.options.linkLabelCopyPath) { 1028 | const link = parent.appendChild(document.createElement('a')); 1029 | link.classList.add('json-node-link'); 1030 | link.href = 'javascript:'; 1031 | link.appendChild(this.getLabelNode(this.options.linkLabelCopyPath)); 1032 | link.addEventListener('click', e => { 1033 | e.preventDefault(); 1034 | const input = document.createElement('input'); 1035 | input.type = 'text'; 1036 | input.value = node.path.join('.'); 1037 | const nodeElement = this.findNodeElement(parent); 1038 | this.dispatchNodeEvent('copyPath', nodeElement); 1039 | parent.appendChild(input); 1040 | input.select(); 1041 | try { 1042 | if (!document.execCommand('copy')) { 1043 | console.warn('Unable to copy path to clipboard'); 1044 | } 1045 | } catch (e) { 1046 | console.error('Unable to copy path to clipboard', e); 1047 | } 1048 | parent.removeChild(input); 1049 | }); 1050 | } 1051 | 1052 | if (typeof this.options.addLinksHook === 'function') { 1053 | for (const element of this.options.addLinksHook(node)) { 1054 | parent.appendChild(element); 1055 | } 1056 | } 1057 | } 1058 | 1059 | protected findNodeElement(el: HTMLElement): JsonNodeElement { 1060 | while (el && !el['jsonNode']) { 1061 | el = el.parentElement; 1062 | } 1063 | return el as JsonNodeElement; 1064 | } 1065 | } 1066 | -------------------------------------------------------------------------------- /src/big-json-viewer-service.ts: -------------------------------------------------------------------------------- 1 | import { searchJsonNodes } from './parser/json-node-search'; 2 | import { 3 | BigJsonViewerNode, 4 | TreeSearchAreaOption, 5 | TreeSearchMatch 6 | } from './model/big-json-viewer.model'; 7 | import { BufferJsonParser } from './parser/buffer-json-parser'; 8 | import { JsonNodeInfo } from './parser/json-node-info'; 9 | import { JsParser } from './parser/js-parser'; 10 | 11 | export class BigJsonViewerService { 12 | rootNode: JsonNodeInfo; 13 | 14 | initWithData(data: ArrayBuffer | string): BigJsonViewerNode { 15 | this.rootNode = new BufferJsonParser(data).getRootNodeInfo(); 16 | 17 | return this.getRenderInfo(this.rootNode); 18 | } 19 | 20 | initWithJs(data: any): BigJsonViewerNode { 21 | this.rootNode = new JsParser(data).getRootNodeInfo(); 22 | 23 | return this.getRenderInfo(this.rootNode); 24 | } 25 | 26 | getNodes(path: string[], start: number, limit: number): BigJsonViewerNode[] { 27 | const node = this.rootNode.getByPath(path); 28 | if (node && node.type === 'object') { 29 | return node.getObjectNodes(start, limit).map(n => this.getRenderInfo(n)); 30 | } 31 | if (node && node.type === 'array') { 32 | return node.getArrayNodes(start, limit).map(n => this.getRenderInfo(n)); 33 | } 34 | return null; 35 | } 36 | 37 | getKeyIndex(path: string[], key: string): number { 38 | const node = this.rootNode.getByPath(path); 39 | if (!node) { 40 | return -1; 41 | } 42 | const keys = node.getObjectKeys(); 43 | return keys.indexOf(key); 44 | } 45 | 46 | search(pattern: RegExp, searchArea: TreeSearchAreaOption): TreeSearchMatch[] { 47 | return searchJsonNodes(this.rootNode, pattern, searchArea); 48 | } 49 | 50 | protected getRenderInfo(node: JsonNodeInfo): BigJsonViewerNode { 51 | const info: BigJsonViewerNode = { 52 | type: node.type, 53 | length: node.length, 54 | path: node.path, 55 | openable: this.isOpenableNode(node) 56 | }; 57 | if (!info.openable) { 58 | info.value = node.getValue(); 59 | } 60 | return info; 61 | } 62 | 63 | protected isOpenableNode(node: JsonNodeInfo): boolean { 64 | return (node.type === 'array' || node.type === 'object') && !!node.length; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/browser-api.ts: -------------------------------------------------------------------------------- 1 | import 'babel-polyfill'; 2 | import { BigJsonViewerDom } from './big-json-viewer-dom'; 3 | 4 | window['BigJsonViewerDom'] = BigJsonViewerDom; 5 | -------------------------------------------------------------------------------- /src/helpers/utils.ts: -------------------------------------------------------------------------------- 1 | export function assertStartLimit(start, limit) { 2 | if (isNaN(start) || start < 0) { 3 | throw new Error(`Invalid start ${start}`); 4 | } 5 | if (limit && limit < 0) { 6 | throw new Error(`Invalid limit ${limit}`); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/helpers/worker-client.spec.ts: -------------------------------------------------------------------------------- 1 | import { WorkerClient, WorkerClientMock } from './worker-client'; 2 | import { initProvider } from './worker-provider'; 3 | 4 | class MockMessageEvent { 5 | constructor(public data) {} 6 | } 7 | 8 | class MockErrorEvent { 9 | constructor(public message) {} 10 | } 11 | 12 | class MockWorker { 13 | onerror: (ev: MockErrorEvent) => any; 14 | onmessage: (ev: MockMessageEvent) => any; 15 | 16 | postMessage(message: any, transfer?: any[]): void {} 17 | 18 | terminate(): void {} 19 | } 20 | 21 | class MockScope { 22 | onerror: (ev: MockErrorEvent) => any; 23 | onmessage: (ev: MockMessageEvent) => any; 24 | 25 | constructor(private worker: MockWorker) { 26 | worker.postMessage = (message: any, transfer?: any[]) => { 27 | this.onmessage(new MockMessageEvent(message)); 28 | }; 29 | } 30 | 31 | postMessage(message: any, transfer?: any[]): void { 32 | this.worker.onmessage(new MockMessageEvent(message)); 33 | } 34 | } 35 | 36 | describe('Worker Client', function() { 37 | it('should init client with worker', async function() { 38 | const mockWorker = new MockWorker(); 39 | const mockScope = new MockScope(mockWorker); 40 | initProvider({}, mockScope); 41 | 42 | const client = new WorkerClient((mockWorker as any) as Worker); 43 | expect(client).toBeTruthy(); 44 | await expect(client.initWorker()).resolves.toBeTruthy(); 45 | 46 | spyOn(mockWorker, 'terminate'); 47 | 48 | client.destroy(); 49 | 50 | expect(mockWorker.terminate).toHaveBeenCalled(); 51 | }); 52 | 53 | it('should fail to init', async function() { 54 | const mockWorker = new MockWorker(); 55 | mockWorker.postMessage = msg => { 56 | mockWorker.onerror(new MockErrorEvent('failed')); 57 | }; 58 | const client = new WorkerClient((mockWorker as any) as Worker); 59 | expect(client).toBeTruthy(); 60 | await expect(client.initWorker()).rejects.toEqual({ message: 'failed' }); 61 | }); 62 | 63 | it('should request hello', async function() { 64 | const mockWorker = new MockWorker(); 65 | const mockScope = new MockScope(mockWorker); 66 | initProvider( 67 | { 68 | hello(name: string) { 69 | return 'Hello ' + name; 70 | } 71 | }, 72 | mockScope 73 | ); 74 | 75 | const client = new WorkerClient((mockWorker as any) as Worker); 76 | expect(client).toBeTruthy(); 77 | await expect(client.initWorker()).resolves.toBeTruthy(); 78 | await expect(client.call('hello', 'World')).resolves.toBe('Hello World'); 79 | }); 80 | 81 | it('should fail', async function() { 82 | const mockWorker = new MockWorker(); 83 | const mockScope = new MockScope(mockWorker); 84 | initProvider( 85 | { 86 | fail() { 87 | throw new Error('failed'); 88 | } 89 | }, 90 | mockScope 91 | ); 92 | 93 | const client = new WorkerClient((mockWorker as any) as Worker); 94 | expect(client).toBeTruthy(); 95 | await expect(client.initWorker()).resolves.toBeTruthy(); 96 | await expect(client.call('fail')).rejects.toBe( 97 | new Error('failed').toString() 98 | ); 99 | }); 100 | }); 101 | 102 | describe('Worker Client Mock', function() { 103 | it('should call hello', async function() { 104 | const client = new WorkerClientMock({ 105 | hello(name) { 106 | return 'Hello ' + name; 107 | } 108 | }); 109 | 110 | expect(client).toBeTruthy(); 111 | 112 | await expect(client.call('hello', 'World')).resolves.toBe('Hello World'); 113 | 114 | client.destroy(); 115 | }); 116 | }); 117 | -------------------------------------------------------------------------------- /src/helpers/worker-client.ts: -------------------------------------------------------------------------------- 1 | export interface WorkerClientApi { 2 | call(handler: string, ...args): Promise; 3 | 4 | callWorker(handler: string, transfers: any[], ...args): Promise; 5 | 6 | destroy(); 7 | } 8 | 9 | export class WorkerClient implements WorkerClientApi { 10 | private requestIndex = 0; 11 | private requestCallbacks = {}; 12 | private initialized; 13 | 14 | constructor(private worker: Worker) {} 15 | 16 | public initWorker(): Promise { 17 | return new Promise((resolve, reject) => { 18 | this.worker.onmessage = msg => { 19 | const data = msg.data; 20 | if (data._init === true) { 21 | this.initialized = true; 22 | resolve(true); 23 | return; 24 | } 25 | if (data.resultId && this.requestCallbacks[data.resultId]) { 26 | const callb = this.requestCallbacks[data.resultId]; 27 | delete this.requestCallbacks[data.resultId]; 28 | callb(data); 29 | } 30 | }; 31 | this.worker.onerror = e => { 32 | if (!this.initialized) { 33 | reject(e); 34 | } else { 35 | console.error('Worker error', e); 36 | } 37 | }; 38 | this.worker.postMessage({ _init: true }); 39 | }); 40 | } 41 | 42 | public call(handler, ...args): Promise { 43 | return this.callWorker(handler, undefined, ...args); 44 | } 45 | 46 | public callWorker(handler, transfers = undefined, ...args): Promise { 47 | return new Promise((resolve, reject) => { 48 | const resultId = ++this.requestIndex; 49 | this.requestCallbacks[resultId] = data => { 50 | if (data.error !== undefined) { 51 | reject(data.error); 52 | return; 53 | } 54 | resolve(data.result); 55 | }; 56 | this.worker.postMessage( 57 | { 58 | handler: handler, 59 | args: args, 60 | resultId: resultId 61 | }, 62 | transfers 63 | ); 64 | }); 65 | } 66 | 67 | public destroy() { 68 | this.worker.terminate(); 69 | this.worker = null; 70 | this.requestCallbacks = null; 71 | } 72 | } 73 | 74 | export class WorkerClientMock implements WorkerClientApi { 75 | constructor(private provider) {} 76 | 77 | public call(handler, ...args): Promise { 78 | return this.callWorker(handler, undefined, ...args); 79 | } 80 | 81 | public callWorker(handler, transfers = undefined, ...args): Promise { 82 | return new Promise(resolve => { 83 | resolve(this.provider[handler].apply(this.provider, args)); 84 | }); 85 | } 86 | public destroy() { 87 | this.provider = null; 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/helpers/worker-provider.ts: -------------------------------------------------------------------------------- 1 | export function initProvider(impl, scope = self as any) { 2 | scope.onmessage = function(msg) { 3 | const data = msg.data; 4 | if (data._init) { 5 | scope.postMessage({ _init: true }); 6 | return; 7 | } 8 | 9 | if (data.handler && impl[data.handler]) { 10 | try { 11 | const result = impl[data.handler].apply(impl, data.args); 12 | if (data.resultId) { 13 | scope.postMessage({ 14 | resultId: data.resultId, 15 | result: result 16 | }); 17 | } 18 | } catch (e) { 19 | if (data.resultId) { 20 | scope.postMessage({ 21 | resultId: data.resultId, 22 | error: e.toString() 23 | }); 24 | } 25 | } 26 | } 27 | }; 28 | } 29 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './big-json-viewer-dom'; 2 | export * from './model/big-json-viewer.model'; 3 | -------------------------------------------------------------------------------- /src/model/big-json-viewer.model.ts: -------------------------------------------------------------------------------- 1 | import { JsonNodeInfoBase } from '../parser/json-node-info'; 2 | 3 | /** 4 | * Represents an interactive node in the Big Json Viewer 5 | */ 6 | export interface BigJsonViewerNode extends JsonNodeInfoBase { 7 | openable: boolean; 8 | value?: any; 9 | children?: BigJsonViewerNode[]; 10 | } 11 | 12 | export interface JsonNodesStubElement extends HTMLDivElement { 13 | headerElement: HTMLElement; 14 | childrenElement?: HTMLElement; 15 | 16 | isNodeOpen(): boolean; 17 | 18 | openNode(): Promise; 19 | 20 | closeNode(): Promise; 21 | 22 | toggleNode(): Promise; 23 | } 24 | 25 | export type BigJsonViewerEvent = 26 | | 'openNode' // when the user opens a single node 27 | | 'closeNode' // when the user closes a node 28 | | 'openedNodes' // when multiple nodes were opened e.g. by expand all or search 29 | | 'openStub' // when the user opens a single stub 30 | | 'closeStub' // when the user closed a stub 31 | | 'copyPath'; 32 | export type PaginatedOption = 33 | | 'first' // open only the first pagination stub 34 | | 'all' // open all pagination stubs 35 | | 'none'; 36 | 37 | export type TreeSearchAreaOption = 38 | | 'all' // search in keys and values 39 | | 'keys' // search only in keys 40 | | 'values'; 41 | 42 | export interface TreeSearchMatch { 43 | path: string[]; 44 | key?: number; // if the match was in the key, at which index 45 | value?: number; // if the match was in the value, at which index 46 | length: number; // length of the match 47 | } 48 | 49 | export interface TreeSearchCursor { 50 | /** 51 | * Currently focused match 52 | */ 53 | index: number; 54 | 55 | /** 56 | * Matches represented by their paths 57 | */ 58 | matches: TreeSearchMatch[]; 59 | 60 | /** 61 | * Navigate to the next match 62 | */ 63 | next(): Promise; 64 | 65 | /** 66 | * Navigate to the previous match 67 | */ 68 | previous(): Promise; 69 | 70 | /** 71 | * Navigate to the given match 72 | */ 73 | navigateTo(index: number): Promise; 74 | } 75 | 76 | export interface BigJsonViewerOptions { 77 | /** 78 | * How many nodes to show under an object at once 79 | * before pagination starts 80 | * @default 50 81 | */ 82 | objectNodesLimit?: number; 83 | 84 | /** 85 | * How many nodes to show under an array at once 86 | * before pagination starts 87 | * @default 50 88 | */ 89 | arrayNodesLimit?: number; 90 | 91 | /** 92 | * Whether the label before an item should show the whole path. 93 | * @default false 94 | */ 95 | labelAsPath?: boolean; 96 | 97 | /** 98 | * What label should be displayed on the Copy Path link. 99 | * Set null to disable this link 100 | */ 101 | linkLabelCopyPath?: string | HTMLElement; 102 | 103 | /** 104 | * What label should be displayed on the Expand all link. 105 | * Set null to disable this link 106 | */ 107 | linkLabelExpandAll?: string | HTMLElement; 108 | 109 | /** 110 | * Path to the worker bundle, null by default 111 | */ 112 | workerPath?: string; 113 | 114 | /** 115 | * Amount of the same value in arrays should be shown before they are being collapsed. 116 | * Can be Infinity 117 | * @default 5 118 | */ 119 | collapseSameValue?: number; 120 | 121 | /** 122 | * Register a hook function that is called for every opened node to add additional links to a node. 123 | */ 124 | addLinksHook?: (node: BigJsonViewerNode) => HTMLElement[]; 125 | } 126 | 127 | export interface BigJsonViewer {} 128 | -------------------------------------------------------------------------------- /src/parser/buffer-json-parser.spec.ts: -------------------------------------------------------------------------------- 1 | import { BufferJsonNodeInfo, BufferJsonParser } from './buffer-json-parser'; 2 | 3 | describe('Buffer JSON Parser', function() { 4 | it('should handle empty input', function() { 5 | const instance = new BufferJsonParser(''); 6 | const info = instance.getRootNodeInfo(); 7 | expect(info).toBeNull(); 8 | }); 9 | 10 | it('should handle empty object', function() { 11 | const instance = new BufferJsonParser('{}'); 12 | const info = instance.getRootNodeInfo(); 13 | expect(info.type).toEqual('object'); 14 | expect(info.length).toEqual(0); 15 | expect(info.chars).toEqual(2); 16 | expect(info.path.length).toEqual(0); 17 | 18 | const keys = info.getObjectKeys(); 19 | expect(keys).toBeTruthy(); 20 | expect(keys.length).toEqual(0); 21 | 22 | const nodes = info.getObjectNodes(); 23 | expect(nodes).toBeTruthy(); 24 | expect(nodes.length).toEqual(0); 25 | }); 26 | 27 | it('should handle empty string', function() { 28 | const instance = new BufferJsonParser('""'); 29 | const info = instance.getRootNodeInfo(); 30 | expect(info.type).toEqual('string'); 31 | expect(info.length).toEqual(0); 32 | expect(info.chars).toEqual(2); 33 | expect(info.path.length).toEqual(0); 34 | expect(info.getValue()).toEqual(''); 35 | }); 36 | 37 | it('should handle number', function() { 38 | const instance = new BufferJsonParser('43246'); 39 | const info = instance.getRootNodeInfo(); 40 | expect(info.type).toEqual('number'); 41 | expect(info.chars).toEqual(5); 42 | expect(info.path.length).toEqual(0); 43 | expect(info.getValue()).toEqual(43246); 44 | }); 45 | 46 | it('should handle negative number', function() { 47 | const instance = new BufferJsonParser('-343'); 48 | const info = instance.getRootNodeInfo(); 49 | expect(info.type).toEqual('number'); 50 | expect(info.chars).toEqual(4); 51 | expect(info.path.length).toEqual(0); 52 | expect(info.getValue()).toEqual(-343); 53 | }); 54 | 55 | it('should handle negative number with e', function() { 56 | const instance = new BufferJsonParser('-34e3'); 57 | const info = instance.getRootNodeInfo(); 58 | expect(info.type).toEqual('number'); 59 | expect(info.chars).toEqual(5); 60 | expect(info.path.length).toEqual(0); 61 | expect(info.getValue()).toEqual(-34e3); 62 | }); 63 | 64 | it('should handle string', function() { 65 | const instance = new BufferJsonParser('"abc"'); 66 | const info = instance.getRootNodeInfo(); 67 | expect(info.type).toEqual('string'); 68 | expect(info.length).toEqual(3); 69 | expect(info.chars).toEqual(5); 70 | expect(info.path.length).toEqual(0); 71 | expect(info.getValue()).toEqual('abc'); 72 | }); 73 | 74 | it('should handle string with special chars', function() { 75 | const instance = new BufferJsonParser('"abc\\nxy\\\\z"'); 76 | const info = instance.getRootNodeInfo(); 77 | expect(info.type).toEqual('string'); 78 | expect(info.length).toEqual(8); 79 | expect(info.chars).toEqual(12); 80 | expect(info.path.length).toEqual(0); 81 | expect(info.getValue()).toEqual('abc\nxy\\z'); 82 | }); 83 | 84 | it('should handle null', function() { 85 | const instance = new BufferJsonParser('null'); 86 | const info = instance.getRootNodeInfo(); 87 | expect(info.type).toEqual('null'); 88 | expect(info.chars).toEqual(4); 89 | expect(info.path.length).toEqual(0); 90 | expect(info.getValue()).toEqual(null); 91 | }); 92 | 93 | it('should handle true', function() { 94 | const instance = new BufferJsonParser('true'); 95 | const info = instance.getRootNodeInfo(); 96 | expect(info.type).toEqual('boolean'); 97 | expect(info.chars).toEqual(4); 98 | expect(info.path.length).toEqual(0); 99 | expect(info.getValue()).toEqual(true); 100 | }); 101 | 102 | it('should handle false', function() { 103 | const instance = new BufferJsonParser('false'); 104 | const info = instance.getRootNodeInfo(); 105 | expect(info.type).toEqual('boolean'); 106 | expect(info.chars).toEqual(5); 107 | expect(info.path.length).toEqual(0); 108 | expect(info.getValue()).toEqual(false); 109 | }); 110 | 111 | it('should handle object with one key', function() { 112 | const instance = new BufferJsonParser('{"key1": "value1"}'); 113 | const info = instance.getRootNodeInfo(); 114 | expect(info.type).toEqual('object'); 115 | expect(info.length).toEqual(1); 116 | expect(info.chars).toEqual(18); 117 | expect(info.path.length).toEqual(0); 118 | 119 | const keys = info.getObjectKeys(); 120 | expect(keys).toBeTruthy(); 121 | expect(keys.length).toEqual(1); 122 | expect(keys[0]).toEqual('key1'); 123 | 124 | const nodes = info.getObjectNodes(); 125 | expect(nodes).toBeTruthy(); 126 | expect(nodes.length).toEqual(1); 127 | expectStringNode(nodes[0], 'value1', 8, ['key1']); 128 | }); 129 | 130 | it('should handle object with multi keys', function() { 131 | const instance = new BufferJsonParser('{"key1": "value1","key2": []}'); 132 | const info = instance.getRootNodeInfo(); 133 | expect(info.type).toEqual('object'); 134 | expect(info.length).toEqual(2); 135 | expect(info.chars).toEqual(29); 136 | expect(info.path.length).toEqual(0); 137 | 138 | const keys = info.getObjectKeys(); 139 | expect(keys).toBeTruthy(); 140 | expect(keys.length).toEqual(2); 141 | expect(keys[0]).toEqual('key1'); 142 | expect(keys[1]).toEqual('key2'); 143 | 144 | const nodes = info.getObjectNodes(); 145 | expect(nodes).toBeTruthy(); 146 | expect(nodes.length).toEqual(2); 147 | 148 | let node = nodes[0]; 149 | expectStringNode(nodes[0], 'value1', 8, ['key1']); 150 | 151 | node = nodes[1]; 152 | expect(node.type).toEqual('array'); 153 | expect(node.length).toEqual(0); 154 | expect(node.chars).toEqual(2); 155 | expect(node.path.length).toEqual(1); 156 | expect(node.path[0]).toEqual('key2'); 157 | 158 | node = info.getByKey('key1'); 159 | expectStringNode(node, 'value1', 8, ['key1']); 160 | }); 161 | 162 | it('should handle empty array', function() { 163 | const instance = new BufferJsonParser('[]'); 164 | const info = instance.getRootNodeInfo(); 165 | expect(info.type).toEqual('array'); 166 | expect(info.length).toEqual(0); 167 | expect(info.chars).toEqual(2); 168 | expect(info.path.length).toEqual(0); 169 | 170 | const nodes = info.getArrayNodes(); 171 | expect(nodes.length).toEqual(0); 172 | }); 173 | 174 | it('should handle array with elements', function() { 175 | const instance = new BufferJsonParser('[0, "ac"]'); 176 | const info = instance.getRootNodeInfo(); 177 | expect(info.type).toEqual('array'); 178 | expect(info.length).toEqual(2); 179 | expect(info.chars).toEqual(9); 180 | expect(info.path.length).toEqual(0); 181 | 182 | const nodes = info.getArrayNodes(); 183 | expect(nodes.length).toEqual(2); 184 | 185 | expectNumberNode(nodes[0], 0, 1, ['0']); 186 | expectStringNode(nodes[1], 'ac', 4, ['1']); 187 | }); 188 | 189 | it('should handle array with tokens', function() { 190 | const instance = new BufferJsonParser('[true, false, null]'); 191 | const info = instance.getRootNodeInfo(); 192 | expect(info.type).toEqual('array'); 193 | expect(info.length).toEqual(3); 194 | expect(info.chars).toEqual(19); 195 | expect(info.path.length).toEqual(0); 196 | 197 | const nodes = info.getArrayNodes(); 198 | expect(nodes.length).toEqual(3); 199 | 200 | expect(nodes[0].type).toEqual('boolean'); 201 | expect(nodes[0].chars).toEqual(4); 202 | expect(nodes[0].getValue()).toEqual(true); 203 | expect(nodes[0].path.length).toEqual(1); 204 | 205 | expect(nodes[1].type).toEqual('boolean'); 206 | expect(nodes[1].chars).toEqual(5); 207 | expect(nodes[1].getValue()).toEqual(false); 208 | expect(nodes[1].path.length).toEqual(1); 209 | 210 | expect(nodes[2].type).toEqual('null'); 211 | expect(nodes[2].chars).toEqual(4); 212 | expect(nodes[2].getValue()).toEqual(null); 213 | expect(nodes[2].path.length).toEqual(1); 214 | }); 215 | 216 | it('should handle array pagination', function() { 217 | const instance = new BufferJsonParser('["a", "b", "c", "d", "e"]'); 218 | const info = instance.getRootNodeInfo(); 219 | expect(info.type).toEqual('array'); 220 | expect(info.length).toEqual(5); 221 | expect(info.chars).toEqual(25); 222 | 223 | let nodes = info.getArrayNodes(1, 2); 224 | expect(nodes.length).toEqual(2); 225 | expectStringNode(nodes[0], 'b', 3, ['1']); 226 | expectStringNode(nodes[1], 'c', 3, ['2']); 227 | 228 | nodes = info.getArrayNodes(0, 1); 229 | expect(nodes.length).toEqual(1); 230 | expectStringNode(nodes[0], 'a', 3, ['0']); 231 | 232 | nodes = info.getArrayNodes(3, 4); 233 | expect(nodes.length).toEqual(2); 234 | expectStringNode(nodes[0], 'd', 3, ['3']); 235 | expectStringNode(nodes[1], 'e', 3, ['4']); 236 | }); 237 | 238 | it('should handle object pagination', function() { 239 | const instance = new BufferJsonParser( 240 | '{"a": "A", "b": "B", "c": "C", "d": "D"}' 241 | ); 242 | const info = instance.getRootNodeInfo(); 243 | expect(info.type).toEqual('object'); 244 | expect(info.length).toEqual(4); 245 | expect(info.chars).toEqual(40); 246 | 247 | let keys = info.getObjectKeys(1, 2); 248 | expect(keys.length).toEqual(2); 249 | expect(keys[0]).toEqual('b'); 250 | expect(keys[1]).toEqual('c'); 251 | 252 | keys = info.getObjectKeys(3, 5); 253 | expect(keys.length).toEqual(1); 254 | expect(keys[0]).toEqual('d'); 255 | 256 | const nodes = info.getObjectNodes(2, 5); 257 | expect(nodes.length).toEqual(2); 258 | expectStringNode(nodes[0], 'C', 3, ['c']); 259 | expectStringNode(nodes[1], 'D', 3, ['d']); 260 | 261 | const node = info.getByIndex(1); 262 | expectStringNode(node, 'B', 3, ['b']); 263 | }); 264 | 265 | it('should handle nested objects', function() { 266 | const instance = new BufferJsonParser( 267 | '{"a": {"b": {"c": true, "d": "D"}}}' 268 | ); 269 | const info = instance.getRootNodeInfo(); 270 | expect(info.type).toEqual('object'); 271 | expect(info.length).toEqual(1); 272 | expect(info.chars).toEqual(35); 273 | 274 | let nodes = info.getObjectNodes(); 275 | let node = nodes[0]; 276 | expect(node.type).toEqual('object'); 277 | expect(node.length).toEqual(1); 278 | expect(node.chars).toEqual(28); 279 | expect(node.path.length).toEqual(1); 280 | expect(node.path[0]).toEqual('a'); 281 | 282 | nodes = node.getObjectNodes(); 283 | node = nodes[0]; 284 | expect(node.type).toEqual('object'); 285 | expect(node.length).toEqual(2); 286 | expect(node.chars).toEqual(21); 287 | expect(node.path.length).toEqual(2); 288 | expect(node.path[0]).toEqual('a'); 289 | expect(node.path[1]).toEqual('b'); 290 | 291 | nodes = node.getObjectNodes(); 292 | node = nodes[0]; 293 | expect(node.type).toEqual('boolean'); 294 | expect(node.chars).toEqual(4); 295 | expect(node.path.length).toEqual(3); 296 | expect(node.path[0]).toEqual('a'); 297 | expect(node.path[1]).toEqual('b'); 298 | expect(node.path[2]).toEqual('c'); 299 | expect(node.getValue()).toEqual(true); 300 | 301 | node = info.getByPath('a.b.d'.split('.')); 302 | expectStringNode(node, 'D', 3, ['a', 'b', 'd']); 303 | 304 | expect(info.getByPath('a.b.e'.split('.'))).toBeUndefined(); 305 | expect(info.getByPath([])).toEqual(info); 306 | }); 307 | 308 | it('should throw on incomplete object', function() { 309 | const instance = new BufferJsonParser('{'); 310 | expect(() => instance.getRootNodeInfo()).toThrowError( 311 | 'parse object incomplete at end' 312 | ); 313 | }); 314 | 315 | it('should throw on incomplete array', function() { 316 | const instance = new BufferJsonParser('{"d": ["sd",}'); 317 | expect(() => instance.getRootNodeInfo()).toThrowError( 318 | 'parse value unknown token } at 12' 319 | ); 320 | }); 321 | 322 | it('should throw on incomplete string', function() { 323 | const instance = new BufferJsonParser('"abc'); 324 | expect(() => instance.getRootNodeInfo()).toThrowError( 325 | 'parse string incomplete at end' 326 | ); 327 | }); 328 | }); 329 | 330 | function expectStringNode( 331 | node: BufferJsonNodeInfo, 332 | value: string, 333 | chars: number, 334 | path: string[] 335 | ) { 336 | expect(node.type).toEqual('string'); 337 | expect(node.length).toEqual(value.length); 338 | expect(node.chars).toEqual(chars); 339 | expect(node.path.length).toEqual(path.length); 340 | for (let i = 0; i < path.length; i++) { 341 | expect(node.path[i]).toEqual(path[i]); 342 | } 343 | expect(node.getValue()).toEqual(value); 344 | } 345 | 346 | function expectNumberNode( 347 | node: BufferJsonNodeInfo, 348 | value: number, 349 | chars: number, 350 | path: string[] 351 | ) { 352 | expect(node.type).toEqual('number'); 353 | expect(node.chars).toEqual(chars); 354 | expect(node.path.length).toEqual(path.length); 355 | for (let i = 0; i < path.length; i++) { 356 | expect(node.path[i]).toEqual(path[i]); 357 | } 358 | expect(node.getValue()).toEqual(value); 359 | } 360 | -------------------------------------------------------------------------------- /src/parser/buffer-json-parser.ts: -------------------------------------------------------------------------------- 1 | import { JsonNodeInfo, NodeType } from './json-node-info'; 2 | import { assertStartLimit } from '../helpers/utils'; 3 | 4 | const BRACE_START = '{'.charCodeAt(0); 5 | const BRACE_END = '}'.charCodeAt(0); 6 | const BRACKET_START = '['.charCodeAt(0); 7 | const BRACKET_END = ']'.charCodeAt(0); 8 | const COLON = ':'.charCodeAt(0); 9 | const COMMA = ','.charCodeAt(0); 10 | const DOUBLE_QUOTE = '"'.charCodeAt(0); 11 | const SINGLE_QUOTE = "'".charCodeAt(0); 12 | const SPACE = ' '.charCodeAt(0); 13 | const TAB = '\t'.charCodeAt(0); 14 | const NEWLINE = '\n'.charCodeAt(0); 15 | const BACKSPACE = '\b'.charCodeAt(0); 16 | const CARRIAGE_RETURN = '\r'.charCodeAt(0); 17 | const FORM_FEED = '\f'.charCodeAt(0); 18 | const BACK_SLASH = '\\'.charCodeAt(0); 19 | const FORWARD_SLASH = '/'.charCodeAt(0); 20 | const MINUS = '-'.charCodeAt(0); 21 | const PLUS = '+'.charCodeAt(0); 22 | const DOT = '.'.charCodeAt(0); 23 | const CHAR_E_LOW = 'e'.charCodeAt(0); 24 | const CHAR_E_HIGH = 'E'.charCodeAt(0); 25 | const DIGIT_0 = '0'.charCodeAt(0); 26 | const DIGIT_9 = '9'.charCodeAt(0); 27 | 28 | const IGNORED = [SPACE, TAB, NEWLINE, CARRIAGE_RETURN]; 29 | 30 | const NULL = 'null'.split('').map(d => d.charCodeAt(0)); 31 | const TRUE = 'true'.split('').map(d => d.charCodeAt(0)); 32 | const FALSE = 'false'.split('').map(d => d.charCodeAt(0)); 33 | 34 | export interface ParseContext { 35 | path: string[]; 36 | start?: number; 37 | limit?: number; 38 | objectKey?: string; 39 | objectKeys?: string[]; // truthy if keys should be resolved 40 | objectNodes?: BufferJsonNodeInfo[]; // truthy if nodes should be resolved 41 | arrayNodes?: BufferJsonNodeInfo[]; // truthy if nodes should be resolved 42 | value?: string | number | boolean; // truthy if value should be resolved 43 | nodeInfo?: BufferJsonNodeInfo; // truthy if node info should be filled 44 | } 45 | 46 | export class BufferJsonNodeInfo implements JsonNodeInfo { 47 | public type: NodeType; 48 | public path: string[] = []; 49 | public length?: number; // in case of array, object, string 50 | public chars: number; 51 | private parser: BufferJsonParser; 52 | private index; 53 | 54 | constructor(parser: BufferJsonParser, index: number, path: string[]) { 55 | this.parser = parser; 56 | this.index = index; 57 | this.path = path; 58 | } 59 | 60 | /** 61 | * Returns the list of keys in case of an object for the defined range 62 | * @param {number} start 63 | * @param {number} limit 64 | */ 65 | public getObjectKeys(start = 0, limit?: number): string[] { 66 | if (this.type !== 'object') { 67 | throw new Error(`Unsupported method on non-object ${this.type}`); 68 | } 69 | assertStartLimit(start, limit); 70 | const ctx: ParseContext = { 71 | path: this.path, 72 | objectKeys: [], 73 | start: start, 74 | limit: limit 75 | }; 76 | this.parser.parseObject(this.index, ctx); 77 | return ctx.objectKeys; 78 | } 79 | 80 | /** 81 | * Return the NodeInfo at the defined position. 82 | * Use the index from getObjectKeys 83 | * @param index 84 | */ 85 | public getByIndex(index: number): BufferJsonNodeInfo { 86 | if (this.type === 'object') { 87 | const nodes = this.getObjectNodes(index, 1); 88 | if (nodes.length) { 89 | return nodes[0]; 90 | } 91 | } 92 | if (this.type === 'array') { 93 | const nodes = this.getArrayNodes(index, 1); 94 | if (nodes.length) { 95 | return nodes[0]; 96 | } 97 | } 98 | return undefined; 99 | } 100 | 101 | /** 102 | * Return the NodeInfo for the specified key 103 | * Use the index from getObjectKeys 104 | * @param key 105 | */ 106 | public getByKey(key: string): BufferJsonNodeInfo { 107 | if (this.type === 'object') { 108 | const ctx: ParseContext = { 109 | path: this.path, 110 | objectKey: key 111 | }; 112 | this.parser.parseObject(this.index, ctx); 113 | return ctx.objectNodes ? ctx.objectNodes[0] : undefined; 114 | } 115 | if (this.type === 'array') { 116 | return this.getByIndex(parseInt(key)); 117 | } 118 | return undefined; 119 | } 120 | 121 | /** 122 | * Find the information for a given path 123 | * @param {string[]} path 124 | */ 125 | public getByPath(path: string[]): BufferJsonNodeInfo { 126 | if (!path) { 127 | return undefined; 128 | } 129 | if (!path.length) { 130 | return this; 131 | } 132 | const p = path.slice(); 133 | let key: string; 134 | let node: BufferJsonNodeInfo = this; 135 | while ((key = p.shift()) !== undefined && node) { 136 | node = node.getByKey(key); 137 | } 138 | return node; 139 | } 140 | 141 | /** 142 | * Returns a list with the NodeInfo objects for the defined range 143 | * @param {number} start 144 | * @param {number} limit 145 | */ 146 | public getObjectNodes(start = 0, limit?: number): BufferJsonNodeInfo[] { 147 | if (this.type !== 'object') { 148 | throw new Error(`Unsupported method on non-object ${this.type}`); 149 | } 150 | assertStartLimit(start, limit); 151 | const ctx: ParseContext = { 152 | path: this.path, 153 | objectNodes: [], 154 | start: start, 155 | limit: limit 156 | }; 157 | this.parser.parseObject(this.index, ctx); 158 | return ctx.objectNodes; 159 | } 160 | 161 | /** 162 | * Returns a list of NodeInfo for the defined range 163 | * @param {number} start 164 | * @param {number} limit 165 | */ 166 | public getArrayNodes(start = 0, limit?: number): BufferJsonNodeInfo[] { 167 | if (this.type !== 'array') { 168 | throw new Error(`Unsupported method on non-array ${this.type}`); 169 | } 170 | assertStartLimit(start, limit); 171 | const ctx: ParseContext = { 172 | path: this.path, 173 | arrayNodes: [], 174 | start: start, 175 | limit: limit 176 | }; 177 | this.parser.parseArray(this.index, ctx); 178 | return ctx.arrayNodes; 179 | } 180 | 181 | /** 182 | * Get the natively parsed value 183 | */ 184 | public getValue(): any { 185 | return this.parser.parseNative(this.index, this.index + this.chars); 186 | } 187 | } 188 | 189 | declare const TextEncoder; 190 | 191 | /** 192 | * Parses meta data about a JSON structure in an ArrayBuffer. 193 | */ 194 | export class BufferJsonParser { 195 | data: Uint16Array; 196 | 197 | constructor(data: ArrayBuffer | string) { 198 | if (data instanceof ArrayBuffer) { 199 | this.data = new Uint16Array(data); 200 | } else if (typeof data === 'string' && typeof TextEncoder !== 'undefined') { 201 | this.data = new TextEncoder().encode(data); 202 | } else if (typeof data === 'string') { 203 | this.data = new Uint16Array(new ArrayBuffer(data.length * 2)); 204 | for (let i = 0; i < data.length; i++) { 205 | this.data[i] = data.charCodeAt(i); 206 | } 207 | } 208 | } 209 | 210 | getRootNodeInfo(): BufferJsonNodeInfo { 211 | let start = this.skipIgnored(0); 212 | 213 | const ctx: ParseContext = { 214 | path: [], 215 | nodeInfo: new BufferJsonNodeInfo(this, start, []) 216 | }; 217 | 218 | const end = this.parseValue(start, ctx, false); 219 | if (start === end) { 220 | return null; 221 | } 222 | return ctx.nodeInfo; 223 | } 224 | 225 | parseValue(start: number, ctx?: ParseContext, throwUnknown = true): number { 226 | const char = this.data[start]; 227 | if (isString(char)) { 228 | return this.parseString(start, ctx); 229 | } 230 | if (isNumber(char)) { 231 | return this.parseNumber(start, ctx); 232 | } 233 | if (char === BRACE_START) { 234 | return this.parseObject(start, ctx); 235 | } 236 | if (char === BRACKET_START) { 237 | return this.parseArray(start, ctx); 238 | } 239 | if (char === TRUE[0]) { 240 | return this.parseToken(start, TRUE, ctx); 241 | } 242 | if (char === FALSE[0]) { 243 | return this.parseToken(start, FALSE, ctx); 244 | } 245 | if (char === NULL[0]) { 246 | return this.parseToken(start, NULL, ctx); 247 | } 248 | 249 | if (throwUnknown) { 250 | throw new Error( 251 | `parse value unknown token ${bufToString(char)} at ${start}` 252 | ); 253 | } 254 | 255 | function isString(char) { 256 | return char === DOUBLE_QUOTE || char === SINGLE_QUOTE; 257 | } 258 | 259 | function isNumber(char) { 260 | return char === MINUS || (char >= DIGIT_0 && char <= DIGIT_9); 261 | } 262 | } 263 | 264 | parseObject(start: number, ctx?: ParseContext): number { 265 | let index = start + 1; // skip the start brace 266 | 267 | let length = 0; 268 | const keys = []; 269 | const nodes = []; 270 | 271 | while (index <= this.data.length) { 272 | if (index === this.data.length) { 273 | throw new Error(`parse object incomplete at end`); 274 | } 275 | index = this.skipIgnored(index); 276 | if (this.data[index] === BRACE_END) { 277 | index++; 278 | break; 279 | } 280 | const keyCtx = getKeyContext(length); 281 | index = this.parseString(index, keyCtx); 282 | 283 | if (keyCtx && ctx && ctx.objectKeys) { 284 | keys.push(keyCtx.value); 285 | } 286 | 287 | index = this.skipIgnored(index); 288 | if (this.data[index] !== COLON) { 289 | throw new Error( 290 | `parse object unexpected token ${bufToString( 291 | this.data[index] 292 | )} at ${index}. Expected :` 293 | ); 294 | } else { 295 | index++; 296 | } 297 | 298 | index = this.skipIgnored(index); 299 | 300 | let valueCtx: ParseContext = null; 301 | if ( 302 | keyCtx && 303 | ctx && 304 | (ctx.objectNodes || keyCtx.value === ctx.objectKey) 305 | ) { 306 | valueCtx = { 307 | path: ctx.path, 308 | nodeInfo: new BufferJsonNodeInfo(this, index, [ 309 | ...ctx.path, 310 | keyCtx.value as string 311 | ]) 312 | }; 313 | } 314 | 315 | index = this.parseValue(index, valueCtx); 316 | index = this.skipIgnored(index); 317 | 318 | if (valueCtx && ctx.objectNodes) { 319 | nodes.push(valueCtx.nodeInfo); 320 | } else if (valueCtx && ctx.objectKey !== undefined) { 321 | ctx.objectNodes = [valueCtx.nodeInfo]; 322 | return; 323 | } 324 | 325 | length++; 326 | 327 | if (this.data[index] === COMMA) { 328 | index++; 329 | } else if (this.data[index] !== BRACE_END) { 330 | throw new Error( 331 | `parse object unexpected token ${bufToString( 332 | this.data[index] 333 | )} at ${index}. Expected , or }` 334 | ); 335 | } 336 | } 337 | 338 | if (ctx && ctx.nodeInfo) { 339 | ctx.nodeInfo.type = 'object'; 340 | ctx.nodeInfo.length = length; 341 | ctx.nodeInfo.chars = index - start; 342 | } 343 | if (ctx && ctx.objectKeys) { 344 | ctx.objectKeys = keys; 345 | } 346 | if (ctx && ctx.objectNodes) { 347 | ctx.objectNodes = nodes; 348 | } 349 | 350 | function getKeyContext(keyIndex): ParseContext { 351 | if ( 352 | !ctx || 353 | (ctx.start && keyIndex < ctx.start) || 354 | (ctx.limit && keyIndex >= ctx.start + ctx.limit) 355 | ) { 356 | return null; 357 | } 358 | if ( 359 | ctx && 360 | (ctx.objectKeys || ctx.objectNodes || ctx.objectKey !== undefined) 361 | ) { 362 | return { 363 | path: ctx.path, 364 | value: null 365 | }; 366 | } 367 | return null; 368 | } 369 | 370 | return index; 371 | } 372 | 373 | parseArray(start: number, ctx?: ParseContext): number { 374 | let index = start + 1; // skip the start bracket 375 | let length = 0; 376 | while (index <= this.data.length) { 377 | if (index === this.data.length) { 378 | throw new Error(`parse array incomplete at end`); 379 | } 380 | index = this.skipIgnored(index); 381 | if (this.data[index] === BRACKET_END) { 382 | index++; 383 | break; 384 | } 385 | 386 | let valueCtx: ParseContext = null; 387 | if (isInRange(length) && ctx.arrayNodes) { 388 | valueCtx = { 389 | path: ctx.path, 390 | nodeInfo: new BufferJsonNodeInfo(this, index, [ 391 | ...ctx.path, 392 | length.toString() 393 | ]) 394 | }; 395 | } 396 | 397 | index = this.parseValue(index, valueCtx); 398 | 399 | if (valueCtx) { 400 | ctx.arrayNodes.push(valueCtx.nodeInfo); 401 | } 402 | 403 | index = this.skipIgnored(index); 404 | 405 | length++; 406 | 407 | if (this.data[index] === COMMA) { 408 | index++; 409 | } else if (this.data[index] !== BRACKET_END) { 410 | throw new Error( 411 | `parse array unexpected token ${bufToString( 412 | this.data[index] 413 | )} at ${index}. Expected , or ]` 414 | ); 415 | } 416 | } 417 | 418 | if (ctx && ctx.nodeInfo) { 419 | ctx.nodeInfo.type = 'array'; 420 | ctx.nodeInfo.length = length; 421 | ctx.nodeInfo.chars = index - start; 422 | } 423 | 424 | function isInRange(keyIndex): boolean { 425 | return !( 426 | !ctx || 427 | (ctx.start && keyIndex < ctx.start) || 428 | (ctx.limit && keyIndex >= ctx.start + ctx.limit) 429 | ); 430 | } 431 | 432 | return index; 433 | } 434 | 435 | parseString(start: number, ctx?: ParseContext): number { 436 | let index = start; 437 | const expect = 438 | this.data[index] === DOUBLE_QUOTE ? DOUBLE_QUOTE : SINGLE_QUOTE; 439 | let esc = false, 440 | length = 0; 441 | for (index++; index <= this.data.length; index++) { 442 | if (index === this.data.length) { 443 | throw new Error(`parse string incomplete at end`); 444 | } 445 | if (!esc && this.data[index] === expect) { 446 | index++; 447 | break; 448 | } 449 | if (this.data[index] === BACK_SLASH) { 450 | esc = !esc; 451 | } else { 452 | esc = false; 453 | } 454 | if (!esc) { 455 | length++; 456 | } 457 | } 458 | if (ctx && ctx.nodeInfo) { 459 | ctx.nodeInfo.type = 'string'; 460 | ctx.nodeInfo.length = length; 461 | ctx.nodeInfo.chars = index - start; 462 | } 463 | if (ctx && ctx.value !== undefined) { 464 | ctx.value = JSON.parse(bufToString(this.data.subarray(start, index))); 465 | } 466 | return index; 467 | } 468 | 469 | parseNumber(start: number, ctx?: ParseContext): number { 470 | let i = start; 471 | if (this.data[i] === MINUS) { 472 | i++; 473 | } 474 | i = this.parseDigits(i); 475 | if (this.data[i] === DOT) { 476 | i++; 477 | i = this.parseDigits(i); 478 | } 479 | if (this.data[i] === CHAR_E_HIGH || this.data[i] === CHAR_E_LOW) { 480 | i++; 481 | if (this.data[i] === PLUS || this.data[i] === MINUS) { 482 | i++; 483 | } 484 | i = this.parseDigits(i); 485 | } 486 | if (ctx && ctx.nodeInfo) { 487 | ctx.nodeInfo.type = 'number'; 488 | ctx.nodeInfo.chars = i - start; 489 | } 490 | if (ctx && ctx.value !== undefined) { 491 | ctx.value = JSON.parse(bufToString(this.data.subarray(start, i))); 492 | } 493 | return i; 494 | } 495 | 496 | private parseDigits(start: number): number { 497 | while (this.data[start] >= DIGIT_0 && this.data[start] <= DIGIT_9) { 498 | start++; 499 | } 500 | return start; 501 | } 502 | 503 | parseToken(start, chars, ctx?: ParseContext): number { 504 | let index = start; 505 | for (let i = 0; i < chars.length; i++) { 506 | if (this.data[index] !== chars[i]) { 507 | throw new Error( 508 | `Unexpected token ${bufToString( 509 | this.data[index] 510 | )} at ${index}. Expected ${bufToString(chars)}` 511 | ); 512 | } 513 | index++; 514 | } 515 | const token = bufToString(this.data.subarray(start, index)); 516 | if (ctx && ctx.nodeInfo) { 517 | if (token === 'null') { 518 | ctx.nodeInfo.type = 'null'; 519 | } else { 520 | ctx.nodeInfo.type = 'boolean'; 521 | } 522 | ctx.nodeInfo.chars = index - start; 523 | } 524 | if (ctx && ctx.value !== undefined) { 525 | ctx.value = JSON.parse(token); 526 | } 527 | return index; 528 | } 529 | 530 | parseNative(start, end) { 531 | return JSON.parse(bufToString(this.data.subarray(start, end))); 532 | } 533 | 534 | private skipIgnored(start: number) { 535 | for (let i = start; i < this.data.length; i++) { 536 | if (IGNORED.indexOf(this.data[i]) !== -1) { 537 | continue; 538 | } 539 | return i; 540 | } 541 | } 542 | } 543 | 544 | function bufToString(buf: number | number[] | Uint16Array) { 545 | if (typeof buf === 'number') { 546 | buf = [buf]; 547 | } 548 | return String.fromCharCode.apply(null, buf); 549 | } 550 | -------------------------------------------------------------------------------- /src/parser/js-parser.spec.ts: -------------------------------------------------------------------------------- 1 | import { JsJsonNodeInfo, JsParser } from './js-parser'; 2 | 3 | describe('JS JSON Parser', function() { 4 | it('should handle undefined input', function() { 5 | const instance = new JsParser(undefined); 6 | const info = instance.getRootNodeInfo(); 7 | expect(info).toBeNull(); 8 | }); 9 | 10 | it('should handle symbol', function() { 11 | const symbol = Symbol('a'); 12 | const instance = new JsParser(symbol); 13 | const info = instance.getRootNodeInfo(); 14 | expect(info.type).toEqual('symbol'); 15 | expect(info.getValue()).toEqual(symbol); 16 | }); 17 | 18 | it('should handle function', function() { 19 | const func = function() {}; 20 | const instance = new JsParser(func); 21 | const info = instance.getRootNodeInfo(); 22 | expect(info.type).toEqual('function'); 23 | expect(info.getValue()).toEqual(func); 24 | }); 25 | 26 | it('should handle empty object', function() { 27 | const instance = new JsParser({}); 28 | const info = instance.getRootNodeInfo(); 29 | expect(info.type).toEqual('object'); 30 | expect(info.length).toEqual(0); 31 | expect(info.path.length).toEqual(0); 32 | 33 | const keys = info.getObjectKeys(); 34 | expect(keys).toBeTruthy(); 35 | expect(keys.length).toEqual(0); 36 | 37 | const nodes = info.getObjectNodes(); 38 | expect(nodes).toBeTruthy(); 39 | expect(nodes.length).toEqual(0); 40 | }); 41 | 42 | it('should handle empty string', function() { 43 | const instance = new JsParser(''); 44 | const info = instance.getRootNodeInfo(); 45 | expect(info.type).toEqual('string'); 46 | expect(info.length).toEqual(0); 47 | expect(info.path.length).toEqual(0); 48 | expect(info.getValue()).toEqual(''); 49 | }); 50 | 51 | it('should handle number', function() { 52 | const instance = new JsParser(43246); 53 | const info = instance.getRootNodeInfo(); 54 | expect(info.type).toEqual('number'); 55 | expect(info.path.length).toEqual(0); 56 | expect(info.getValue()).toEqual(43246); 57 | }); 58 | 59 | it('should handle negative number', function() { 60 | const instance = new JsParser(-343); 61 | const info = instance.getRootNodeInfo(); 62 | expect(info.type).toEqual('number'); 63 | expect(info.path.length).toEqual(0); 64 | expect(info.getValue()).toEqual(-343); 65 | }); 66 | 67 | it('should handle negative number with e', function() { 68 | const instance = new JsParser(-34e3); 69 | const info = instance.getRootNodeInfo(); 70 | expect(info.type).toEqual('number'); 71 | expect(info.path.length).toEqual(0); 72 | expect(info.getValue()).toEqual(-34e3); 73 | }); 74 | 75 | it('should handle string', function() { 76 | const instance = new JsParser('abc'); 77 | const info = instance.getRootNodeInfo(); 78 | expect(info.type).toEqual('string'); 79 | expect(info.length).toEqual(3); 80 | expect(info.path.length).toEqual(0); 81 | expect(info.getValue()).toEqual('abc'); 82 | }); 83 | 84 | it('should handle string with special chars', function() { 85 | const instance = new JsParser('abc\nxy\\z'); 86 | const info = instance.getRootNodeInfo(); 87 | expect(info.type).toEqual('string'); 88 | expect(info.length).toEqual(8); 89 | expect(info.path.length).toEqual(0); 90 | expect(info.getValue()).toEqual('abc\nxy\\z'); 91 | }); 92 | 93 | it('should handle null', function() { 94 | const instance = new JsParser(null); 95 | const info = instance.getRootNodeInfo(); 96 | expect(info.type).toEqual('null'); 97 | expect(info.path.length).toEqual(0); 98 | expect(info.getValue()).toEqual(null); 99 | }); 100 | 101 | it('should handle true', function() { 102 | const instance = new JsParser(true); 103 | const info = instance.getRootNodeInfo(); 104 | expect(info.type).toEqual('boolean'); 105 | expect(info.path.length).toEqual(0); 106 | expect(info.getValue()).toEqual(true); 107 | }); 108 | 109 | it('should handle false', function() { 110 | const instance = new JsParser(false); 111 | const info = instance.getRootNodeInfo(); 112 | expect(info.type).toEqual('boolean'); 113 | expect(info.path.length).toEqual(0); 114 | expect(info.getValue()).toEqual(false); 115 | }); 116 | 117 | it('should handle object with one key', function() { 118 | const instance = new JsParser({ key1: 'value1' }); 119 | const info = instance.getRootNodeInfo(); 120 | expect(info.type).toEqual('object'); 121 | expect(info.length).toEqual(1); 122 | expect(info.path.length).toEqual(0); 123 | 124 | const keys = info.getObjectKeys(); 125 | expect(keys).toBeTruthy(); 126 | expect(keys.length).toEqual(1); 127 | expect(keys[0]).toEqual('key1'); 128 | 129 | const nodes = info.getObjectNodes(); 130 | expect(nodes).toBeTruthy(); 131 | expect(nodes.length).toEqual(1); 132 | expectStringNode(nodes[0], 'value1', ['key1']); 133 | }); 134 | 135 | it('should handle object with multi keys', function() { 136 | const instance = new JsParser({ key1: 'value1', key2: [] }); 137 | const info = instance.getRootNodeInfo(); 138 | expect(info.type).toEqual('object'); 139 | expect(info.length).toEqual(2); 140 | expect(info.path.length).toEqual(0); 141 | 142 | const keys = info.getObjectKeys(); 143 | expect(keys).toBeTruthy(); 144 | expect(keys.length).toEqual(2); 145 | expect(keys[0]).toEqual('key1'); 146 | expect(keys[1]).toEqual('key2'); 147 | 148 | const nodes = info.getObjectNodes(); 149 | expect(nodes).toBeTruthy(); 150 | expect(nodes.length).toEqual(2); 151 | 152 | let node = nodes[0]; 153 | expectStringNode(nodes[0], 'value1', ['key1']); 154 | 155 | node = nodes[1]; 156 | expect(node.type).toEqual('array'); 157 | expect(node.length).toEqual(0); 158 | expect(node.path.length).toEqual(1); 159 | expect(node.path[0]).toEqual('key2'); 160 | 161 | node = info.getByKey('key1'); 162 | expectStringNode(node, 'value1', ['key1']); 163 | }); 164 | 165 | it('should handle empty array', function() { 166 | const instance = new JsParser([]); 167 | const info = instance.getRootNodeInfo(); 168 | expect(info.type).toEqual('array'); 169 | expect(info.length).toEqual(0); 170 | expect(info.path.length).toEqual(0); 171 | 172 | const nodes = info.getArrayNodes(); 173 | expect(nodes.length).toEqual(0); 174 | }); 175 | 176 | it('should handle array with elements', function() { 177 | const instance = new JsParser([0, 'ac']); 178 | const info = instance.getRootNodeInfo(); 179 | expect(info.type).toEqual('array'); 180 | expect(info.length).toEqual(2); 181 | expect(info.path.length).toEqual(0); 182 | 183 | const nodes = info.getArrayNodes(); 184 | expect(nodes.length).toEqual(2); 185 | 186 | expectNumberNode(nodes[0], 0, ['0']); 187 | expectStringNode(nodes[1], 'ac', ['1']); 188 | }); 189 | 190 | it('should handle array with tokens', function() { 191 | const instance = new JsParser([true, false, null]); 192 | const info = instance.getRootNodeInfo(); 193 | expect(info.type).toEqual('array'); 194 | expect(info.length).toEqual(3); 195 | expect(info.path.length).toEqual(0); 196 | 197 | const nodes = info.getArrayNodes(); 198 | expect(nodes.length).toEqual(3); 199 | 200 | expect(nodes[0].type).toEqual('boolean'); 201 | expect(nodes[0].getValue()).toEqual(true); 202 | expect(nodes[0].path.length).toEqual(1); 203 | 204 | expect(nodes[1].type).toEqual('boolean'); 205 | expect(nodes[1].getValue()).toEqual(false); 206 | expect(nodes[1].path.length).toEqual(1); 207 | 208 | expect(nodes[2].type).toEqual('null'); 209 | expect(nodes[2].getValue()).toEqual(null); 210 | expect(nodes[2].path.length).toEqual(1); 211 | }); 212 | 213 | it('should handle array pagination', function() { 214 | const instance = new JsParser(['a', 'b', 'c', 'd', 'e']); 215 | const info = instance.getRootNodeInfo(); 216 | expect(info.type).toEqual('array'); 217 | expect(info.length).toEqual(5); 218 | 219 | let nodes = info.getArrayNodes(1, 2); 220 | expect(nodes.length).toEqual(2); 221 | expectStringNode(nodes[0], 'b', ['1']); 222 | expectStringNode(nodes[1], 'c', ['2']); 223 | 224 | nodes = info.getArrayNodes(0, 1); 225 | expect(nodes.length).toEqual(1); 226 | expectStringNode(nodes[0], 'a', ['0']); 227 | 228 | nodes = info.getArrayNodes(3, 4); 229 | expect(nodes.length).toEqual(2); 230 | expectStringNode(nodes[0], 'd', ['3']); 231 | expectStringNode(nodes[1], 'e', ['4']); 232 | 233 | const node = info.getByIndex(1); 234 | expectStringNode(node, 'b', ['1']); 235 | }); 236 | 237 | it('should handle object pagination', function() { 238 | const instance = new JsParser({ a: 'A', b: 'B', c: 'C', d: 'D' }); 239 | const info = instance.getRootNodeInfo(); 240 | expect(info.type).toEqual('object'); 241 | expect(info.length).toEqual(4); 242 | 243 | let keys = info.getObjectKeys(1, 2); 244 | expect(keys.length).toEqual(2); 245 | expect(keys[0]).toEqual('b'); 246 | expect(keys[1]).toEqual('c'); 247 | 248 | keys = info.getObjectKeys(3, 5); 249 | expect(keys.length).toEqual(1); 250 | expect(keys[0]).toEqual('d'); 251 | 252 | const nodes = info.getObjectNodes(2, 5); 253 | expect(nodes.length).toEqual(2); 254 | expectStringNode(nodes[0], 'C', ['c']); 255 | expectStringNode(nodes[1], 'D', ['d']); 256 | 257 | const node = info.getByIndex(1); 258 | expectStringNode(node, 'B', ['b']); 259 | }); 260 | 261 | it('should handle nested objects', function() { 262 | const instance = new JsParser({ a: { b: { c: true, d: 'D' } } }); 263 | const info = instance.getRootNodeInfo(); 264 | expect(info.type).toEqual('object'); 265 | expect(info.length).toEqual(1); 266 | 267 | let nodes = info.getObjectNodes(); 268 | let node = nodes[0]; 269 | expect(node.type).toEqual('object'); 270 | expect(node.length).toEqual(1); 271 | expect(node.path.length).toEqual(1); 272 | expect(node.path[0]).toEqual('a'); 273 | 274 | nodes = node.getObjectNodes(); 275 | node = nodes[0]; 276 | expect(node.type).toEqual('object'); 277 | expect(node.length).toEqual(2); 278 | expect(node.path.length).toEqual(2); 279 | expect(node.path[0]).toEqual('a'); 280 | expect(node.path[1]).toEqual('b'); 281 | 282 | nodes = node.getObjectNodes(); 283 | node = nodes[0]; 284 | expect(node.type).toEqual('boolean'); 285 | expect(node.path.length).toEqual(3); 286 | expect(node.path[0]).toEqual('a'); 287 | expect(node.path[1]).toEqual('b'); 288 | expect(node.path[2]).toEqual('c'); 289 | expect(node.getValue()).toEqual(true); 290 | 291 | node = info.getByPath('a.b.d'.split('.')); 292 | expectStringNode(node, 'D', ['a', 'b', 'd']); 293 | 294 | expect(info.getByPath('a.b.e'.split('.'))).toBeUndefined(); 295 | expect(info.getByPath([])).toEqual(info); 296 | }); 297 | }); 298 | 299 | function expectStringNode(node: JsJsonNodeInfo, value: string, path: string[]) { 300 | expect(node.type).toEqual('string'); 301 | expect(node.length).toEqual(value.length); 302 | expect(node.path.length).toEqual(path.length); 303 | for (let i = 0; i < path.length; i++) { 304 | expect(node.path[i]).toEqual(path[i]); 305 | } 306 | expect(node.getValue()).toEqual(value); 307 | } 308 | 309 | function expectNumberNode(node: JsJsonNodeInfo, value: number, path: string[]) { 310 | expect(node.type).toEqual('number'); 311 | expect(node.path.length).toEqual(path.length); 312 | for (let i = 0; i < path.length; i++) { 313 | expect(node.path[i]).toEqual(path[i]); 314 | } 315 | expect(node.getValue()).toEqual(value); 316 | } 317 | -------------------------------------------------------------------------------- /src/parser/js-parser.ts: -------------------------------------------------------------------------------- 1 | import { JsonNodeInfo, NodeType } from './json-node-info'; 2 | import { assertStartLimit } from '../helpers/utils'; 3 | 4 | export class JsJsonNodeInfo implements JsonNodeInfo { 5 | public type: NodeType; 6 | public path: string[] = []; 7 | public length?: number; // in case of array, object, string 8 | private readonly ref: any; 9 | 10 | constructor(ref: any, path: string[]) { 11 | this.ref = ref; 12 | this.path = path; 13 | const jsType = typeof ref; 14 | if (jsType === 'undefined') { 15 | this.type = 'undefined'; 16 | } 17 | if (jsType === 'symbol') { 18 | this.type = 'symbol'; 19 | } 20 | if (jsType === 'function') { 21 | this.type = 'function'; 22 | } 23 | if (jsType === 'object' && ref === null) { 24 | this.type = 'null'; 25 | } else if (jsType === 'object' && Array.isArray(ref)) { 26 | this.type = 'array'; 27 | } else { 28 | this.type = jsType; 29 | } 30 | 31 | if (this.type === 'object') { 32 | this.length = Object.keys(ref).length; 33 | } 34 | if (this.type === 'array' || this.type === 'string') { 35 | this.length = ref.length; 36 | } 37 | } 38 | 39 | /** 40 | * Returns the list of keys in case of an object for the defined range 41 | * @param {number} start 42 | * @param {number} limit 43 | */ 44 | public getObjectKeys(start = 0, limit?: number): string[] { 45 | if (this.type !== 'object') { 46 | throw new Error(`Unsupported method on non-object ${this.type}`); 47 | } 48 | assertStartLimit(start, limit); 49 | const keys = Object.keys(this.ref); 50 | if (limit) { 51 | return keys.slice(start, start + limit); 52 | } 53 | return keys.slice(start); 54 | } 55 | 56 | /** 57 | * Return the NodeInfo at the defined position. 58 | * Use the index from getObjectKeys 59 | * @param index 60 | */ 61 | public getByIndex(index: number): JsJsonNodeInfo { 62 | if (this.type === 'object') { 63 | const nodes = this.getObjectNodes(index, 1); 64 | if (nodes.length) { 65 | return nodes[0]; 66 | } 67 | } 68 | if (this.type === 'array') { 69 | const nodes = this.getArrayNodes(index, 1); 70 | if (nodes.length) { 71 | return nodes[0]; 72 | } 73 | } 74 | return undefined; 75 | } 76 | 77 | /** 78 | * Return the NodeInfo for the specified key 79 | * Use the index from getObjectKeys 80 | * @param key 81 | */ 82 | public getByKey(key: string): JsJsonNodeInfo { 83 | if (this.type === 'object' && this.ref.hasOwnProperty(key)) { 84 | return new JsJsonNodeInfo(this.ref[key], [...this.path, key]); 85 | } 86 | if (this.type === 'array') { 87 | return this.getByIndex(parseInt(key)); 88 | } 89 | return undefined; 90 | } 91 | 92 | /** 93 | * Find the information for a given path 94 | * @param {string[]} path 95 | */ 96 | public getByPath(path: string[]): JsJsonNodeInfo { 97 | if (!path) { 98 | return undefined; 99 | } 100 | if (!path.length) { 101 | return this; 102 | } 103 | const p = path.slice(); 104 | let key: string; 105 | let node: JsJsonNodeInfo = this; 106 | while ((key = p.shift()) !== undefined && node) { 107 | node = node.getByKey(key); 108 | } 109 | return node; 110 | } 111 | 112 | /** 113 | * Returns a list with the NodeInfo objects for the defined range 114 | * @param {number} start 115 | * @param {number} limit 116 | */ 117 | public getObjectNodes(start = 0, limit?: number): JsJsonNodeInfo[] { 118 | if (this.type !== 'object') { 119 | throw new Error(`Unsupported method on non-object ${this.type}`); 120 | } 121 | assertStartLimit(start, limit); 122 | const nodes = {}; 123 | return this.getObjectKeys(start, limit).map( 124 | key => new JsJsonNodeInfo(this.ref[key], [...this.path, key]) 125 | ); 126 | } 127 | 128 | /** 129 | * Returns a list of NodeInfo for the defined range 130 | * @param {number} start 131 | * @param {number} limit 132 | */ 133 | public getArrayNodes(start = 0, limit?: number): JsJsonNodeInfo[] { 134 | if (this.type !== 'array') { 135 | throw new Error(`Unsupported method on non-array ${this.type}`); 136 | } 137 | assertStartLimit(start, limit); 138 | const elements = limit 139 | ? this.ref.slice(start, start + limit) 140 | : this.ref.slice(start); 141 | return elements.map( 142 | (ref, i) => new JsJsonNodeInfo(ref, [...this.path, String(start + i)]) 143 | ); 144 | } 145 | 146 | /** 147 | * Get the natively parsed value 148 | */ 149 | public getValue(): any { 150 | return this.ref; 151 | } 152 | } 153 | 154 | export class JsParser { 155 | data: any; 156 | 157 | constructor(data: any) { 158 | this.data = data; 159 | } 160 | 161 | getRootNodeInfo(): JsJsonNodeInfo { 162 | if (this.data === undefined) { 163 | return null; 164 | } 165 | return new JsJsonNodeInfo(this.data, []); 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /src/parser/json-node-info.ts: -------------------------------------------------------------------------------- 1 | export type NodeType = 2 | | 'string' 3 | | 'number' 4 | | 'array' 5 | | 'object' 6 | | 'boolean' 7 | | 'null' 8 | | 'undefined' 9 | | string; 10 | 11 | export interface JsonNodeInfoBase { 12 | readonly type: NodeType; 13 | readonly path: string[]; 14 | readonly length?: number; // in case of array, object, string 15 | } 16 | 17 | export interface JsonNodeInfo extends JsonNodeInfoBase { 18 | /** 19 | * Returns the list of keys in case of an object for the defined range 20 | * @param {number} start 21 | * @param {number} limit 22 | */ 23 | getObjectKeys(start?: number, limit?: number): string[]; 24 | 25 | /** 26 | * Return the NodeInfo at the defined position. 27 | * Use the index from getObjectKeys 28 | * @param index 29 | */ 30 | getByIndex(index: number): JsonNodeInfo; 31 | 32 | /** 33 | * Return the NodeInfo for the specified key 34 | * Use the index from getObjectKeys 35 | * @param key 36 | */ 37 | getByKey(key: string): JsonNodeInfo; 38 | 39 | /** 40 | * Find the information for a given path 41 | * @param {string[]} path 42 | * @returns {BufferJsonNodeInfo} 43 | */ 44 | getByPath(path: string[]): JsonNodeInfo; 45 | 46 | /** 47 | * Returns a map with the NodeInfo objects for the defined range 48 | * @param {number} start 49 | * @param {number} limit 50 | */ 51 | getObjectNodes(start?: number, limit?: number): JsonNodeInfo[]; 52 | 53 | /** 54 | * Returns a list of NodeInfo for the defined range 55 | * @param {number} start 56 | * @param {number} limit 57 | */ 58 | getArrayNodes(start?: number, limit?: number): JsonNodeInfo[]; 59 | 60 | /** 61 | * Get the natively parsed value 62 | */ 63 | getValue(): any; 64 | } 65 | -------------------------------------------------------------------------------- /src/parser/json-node-search.spec.ts: -------------------------------------------------------------------------------- 1 | import { BufferJsonParser } from './buffer-json-parser'; 2 | import { searchJsonNodes } from './json-node-search'; 3 | import { TreeSearchMatch } from '../'; 4 | 5 | describe('JSON Node Search', function() { 6 | it('should find results', function() { 7 | const instance = new BufferJsonParser( 8 | '{"a": "A", "b": "B", "c": "C", "d": "D"}' 9 | ); 10 | const rootNode = instance.getRootNodeInfo(); 11 | const pattern = /b/; 12 | 13 | let results = searchJsonNodes(rootNode, pattern); 14 | expect(results.length).toEqual(1); 15 | expectKeyResult(results[0], ['b'], 0, 1); 16 | 17 | results = searchJsonNodes(rootNode, /D/); 18 | expect(results.length).toEqual(1); 19 | expectValueResult(results[0], ['d'], 0, 1); 20 | }); 21 | 22 | it('should find multiple results', function() { 23 | const instance = new BufferJsonParser( 24 | '{"hello": "hello world, is a great world","test": [0,"old world",{"worldgame": true}]}' 25 | ); 26 | const rootNode = instance.getRootNodeInfo(); 27 | const pattern = /world/; 28 | 29 | let results = searchJsonNodes(rootNode, pattern); 30 | expect(results.length).toEqual(4); 31 | expectValueResult(results[0], ['hello'], 6, 5); 32 | expectValueResult(results[1], ['hello'], 24, 5); 33 | expectValueResult(results[2], ['test', '1'], 4, 5); 34 | expectKeyResult(results[3], ['test', '2', 'worldgame'], 0, 5); 35 | }); 36 | }); 37 | 38 | function expectKeyResult( 39 | result: TreeSearchMatch, 40 | path: string[], 41 | index: number, 42 | length: number 43 | ) { 44 | expect(result.path).toEqual(path); 45 | expect(result.key).toEqual(index); 46 | expect(result.value).toBeUndefined(); 47 | expect(result.length).toEqual(length); 48 | } 49 | function expectValueResult( 50 | result: TreeSearchMatch, 51 | path: string[], 52 | index: number, 53 | length: number 54 | ) { 55 | expect(result.path).toEqual(path); 56 | expect(result.key).toBeUndefined(); 57 | expect(result.value).toEqual(index); 58 | expect(result.length).toEqual(length); 59 | } 60 | -------------------------------------------------------------------------------- /src/parser/json-node-search.ts: -------------------------------------------------------------------------------- 1 | import { JsonNodeInfo } from './json-node-info'; 2 | import { 3 | TreeSearchAreaOption, 4 | TreeSearchMatch 5 | } from '../model/big-json-viewer.model'; 6 | 7 | // search only in values 8 | 9 | export function searchJsonNodes( 10 | node: JsonNodeInfo, 11 | pattern: RegExp, 12 | searchArea: TreeSearchAreaOption = 'all' 13 | ): TreeSearchMatch[] { 14 | pattern = ensureGlobal(pattern); 15 | const results: TreeSearchMatch[] = []; 16 | if (node.path.length && (searchArea === 'all' || searchArea === 'keys')) { 17 | forEachMatchFromString( 18 | pattern, 19 | node.path[node.path.length - 1], 20 | (index, length) => { 21 | results.push({ path: node.path, key: index, length: length }); 22 | } 23 | ); 24 | } 25 | if (node.type === 'object') { 26 | node.getObjectNodes().forEach(subNode => { 27 | results.push(...searchJsonNodes(subNode, pattern, searchArea)); 28 | }); 29 | } else if (node.type === 'array') { 30 | node.getArrayNodes().forEach(subNode => { 31 | results.push(...searchJsonNodes(subNode, pattern, searchArea)); 32 | }); 33 | } else if (searchArea === 'all' || searchArea === 'values') { 34 | forEachMatchFromString( 35 | pattern, 36 | String(node.getValue()), 37 | (index, length) => { 38 | results.push({ path: node.path, value: index, length: length }); 39 | } 40 | ); 41 | } 42 | 43 | return results; 44 | } 45 | 46 | export function forEachMatchFromString( 47 | pattern: RegExp, 48 | subject: string, 49 | callback: (i: number, length: number) => void 50 | ) { 51 | pattern = ensureGlobal(pattern); 52 | pattern.lastIndex = 0; 53 | let match = null; 54 | while ((match = pattern.exec(subject)) !== null) { 55 | callback(match.index, match[0].length); 56 | } 57 | pattern.lastIndex = 0; 58 | } 59 | 60 | function ensureGlobal(pattern: RegExp): RegExp { 61 | if (!pattern.global) { 62 | const flags = 63 | 'g' + (pattern.ignoreCase ? 'i' : '') + (pattern.multiline ? 'm' : ''); 64 | return new RegExp(pattern.source, flags); 65 | } 66 | return pattern; 67 | } 68 | -------------------------------------------------------------------------------- /src/worker/big-json-viewer.worker.inline.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | export function initWorker() { 3 | const blob = new Blob(["parcelRequire=function(e,r,n,t){var i=\"function\"==typeof parcelRequire&&parcelRequire,o=\"function\"==typeof require&&require;function u(n,t){if(!r[n]){if(!e[n]){var f=\"function\"==typeof parcelRequire&&parcelRequire;if(!t&&f)return f(n,!0);if(i)return i(n,!0);if(o&&\"string\"==typeof n)return o(n);var c=new Error(\"Cannot find module '\"+n+\"'\");throw c.code=\"MODULE_NOT_FOUND\",c}p.resolve=function(r){return e[n][1][r]||r},p.cache={};var l=r[n]=new u.Module(n);e[n][0].call(l.exports,p,l,l.exports,this)}return r[n].exports;function p(e){return u(p.resolve(e))}}u.isParcelRequire=!0,u.Module=function(e){this.id=e,this.bundle=u,this.exports={}},u.modules=e,u.cache=r,u.parent=i,u.register=function(r,n){e[r]=[function(e,r){r.exports=n},{}]};for(var f=0;f=w&&t<=C}(n))return this.parseNumber(t,r);if(n===e)return this.parseObject(t,r);if(n===a)return this.parseArray(t,r);if(n===x[0])return this.parseToken(t,x,r);if(n===k[0])return this.parseToken(t,k,r);if(n===m[0])return this.parseToken(t,m,r);if(o)throw new Error(\"parse value unknown token \"+K(n)+\" at \"+t)},t.prototype.parseObject=function(t,e){for(var a=t+1,o=0,s=[],h=[];a<=this.data.length;){if(a===this.data.length)throw new Error(\"parse object incomplete at end\");if(a=this.skipIgnored(a),this.data[a]===r){a++;break}var d=c(o);if(a=this.parseString(a,d),d&&e&&e.objectKeys&&s.push(d.value),a=this.skipIgnored(a),this.data[a]!==n)throw new Error(\"parse object unexpected token \"+K(this.data[a])+\" at \"+a+\". Expected :\");a++,a=this.skipIgnored(a);var p=null;if(d&&e&&(e.objectNodes||d.value===e.objectKey)&&(p={path:e.path,nodeInfo:new E(this,a,e.path.concat([d.value]))}),a=this.parseValue(a,p),a=this.skipIgnored(a),p&&e.objectNodes)h.push(p.nodeInfo);else if(p&&void 0!==e.objectKey)return void(e.objectNodes=[p.nodeInfo]);if(o++,this.data[a]===i)a++;else if(this.data[a]!==r)throw new Error(\"parse object unexpected token \"+K(this.data[a])+\" at \"+a+\". Expected , or }\")}function c(t){return!e||e.start&&t=e.start+e.limit?null:e&&(e.objectKeys||e.objectNodes||void 0!==e.objectKey)?{path:e.path,value:null}:null}return e&&e.nodeInfo&&(e.nodeInfo.type=\"object\",e.nodeInfo.length=o,e.nodeInfo.chars=a-t),e&&e.objectKeys&&(e.objectKeys=s),e&&e.objectNodes&&(e.objectNodes=h),a},t.prototype.parseArray=function(t,e){for(var r,a=t+1,n=0;a<=this.data.length;){if(a===this.data.length)throw new Error(\"parse array incomplete at end\");if(a=this.skipIgnored(a),this.data[a]===o){a++;break}var s=null;if(r=n,!e||e.start&&r=e.start+e.limit||!e.arrayNodes||(s={path:e.path,nodeInfo:new E(this,a,e.path.concat([n.toString()]))}),a=this.parseValue(a,s),s&&e.arrayNodes.push(s.nodeInfo),a=this.skipIgnored(a),n++,this.data[a]===i)a++;else if(this.data[a]!==o)throw new Error(\"parse array unexpected token \"+K(this.data[a])+\" at \"+a+\". Expected , or ]\")}return e&&e.nodeInfo&&(e.nodeInfo.type=\"array\",e.nodeInfo.length=n,e.nodeInfo.chars=a-t),a},t.prototype.parseString=function(t,e){var r=t,a=this.data[r]===s?s:h,o=!1,n=0;for(r++;r<=this.data.length;r++){if(r===this.data.length)throw new Error(\"parse string incomplete at end\");if(!o&&this.data[r]===a){r++;break}(o=this.data[r]===l&&!o)||n++}return e&&e.nodeInfo&&(e.nodeInfo.type=\"string\",e.nodeInfo.length=n,e.nodeInfo.chars=r-t),e&&void 0!==e.value&&(e.value=JSON.parse(K(this.data.subarray(t,r)))),r},t.prototype.parseNumber=function(t,e){var r=t;return this.data[r]===g&&r++,r=this.parseDigits(r),this.data[r]===j&&(r++,r=this.parseDigits(r)),this.data[r]!==A&&this.data[r]!==I||(r++,this.data[r]!==v&&this.data[r]!==g||r++,r=this.parseDigits(r)),e&&e.nodeInfo&&(e.nodeInfo.type=\"number\",e.nodeInfo.chars=r-t),e&&void 0!==e.value&&(e.value=JSON.parse(K(this.data.subarray(t,r)))),r},t.prototype.parseDigits=function(t){for(;this.data[t]>=w&&this.data[t]<=C;)t++;return t},t.prototype.parseToken=function(t,e,r){for(var a=t,o=0;o { 42 | throw e; 43 | }); 44 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es5", 5 | "noImplicitAny": false, 6 | "moduleResolution": "node", 7 | "sourceMap": true, 8 | "outDir": "dist", 9 | "baseUrl": ".", 10 | "declaration": true, 11 | "lib": ["es2015", "es5", "es6", "dom"], 12 | "paths": { 13 | "*": ["node_modules/*"] 14 | } 15 | }, 16 | "include": ["src/**/*"], 17 | "exclude": ["src/**/*.spec.ts", "src/browser-api.ts"] 18 | } 19 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "max-line-length": [true, 140], 4 | "no-inferrable-types": true, 5 | "class-name": true, 6 | "comment-format": [true, "check-space"], 7 | "indent": [true, "spaces"], 8 | "eofline": true, 9 | "no-duplicate-variable": true, 10 | "no-eval": true, 11 | "no-arg": true, 12 | "no-internal-module": true, 13 | "no-trailing-whitespace": true, 14 | "no-bitwise": true, 15 | "no-unused-expression": true, 16 | "no-var-keyword": true, 17 | "one-line": [ 18 | true, 19 | "check-catch", 20 | "check-else", 21 | "check-open-brace", 22 | "check-whitespace" 23 | ], 24 | "quotemark": [true, "single", "avoid-escape"], 25 | "semicolon": [true, "always"], 26 | "typedef-whitespace": [ 27 | true, 28 | { 29 | "call-signature": "nospace", 30 | "index-signature": "nospace", 31 | "parameter": "nospace", 32 | "property-declaration": "nospace", 33 | "variable-declaration": "nospace" 34 | } 35 | ], 36 | "curly": true, 37 | "variable-name": [ 38 | true, 39 | "ban-keywords", 40 | "check-format", 41 | "allow-leading-underscore", 42 | "allow-pascal-case" 43 | ], 44 | "whitespace": [ 45 | true, 46 | "check-branch", 47 | "check-decl", 48 | "check-operator", 49 | "check-separator", 50 | "check-type" 51 | ] 52 | } 53 | } 54 | --------------------------------------------------------------------------------