├── .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 | [](https://www.npmjs.com/package/big-json-viewer)
4 | [](https://travis-ci.org/dhcode/big-json-viewer)
5 | [](https://codecov.io/gh/dhcode/big-json-viewer)
6 | [](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 |
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
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 |
--------------------------------------------------------------------------------