├── .npmrc
├── cjs
├── package.json
└── index.js
├── test
├── package.json
├── index.html
├── strings.txt
├── numbers.txt
├── bench.js
├── index.js
└── data.json
├── flatted.jpg
├── .gitignore
├── .npmignore
├── rollup
├── esm.config.js
├── es.config.js
└── babel.config.js
├── types
└── index.d.ts
├── tsconfig.json
├── .github
├── FUNDING.yml
└── workflows
│ ├── publish.yml
│ └── node.js.yml
├── jsr.json
├── LICENSE
├── esm.js
├── es.js
├── min.js
├── python
├── test.py
└── flatted.py
├── package.json
├── SPECS.md
├── esm
└── index.js
├── php
├── test.php
└── flatted.php
├── README.md
└── index.js
/.npmrc:
--------------------------------------------------------------------------------
1 | package-lock=true
2 |
--------------------------------------------------------------------------------
/cjs/package.json:
--------------------------------------------------------------------------------
1 | {"type":"commonjs"}
--------------------------------------------------------------------------------
/test/package.json:
--------------------------------------------------------------------------------
1 | {"type":"commonjs"}
--------------------------------------------------------------------------------
/flatted.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/WebReflection/flatted/HEAD/flatted.jpg
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | .nyc_output
3 | node_modules/
4 | coverage/
5 | __pycache__
6 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | .github
3 | .nyc_output
4 | .travis.yml
5 | __pycache__/
6 | node_modules/
7 | coverage/
8 | rollup/
9 | test/
10 | php/test.php
11 | flatted.jpg
12 | package-lock.json
13 | SPECS.md
14 | tsconfig.json
15 | jsr.json
16 |
--------------------------------------------------------------------------------
/rollup/esm.config.js:
--------------------------------------------------------------------------------
1 | import terser from '@rollup/plugin-terser';
2 |
3 | export default {
4 | input: './esm/index.js',
5 | plugins: [
6 | terser()
7 | ],
8 | output: {
9 | file: './esm.js',
10 | format: 'module'
11 | }
12 | };
13 |
--------------------------------------------------------------------------------
/rollup/es.config.js:
--------------------------------------------------------------------------------
1 | import terser from '@rollup/plugin-terser';
2 |
3 | export default {
4 | input: './esm/index.js',
5 | plugins: [
6 | terser()
7 | ],
8 | output: {
9 | esModule: false,
10 | exports: 'named',
11 | file: './es.js',
12 | format: 'iife',
13 | name: 'Flatted'
14 | }
15 | };
16 |
--------------------------------------------------------------------------------
/rollup/babel.config.js:
--------------------------------------------------------------------------------
1 | import babel from '@rollup/plugin-babel';
2 |
3 | export default {
4 | input: './esm/index.js',
5 | plugins: [
6 | babel({presets: ['@babel/preset-env']})
7 | ],
8 | output: {
9 | esModule: false,
10 | exports: 'named',
11 | file: './index.js',
12 | format: 'iife',
13 | name: 'Flatted'
14 | }
15 | };
16 |
--------------------------------------------------------------------------------
/types/index.d.ts:
--------------------------------------------------------------------------------
1 | export function parse(text: string, reviver?: (this: any, key: string, value: any) => any): any;
2 | export function stringify(value: any, replacer?: (string | number)[] | ((this: any, key: string, value: any) => any), space?: string | number | undefined): string;
3 | export function toJSON(value: any): any;
4 | export function fromJSON(value: any): any;
5 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "module": "NodeNext",
4 | "target": "esnext",
5 | "moduleResolution": "nodenext",
6 | "allowJs": true,
7 | "declaration": true,
8 | "emitDeclarationOnly": true,
9 | "declarationDir": "types"
10 | },
11 | "include": [
12 | "esm/index.js"
13 | ]
14 | }
15 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
4 | patreon: # not working due missing www.
5 | open_collective: #
6 | ko_fi: # Replace with a single Ko-fi username
7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
8 | custom: https://www.patreon.com/webreflection
9 |
--------------------------------------------------------------------------------
/jsr.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@wr/flatted",
3 | "version": "3.3.1",
4 | "exports": "./esm/index.js",
5 | "exclude": [
6 | ".github/*",
7 | "./cjs/*",
8 | "./coverage/*",
9 | "./node_modules/*",
10 | "./rollup/*",
11 | "./test/*",
12 | ".gitignore",
13 | ".npmignore",
14 | ".npmrc",
15 | "es.js",
16 | "index.js",
17 | "min.js",
18 | "package-lock.json",
19 | "tsconfig.json"
20 | ]
21 | }
22 |
--------------------------------------------------------------------------------
/.github/workflows/publish.yml:
--------------------------------------------------------------------------------
1 | name: Publish
2 | on:
3 | push:
4 | branches:
5 | - main
6 |
7 | jobs:
8 | publish:
9 | runs-on: ubuntu-latest
10 |
11 | permissions:
12 | contents: read
13 | id-token: write
14 |
15 | steps:
16 | - uses: actions/checkout@v4
17 |
18 | - name: Install Deno
19 | uses: denoland/setup-deno@v1
20 |
21 | - name: Publish package
22 | run: deno publish --config jsr.json
23 |
--------------------------------------------------------------------------------
/test/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | flatted
7 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | ISC License
2 |
3 | Copyright (c) 2018-2020, Andrea Giammarchi, @WebReflection
4 |
5 | Permission to use, copy, modify, and/or distribute this software for any
6 | purpose with or without fee is hereby granted, provided that the above
7 | copyright notice and this permission notice appear in all copies.
8 |
9 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
10 | REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
11 | AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
12 | INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
13 | LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
14 | OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
15 | PERFORMANCE OF THIS SOFTWARE.
16 |
--------------------------------------------------------------------------------
/.github/workflows/node.js.yml:
--------------------------------------------------------------------------------
1 | # This workflow will do a clean install of node dependencies, cache/restore them, build the source code and run tests across different versions of node
2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
3 |
4 | name: build
5 |
6 | on: [push, pull_request]
7 |
8 | jobs:
9 | build:
10 |
11 | runs-on: ubuntu-latest
12 |
13 | strategy:
14 | matrix:
15 | node-version: [24]
16 |
17 | steps:
18 | - uses: actions/checkout@v4
19 | - name: Use Node.js ${{ matrix.node-version }}
20 | uses: actions/setup-node@v4
21 | with:
22 | node-version: ${{ matrix.node-version }}
23 | cache: 'npm'
24 | - run: npm ci
25 | - run: npm run build --if-present
26 | - run: npm test
27 | - run: npm run coverage --if-present
28 | - name: Coveralls
29 | uses: coverallsapp/github-action@master
30 | with:
31 | github-token: ${{ secrets.GITHUB_TOKEN }}
32 |
--------------------------------------------------------------------------------
/esm.js:
--------------------------------------------------------------------------------
1 | const{parse:t,stringify:e}=JSON,{keys:n}=Object,l=String,o="string",r={},s="object",c=(t,e)=>e,a=t=>t instanceof l?l(t):t,f=(t,e)=>typeof e===o?new l(e):e,i=(t,e,o,c)=>{const a=[];for(let f=n(o),{length:i}=f,p=0;p{const o=l(e.push(n)-1);return t.set(n,o),o},u=(e,n)=>{const l=t(e,f).map(a),o=l[0],r=n||c,p=typeof o===s&&o?i(l,new Set,o,r):o;return r.call({"":p},"",p)},h=(t,n,l)=>{const r=n&&typeof n===s?(t,e)=>""===t||-1t(h(e)),g=t=>u(e(t));export{g as fromJSON,u as parse,h as stringify,y as toJSON};
2 |
--------------------------------------------------------------------------------
/es.js:
--------------------------------------------------------------------------------
1 | self.Flatted=function(t){"use strict";const{parse:e,stringify:n}=JSON,{keys:r}=Object,s=String,o="string",c={},l="object",a=(t,e)=>e,f=t=>t instanceof s?s(t):t,i=(t,e)=>typeof e===o?new s(e):e,u=(t,e,n,o)=>{const a=[];for(let f=r(n),{length:i}=f,u=0;u{const r=s(e.push(n)-1);return t.set(n,r),r},y=(t,n)=>{const r=e(t,i).map(f),s=r[0],o=n||a,c=typeof s===l&&s?u(r,new Set,s,o):s;return o.call({"":c},"",c)},g=(t,e,r)=>{const s=e&&typeof e===l?(t,n)=>""===t||-1y(n(t)),t.parse=y,t.stringify=g,t.toJSON=t=>e(g(t)),t}({});
2 |
--------------------------------------------------------------------------------
/min.js:
--------------------------------------------------------------------------------
1 | self.Flatted=function(n){"use strict";function t(n){return t="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(n){return typeof n}:function(n){return n&&"function"==typeof Symbol&&n.constructor===Symbol&&n!==Symbol.prototype?"symbol":typeof n},t(n)}var r=JSON.parse,e=JSON.stringify,o=Object.keys,u=String,f="string",i={},c="object",a=function(n,t){return t},l=function(n){return n instanceof u?u(n):n},s=function(n,r){return t(r)===f?new u(r):r},y=function(n,r,e,f){for(var a=[],l=o(e),s=l.length,p=0;p ./coverage/lcov.info"
20 | },
21 | "repository": {
22 | "type": "git",
23 | "url": "git+https://github.com/WebReflection/flatted.git"
24 | },
25 | "files": [
26 | "LICENSE",
27 | "README.md",
28 | "cjs/",
29 | "es.js",
30 | "esm.js",
31 | "esm/",
32 | "index.js",
33 | "min.js",
34 | "php/flatted.php",
35 | "python/flatted.py",
36 | "types/"
37 | ],
38 | "keywords": [
39 | "circular",
40 | "JSON",
41 | "fast",
42 | "parser",
43 | "minimal"
44 | ],
45 | "author": "Andrea Giammarchi",
46 | "license": "ISC",
47 | "bugs": {
48 | "url": "https://github.com/WebReflection/flatted/issues"
49 | },
50 | "homepage": "https://github.com/WebReflection/flatted#readme",
51 | "devDependencies": {
52 | "@babel/core": "^7.27.1",
53 | "@babel/preset-env": "^7.27.2",
54 | "@rollup/plugin-babel": "^6.0.4",
55 | "@rollup/plugin-terser": "^0.4.4",
56 | "@ungap/structured-clone": "^1.3.0",
57 | "ascjs": "^6.0.3",
58 | "c8": "^10.1.3",
59 | "circular-json": "^0.5.9",
60 | "circular-json-es6": "^2.0.2",
61 | "jsan": "^3.1.14",
62 | "rollup": "^4.41.1",
63 | "terser": "^5.39.2",
64 | "typescript": "^5.8.3"
65 | },
66 | "module": "./esm/index.js",
67 | "type": "module",
68 | "exports": {
69 | ".": {
70 | "types": "./types/index.d.ts",
71 | "import": "./esm/index.js",
72 | "default": "./cjs/index.js"
73 | },
74 | "./esm": "./esm.js",
75 | "./package.json": "./package.json"
76 | },
77 | "types": "./types/index.d.ts"
78 | }
79 |
--------------------------------------------------------------------------------
/test/strings.txt:
--------------------------------------------------------------------------------
1 | -----------------------------------
2 | Object with 7 keys each
3 | -----------------------------------
4 | CircularJSON 100 objects parsed 5.95 times per second
5 | CircularJSON 15481 chars parsed 2.83 times per second
6 | CircularJSON 50 objects parsed 12.38 times per second
7 | CircularJSON 7698 chars parsed 5.64 times per second
8 | CircularJSON 10 objects parsed 58.88 times per second
9 | CircularJSON 1515 chars parsed 26.56 times per second
10 | -----------------------------------
11 | circular-json-es6 100 objects parsed 21.30 times per second
12 | circular-json-es6 15481 chars parsed 12.73 times per second
13 | circular-json-es6 50 objects parsed 41.54 times per second
14 | circular-json-es6 7698 chars parsed 25.40 times per second
15 | circular-json-es6 10 objects parsed 180.76 times per second
16 | circular-json-es6 1515 chars parsed 121.09 times per second
17 | -----------------------------------
18 | jsan 100 objects parsed 21.35 times per second
19 | jsan 15481 chars parsed 11.49 times per second
20 | jsan 50 objects parsed 41.91 times per second
21 | jsan 7698 chars parsed 23.01 times per second
22 | jsan 10 objects parsed 179.26 times per second
23 | jsan 1515 chars parsed 108.14 times per second
24 | -----------------------------------
25 | flatted 100 objects parsed 5.27 times per second
26 | flatted 11695 chars parsed 1.66 times per second
27 | flatted 50 objects parsed 9.63 times per second
28 | flatted 5857 chars parsed 3.24 times per second
29 | flatted 10 objects parsed 37.45 times per second
30 | flatted 1336 chars parsed 14.84 times per second
31 | -----------------------------------
32 | 50% same objects
33 | -----------------------------------
34 | CircularJSON 100 objects parsed 9.85 times per second
35 | CircularJSON 7988 chars parsed 4.22 times per second
36 | circular-json-es6 100 objects parsed 21.53 times per second
37 | circular-json-es6 15395 chars parsed 12.87 times per second
38 | jsan 100 objects parsed 21.51 times per second
39 | jsan 15395 chars parsed 11.41 times per second
40 | flatted 100 objects parsed 8.96 times per second
41 | flatted 6098 chars parsed 2.84 times per second
42 | -----------------------------------
43 | 90% same objects
44 | -----------------------------------
45 | CircularJSON 100 objects parsed 17.95 times per second
46 | CircularJSON 1965 chars parsed 9.52 times per second
47 | circular-json-es6 100 objects parsed 21.43 times per second
48 | circular-json-es6 15141 chars parsed 12.79 times per second
49 | jsan 100 objects parsed 21.82 times per second
50 | jsan 15141 chars parsed 11.71 times per second
51 | flatted 100 objects parsed 25.65 times per second
52 | flatted 1705 chars parsed 7.96 times per second
53 | -----------------------------------
54 | with circular
55 | -----------------------------------
56 | CircularJSON 100 objects parsed 12.15 times per second
57 | CircularJSON 1191 chars parsed 6.13 times per second
58 | circular-json-es6 100 objects parsed 37.64 times per second
59 | circular-json-es6 1188 chars parsed 52.54 times per second
60 | jsan 100 objects parsed 19.40 times per second
61 | jsan 2391 chars parsed 15.57 times per second
62 | flatted 100 objects parsed 11.43 times per second
63 | flatted 1587 chars parsed 5.06 times per second
64 | -----------------------------------
65 | with circular 90% same
66 | -----------------------------------
67 | CircularJSON 100 objects parsed 21.75 times per second
68 | CircularJSON 561 chars parsed 12.35 times per second
69 | circular-json-es6 100 objects parsed 98.35 times per second
70 | circular-json-es6 295 chars parsed 246.58 times per second
71 | jsan 100 objects parsed 34.27 times per second
72 | jsan 1761 chars parsed 23.58 times per second
73 | flatted 100 objects parsed 43.85 times per second
74 | flatted 514 chars parsed 12.96 times per second
--------------------------------------------------------------------------------
/test/numbers.txt:
--------------------------------------------------------------------------------
1 | -----------------------------------
2 | Object with 7 keys each
3 | -----------------------------------
4 | CircularJSON 100 objects parsed 5.19 times per second
5 | CircularJSON 15481 chars parsed 2.82 times per second
6 | CircularJSON 50 objects parsed 10.69 times per second
7 | CircularJSON 7698 chars parsed 5.58 times per second
8 | CircularJSON 10 objects parsed 51.15 times per second
9 | CircularJSON 1515 chars parsed 26.56 times per second
10 | -----------------------------------
11 | circular-json-es6 100 objects parsed 21.12 times per second
12 | circular-json-es6 15481 chars parsed 12.73 times per second
13 | circular-json-es6 50 objects parsed 41.70 times per second
14 | circular-json-es6 7698 chars parsed 25.41 times per second
15 | circular-json-es6 10 objects parsed 181.25 times per second
16 | circular-json-es6 1515 chars parsed 117.90 times per second
17 | -----------------------------------
18 | jsan 100 objects parsed 21.21 times per second
19 | jsan 15481 chars parsed 11.44 times per second
20 | jsan 50 objects parsed 41.30 times per second
21 | jsan 7698 chars parsed 22.96 times per second
22 | jsan 10 objects parsed 180.61 times per second
23 | jsan 1515 chars parsed 107.22 times per second
24 | -----------------------------------
25 | flatted 100 objects parsed 4.29 times per second
26 | flatted 16290 chars parsed 1.89 times per second
27 | flatted 50 objects parsed 8.23 times per second
28 | flatted 8052 chars parsed 3.69 times per second
29 | flatted 10 objects parsed 36.67 times per second
30 | flatted 1589 chars parsed 17.50 times per second
31 | -----------------------------------
32 | 50% same objects
33 | -----------------------------------
34 | CircularJSON 100 objects parsed 8.71 times per second
35 | CircularJSON 7988 chars parsed 4.17 times per second
36 | circular-json-es6 100 objects parsed 21.36 times per second
37 | circular-json-es6 15395 chars parsed 12.82 times per second
38 | jsan 100 objects parsed 21.40 times per second
39 | jsan 15395 chars parsed 11.60 times per second
40 | flatted 100 objects parsed 7.77 times per second
41 | flatted 8193 chars parsed 3.33 times per second
42 | -----------------------------------
43 | 90% same objects
44 | -----------------------------------
45 | CircularJSON 100 objects parsed 17.82 times per second
46 | CircularJSON 1965 chars parsed 9.56 times per second
47 | circular-json-es6 100 objects parsed 21.64 times per second
48 | circular-json-es6 15141 chars parsed 12.93 times per second
49 | jsan 100 objects parsed 21.76 times per second
50 | jsan 15141 chars parsed 11.60 times per second
51 | flatted 100 objects parsed 26.10 times per second
52 | flatted 1778 chars parsed 9.21 times per second
53 | -----------------------------------
54 | with circular
55 | -----------------------------------
56 | CircularJSON 100 objects parsed 11.57 times per second
57 | CircularJSON 1191 chars parsed 6.07 times per second
58 | circular-json-es6 100 objects parsed 37.32 times per second
59 | circular-json-es6 1188 chars parsed 51.12 times per second
60 | jsan 100 objects parsed 19.72 times per second
61 | jsan 2391 chars parsed 15.66 times per second
62 | flatted 100 objects parsed 11.81 times per second
63 | flatted 1187 chars parsed 5.75 times per second
64 | -----------------------------------
65 | with circular 90% same
66 | -----------------------------------
67 | CircularJSON 100 objects parsed 22.17 times per second
68 | CircularJSON 561 chars parsed 12.11 times per second
69 | circular-json-es6 100 objects parsed 99.08 times per second
70 | circular-json-es6 295 chars parsed 242.76 times per second
71 | jsan 100 objects parsed 35.49 times per second
72 | jsan 1761 chars parsed 23.64 times per second
73 | flatted 100 objects parsed 48.95 times per second
74 | flatted 294 chars parsed 14.13 times per second
75 |
--------------------------------------------------------------------------------
/SPECS.md:
--------------------------------------------------------------------------------
1 | # Flatted Specifications
2 |
3 | This document describes operations performed to produce, or parse, the flatted output.
4 |
5 | ## stringify(any) => flattedString
6 |
7 | The output is always an `Array` that contains at index `0` the given value.
8 |
9 | If the value is an `Array` or an `Object`, per each property value passed through the callback, return the value as is if it's not an `Array`, an `Object`, or a `string`.
10 |
11 | In case it's an `Array`, an `Object`, or a `string`, return the index as `string`, associated through a `Map`.
12 |
13 | Giving the following example:
14 |
15 | ```js
16 | flatted.stringify('a'); // ["a"]
17 | flatted.stringify(['a']); // [["1"],"a"]
18 | flatted.stringify(['a', 1, 'b']); // [["1",1,"2"],"a","b"]
19 | ```
20 |
21 | There is an `input` containing `[array, "a", "b"]`, where the `array` has indexes `"1"` and `"2"` as strings, indexes that point respectively at `"a"` and `"b"` within the input `[array, "a", "b"]`.
22 |
23 | The exact same happens for objects.
24 |
25 | ```js
26 | flatted.stringify('a'); // ["a"]
27 | flatted.stringify({a: 'a'}); // [{"a":"1"},"a"]
28 | flatted.stringify({a: 'a', n: 1, b: 'b'}); // [{"a":"1","n":1,"b":"2"},"a","b"]
29 | ```
30 |
31 | Every object, string, or array, encountered during serialization will be stored once as stringified index.
32 |
33 | ```js
34 | // per each property/value of the object/array
35 | if (any == null || !/object|string/.test(typeof any))
36 | return any;
37 | if (!map.has(any)) {
38 | const index = String(arr.length);
39 | arr.push(any);
40 | map.set(any, index);
41 | }
42 | return map.get(any);
43 | ```
44 |
45 | This, performed before going through all properties, grants unique indexes per reference.
46 |
47 | The stringified indexes ensure there won't be conflicts with regularly stored numbers.
48 |
49 | ## parse(flattedString) => any
50 |
51 | Everything that is a `string` is wrapped as `new String`, but strings in the array, from index `1` on, is kept as regular `string`.
52 |
53 | ```js
54 | const input = JSON.parse('[{"a":"1"},"b"]', Strings).map(strings);
55 | // convert strings primitives into String instances
56 | function Strings(key, value) {
57 | return typeof value === 'string' ? new String(value) : value;
58 | }
59 | // converts String instances into strings primitives
60 | function strings(value) {
61 | return value instanceof String ? String(value) : value;
62 | }
63 | ```
64 |
65 | The `input` array will have a regular `string` at index `1`, but its object at index `0` will have an `instanceof String` as `.a` property.
66 |
67 | That is the key to place back values from the rest of the array, so that per each property of the object at index `0`, if the value is an `instanceof` String, something not serializable via JSON, it means it can be used to retrieve the position of its value from the `input` array.
68 |
69 | If such `value` is an object and it hasn't been parsed yet, add it as parsed and go through all its properties/values.
70 |
71 | ```js
72 | // outside any loop ...
73 | const parsed = new Set;
74 |
75 | // ... per each property/value ...
76 | if (value instanceof Primitive) {
77 | const tmp = input[parseInt(value)];
78 | if (typeof tmp === 'object' && !parsed.has(tmp)) {
79 | parsed.add(tmp);
80 | output[key] = tmp;
81 | if (typeof tmp === 'object' && tmp != null) {
82 | // perform this same logic per
83 | // each nested property/value ...
84 | }
85 | } else {
86 | output[key] = tmp;
87 | }
88 | } else
89 | output[key] = tmp;
90 | ```
91 |
92 | As summary, the whole logic is based on polluting the de-serialization with a kind of variable that is unexpected, hence secure to use as directive to retrieve an index with a value.
93 |
94 | The usage of a `Map` and a `Set` to flag known references/strings as visited/stored makes **flatted** a rock solid, fast, and compact, solution.
95 |
--------------------------------------------------------------------------------
/esm/index.js:
--------------------------------------------------------------------------------
1 | ///
2 |
3 | // (c) 2020-present Andrea Giammarchi
4 |
5 | const {parse: $parse, stringify: $stringify} = JSON;
6 | const {keys} = Object;
7 |
8 | const Primitive = String; // it could be Number
9 | const primitive = 'string'; // it could be 'number'
10 |
11 | const ignore = {};
12 | const object = 'object';
13 |
14 | const noop = (_, value) => value;
15 |
16 | const primitives = value => (
17 | value instanceof Primitive ? Primitive(value) : value
18 | );
19 |
20 | const Primitives = (_, value) => (
21 | typeof value === primitive ? new Primitive(value) : value
22 | );
23 |
24 | const revive = (input, parsed, output, $) => {
25 | const lazy = [];
26 | for (let ke = keys(output), {length} = ke, y = 0; y < length; y++) {
27 | const k = ke[y];
28 | const value = output[k];
29 | if (value instanceof Primitive) {
30 | const tmp = input[value];
31 | if (typeof tmp === object && !parsed.has(tmp)) {
32 | parsed.add(tmp);
33 | output[k] = ignore;
34 | lazy.push({k, a: [input, parsed, tmp, $]});
35 | }
36 | else
37 | output[k] = $.call(output, k, tmp);
38 | }
39 | else if (output[k] !== ignore)
40 | output[k] = $.call(output, k, value);
41 | }
42 | for (let {length} = lazy, i = 0; i < length; i++) {
43 | const {k, a} = lazy[i];
44 | output[k] = $.call(output, k, revive.apply(null, a));
45 | }
46 | return output;
47 | };
48 |
49 | const set = (known, input, value) => {
50 | const index = Primitive(input.push(value) - 1);
51 | known.set(value, index);
52 | return index;
53 | };
54 |
55 | /**
56 | * Converts a specialized flatted string into a JS value.
57 | * @param {string} text
58 | * @param {(this: any, key: string, value: any) => any} [reviver]
59 | * @returns {any}
60 | */
61 | export const parse = (text, reviver) => {
62 | const input = $parse(text, Primitives).map(primitives);
63 | const value = input[0];
64 | const $ = reviver || noop;
65 | const tmp = typeof value === object && value ?
66 | revive(input, new Set, value, $) :
67 | value;
68 | return $.call({'': tmp}, '', tmp);
69 | };
70 |
71 | /**
72 | * Converts a JS value into a specialized flatted string.
73 | * @param {any} value
74 | * @param {((this: any, key: string, value: any) => any) | (string | number)[] | null | undefined} [replacer]
75 | * @param {string | number | undefined} [space]
76 | * @returns {string}
77 | */
78 | export const stringify = (value, replacer, space) => {
79 | const $ = replacer && typeof replacer === object ?
80 | (k, v) => (k === '' || -1 < replacer.indexOf(k) ? v : void 0) :
81 | (replacer || noop);
82 | const known = new Map;
83 | const input = [];
84 | const output = [];
85 | let i = +set(known, input, $.call({'': value}, '', value));
86 | let firstRun = !i;
87 | while (i < input.length) {
88 | firstRun = true;
89 | output[i] = $stringify(input[i++], replace, space);
90 | }
91 | return '[' + output.join(',') + ']';
92 | function replace(key, value) {
93 | if (firstRun) {
94 | firstRun = !firstRun;
95 | return value;
96 | }
97 | const after = $.call(this, key, value);
98 | switch (typeof after) {
99 | case object:
100 | if (after === null) return after;
101 | case primitive:
102 | return known.get(after) || set(known, input, after);
103 | }
104 | return after;
105 | }
106 | };
107 |
108 | /**
109 | * Converts a generic value into a JSON serializable object without losing recursion.
110 | * @param {any} value
111 | * @returns {any}
112 | */
113 | export const toJSON = value => $parse(stringify(value));
114 |
115 | /**
116 | * Converts a previously serialized object with recursion into a recursive one.
117 | * @param {any} value
118 | * @returns {any}
119 | */
120 | export const fromJSON = value => parse($stringify(value));
121 |
--------------------------------------------------------------------------------
/cjs/index.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | ///
3 |
4 | // (c) 2020-present Andrea Giammarchi
5 |
6 | const {parse: $parse, stringify: $stringify} = JSON;
7 | const {keys} = Object;
8 |
9 | const Primitive = String; // it could be Number
10 | const primitive = 'string'; // it could be 'number'
11 |
12 | const ignore = {};
13 | const object = 'object';
14 |
15 | const noop = (_, value) => value;
16 |
17 | const primitives = value => (
18 | value instanceof Primitive ? Primitive(value) : value
19 | );
20 |
21 | const Primitives = (_, value) => (
22 | typeof value === primitive ? new Primitive(value) : value
23 | );
24 |
25 | const revive = (input, parsed, output, $) => {
26 | const lazy = [];
27 | for (let ke = keys(output), {length} = ke, y = 0; y < length; y++) {
28 | const k = ke[y];
29 | const value = output[k];
30 | if (value instanceof Primitive) {
31 | const tmp = input[value];
32 | if (typeof tmp === object && !parsed.has(tmp)) {
33 | parsed.add(tmp);
34 | output[k] = ignore;
35 | lazy.push({k, a: [input, parsed, tmp, $]});
36 | }
37 | else
38 | output[k] = $.call(output, k, tmp);
39 | }
40 | else if (output[k] !== ignore)
41 | output[k] = $.call(output, k, value);
42 | }
43 | for (let {length} = lazy, i = 0; i < length; i++) {
44 | const {k, a} = lazy[i];
45 | output[k] = $.call(output, k, revive.apply(null, a));
46 | }
47 | return output;
48 | };
49 |
50 | const set = (known, input, value) => {
51 | const index = Primitive(input.push(value) - 1);
52 | known.set(value, index);
53 | return index;
54 | };
55 |
56 | /**
57 | * Converts a specialized flatted string into a JS value.
58 | * @param {string} text
59 | * @param {(this: any, key: string, value: any) => any} [reviver]
60 | * @returns {any}
61 | */
62 | const parse = (text, reviver) => {
63 | const input = $parse(text, Primitives).map(primitives);
64 | const value = input[0];
65 | const $ = reviver || noop;
66 | const tmp = typeof value === object && value ?
67 | revive(input, new Set, value, $) :
68 | value;
69 | return $.call({'': tmp}, '', tmp);
70 | };
71 | exports.parse = parse;
72 |
73 | /**
74 | * Converts a JS value into a specialized flatted string.
75 | * @param {any} value
76 | * @param {((this: any, key: string, value: any) => any) | (string | number)[] | null | undefined} [replacer]
77 | * @param {string | number | undefined} [space]
78 | * @returns {string}
79 | */
80 | const stringify = (value, replacer, space) => {
81 | const $ = replacer && typeof replacer === object ?
82 | (k, v) => (k === '' || -1 < replacer.indexOf(k) ? v : void 0) :
83 | (replacer || noop);
84 | const known = new Map;
85 | const input = [];
86 | const output = [];
87 | let i = +set(known, input, $.call({'': value}, '', value));
88 | let firstRun = !i;
89 | while (i < input.length) {
90 | firstRun = true;
91 | output[i] = $stringify(input[i++], replace, space);
92 | }
93 | return '[' + output.join(',') + ']';
94 | function replace(key, value) {
95 | if (firstRun) {
96 | firstRun = !firstRun;
97 | return value;
98 | }
99 | const after = $.call(this, key, value);
100 | switch (typeof after) {
101 | case object:
102 | if (after === null) return after;
103 | case primitive:
104 | return known.get(after) || set(known, input, after);
105 | }
106 | return after;
107 | }
108 | };
109 | exports.stringify = stringify;
110 |
111 | /**
112 | * Converts a generic value into a JSON serializable object without losing recursion.
113 | * @param {any} value
114 | * @returns {any}
115 | */
116 | const toJSON = value => $parse(stringify(value));
117 | exports.toJSON = toJSON;
118 |
119 | /**
120 | * Converts a previously serialized object with recursion into a recursive one.
121 | * @param {any} value
122 | * @returns {any}
123 | */
124 | const fromJSON = value => parse($stringify(value));
125 | exports.fromJSON = fromJSON;
126 |
--------------------------------------------------------------------------------
/python/flatted.py:
--------------------------------------------------------------------------------
1 | # ISC License
2 | #
3 | # Copyright (c) 2018-2025, Andrea Giammarchi, @WebReflection
4 | #
5 | # Permission to use, copy, modify, and/or distribute this software for any
6 | # purpose with or without fee is hereby granted, provided that the above
7 | # copyright notice and this permission notice appear in all copies.
8 | #
9 | # THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
10 | # REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
11 | # AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
12 | # INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
13 | # LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
14 | # OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
15 | # PERFORMANCE OF THIS SOFTWARE.
16 |
17 | import json as _json
18 |
19 | class _Known:
20 | def __init__(self):
21 | self.key = []
22 | self.value = []
23 |
24 | class _String:
25 | def __init__(self, value):
26 | self.value = value
27 |
28 |
29 | def _array_keys(value):
30 | keys = []
31 | i = 0
32 | for _ in value:
33 | keys.append(i)
34 | i += 1
35 | return keys
36 |
37 | def _object_keys(value):
38 | keys = []
39 | for key in value:
40 | keys.append(key)
41 | return keys
42 |
43 | def _is_array(value):
44 | return isinstance(value, (list, tuple))
45 |
46 | def _is_object(value):
47 | return isinstance(value, dict)
48 |
49 | def _is_string(value):
50 | return isinstance(value, str)
51 |
52 | def _index(known, input, value):
53 | input.append(value)
54 | index = str(len(input) - 1)
55 | known.key.append(value)
56 | known.value.append(index)
57 | return index
58 |
59 | def _loop(keys, input, known, output):
60 | for key in keys:
61 | value = output[key]
62 | if isinstance(value, _String):
63 | _ref(key, input[int(value.value)], input, known, output)
64 |
65 | return output
66 |
67 | def _ref(key, value, input, known, output):
68 | if _is_array(value) and value not in known:
69 | known.append(value)
70 | value = _loop(_array_keys(value), input, known, value)
71 | elif _is_object(value) and value not in known:
72 | known.append(value)
73 | value = _loop(_object_keys(value), input, known, value)
74 |
75 | output[key] = value
76 |
77 | def _relate(known, input, value):
78 | if _is_string(value) or _is_array(value) or _is_object(value):
79 | try:
80 | return known.value[known.key.index(value)]
81 | except:
82 | return _index(known, input, value)
83 |
84 | return value
85 |
86 | def _transform(known, input, value):
87 | if _is_array(value):
88 | output = []
89 | for val in value:
90 | output.append(_relate(known, input, val))
91 | return output
92 |
93 | if _is_object(value):
94 | obj = {}
95 | for key in value:
96 | obj[key] = _relate(known, input, value[key])
97 | return obj
98 |
99 | return value
100 |
101 | def _wrap(value):
102 | if _is_string(value):
103 | return _String(value)
104 |
105 | if _is_array(value):
106 | i = 0
107 | for val in value:
108 | value[i] = _wrap(val)
109 | i += 1
110 |
111 | elif _is_object(value):
112 | for key in value:
113 | value[key] = _wrap(value[key])
114 |
115 | return value
116 |
117 | def parse(value, *args, **kwargs):
118 | json = _json.loads(value, *args, **kwargs)
119 | wrapped = []
120 | for value in json:
121 | wrapped.append(_wrap(value))
122 |
123 | input = []
124 | for value in wrapped:
125 | if isinstance(value, _String):
126 | input.append(value.value)
127 | else:
128 | input.append(value)
129 |
130 | value = input[0]
131 |
132 | if _is_array(value):
133 | return _loop(_array_keys(value), input, [value], value)
134 |
135 | if _is_object(value):
136 | return _loop(_object_keys(value), input, [value], value)
137 |
138 | return value
139 |
140 |
141 | def stringify(value, *args, **kwargs):
142 | known = _Known()
143 | input = []
144 | output = []
145 | i = int(_index(known, input, value))
146 | while i < len(input):
147 | output.append(_transform(known, input, input[i]))
148 | i += 1
149 | return _json.dumps(output, *args, **kwargs)
150 |
--------------------------------------------------------------------------------
/php/test.php:
--------------------------------------------------------------------------------
1 | o = &$o;
26 |
27 | console::assert(Flatted::stringify($a) === '[["0"]]', 'recursive Array');
28 | console::assert(Flatted::stringify($o) === '[{"o":"0"}]', 'recursive Object');
29 |
30 | $b = Flatted::parse(Flatted::stringify($a));
31 | console::assert(is_array($b) && $b[0] === $b, 'restoring recursive Array');
32 |
33 | $a[] = 1;
34 | $a[] = 'two';
35 | $a[] = true;
36 | $o->one = 1;
37 | $o->two = 'two';
38 | $o->three = true;
39 |
40 | console::assert(Flatted::stringify($a) === '[["0",1,"1",true],"two"]', 'values in Array');
41 | console::assert(Flatted::stringify($o) === '[{"o":"0","one":1,"two":"1","three":true},"two"]', 'values in Object');
42 |
43 | $a[] = &$o;
44 | $o->a = &$a;
45 |
46 | console::assert(Flatted::stringify($a) === '[["0",1,"1",true,"2"],"two",{"o":"2","one":1,"two":"1","three":true,"a":"0"}]', 'object in Array');
47 | console::assert(Flatted::stringify($o) === '[{"o":"0","one":1,"two":"1","three":true,"a":"2"},"two",["2",1,"1",true,"0"]]', 'array in Object');
48 |
49 | $a[] = array('test' => 'OK');
50 | $a[] = [1, 2, 3];
51 |
52 | $o->test = array('test' => 'OK');
53 | $o->array = [1, 2, 3];
54 |
55 | console::assert(Flatted::stringify($a) === '[["0",1,"1",true,"2","3","4"],"two",{"o":"2","one":1,"two":"1","three":true,"a":"0","test":"3","array":"4"},{"test":"5"},[1,2,3],"OK"]', 'objects in Array');
56 | console::assert(Flatted::stringify($o) === '[{"o":"0","one":1,"two":"1","three":true,"a":"2","test":"3","array":"4"},"two",["2",1,"1",true,"0","3","4"],{"test":"5"},[1,2,3],"OK"]', 'objects in Object');
57 |
58 | $a2 = Flatted::parse(Flatted::stringify($a));
59 | $o2 = Flatted::parse(Flatted::stringify($o));
60 |
61 | console::assert($a2[0] === $a2, 'parsed Array');
62 | console::assert($o2->o === $o2, 'parsed Object');
63 |
64 | console::assert(
65 | $a2[1] === 1 &&
66 | $a2[2] === 'two' &&
67 | $a2[3] === true &&
68 | $a2[4] instanceof stdClass &&
69 | json_encode($a2[5]) === json_encode(array('test' => 'OK')) &&
70 | json_encode($a2[6]) === json_encode([1, 2, 3]),
71 | 'array values are all OK'
72 | );
73 |
74 | console::assert($a2[4] === $a2[4]->o && $a2 === $a2[4]->o->a, 'array recursive values are OK');
75 |
76 | console::assert(
77 | $o2->one === 1 &&
78 | $o2->two === 'two' &&
79 | $o2->three === true &&
80 | is_array($o2->a) &&
81 | json_encode($o2->test) === json_encode(array('test' => 'OK')) &&
82 | json_encode($o2->array) === json_encode([1, 2, 3]),
83 | 'object values are all OK'
84 | );
85 |
86 | console::assert($o2->a === $o2->a[0] && $o2 === $o2->a[4], 'object recursive values are OK');
87 |
88 | console::assert(Flatted::parse(Flatted::stringify(1)) === 1, 'numbers can be parsed too');
89 | console::assert(Flatted::parse(Flatted::stringify(false)) === false, 'booleans can be parsed too');
90 | console::assert(Flatted::parse(Flatted::stringify(null)) === null, 'null can be parsed too');
91 | console::assert(Flatted::parse(Flatted::stringify('test')) === 'test', 'strings can be parsed too');
92 |
93 | $str = Flatted::parse('[{"prop":"1","a":"2","b":"3"},{"value":123},["4","5"],{"e":"6","t":"7","p":4},{},{"b":"8"},"f",{"a":"9"},["10"],"sup",{"a":1,"d":2,"c":"7","z":"11","h":1},{"g":2,"a":"7","b":"12","f":6},{"r":4,"u":"7","c":5}]');
94 |
95 | console::assert(
96 | $str->b->t->a === 'sup' &&
97 | $str->a[1]->b[0]->c === $str->b->t,
98 | 'str is fine'
99 | );
100 |
101 | $oo = Flatted::parse('[{"a":"1","b":"0","c":"2"},{"aa":"3"},{"ca":"4","cb":"5","cc":"6","cd":"7","ce":"8","cf":"9"},{"aaa":"10"},{"caa":"4"},{"cba":"5"},{"cca":"2"},{"cda":"4"},"value2","value3","value1"]');
102 |
103 | console::assert(
104 | $oo->a->aa->aaa === 'value1'
105 | && $oo === $oo->b
106 | && $oo->c->ca->caa === $oo->c->ca
107 | && $oo->c->cb->cba === $oo->c->cb
108 | && $oo->c->cc->cca === $oo->c
109 | && $oo->c->cd->cda === $oo->c->ca->caa
110 | && $oo->c->ce === 'value2'
111 | && $oo->c->cf === 'value3',
112 | 'parse is correct'
113 | );
114 |
115 | echo "OK\n";
116 |
117 | ?>
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # flatted
2 |
3 | [](https://www.npmjs.com/package/flatted) [](https://coveralls.io/github/WebReflection/flatted?branch=main) [](https://opensource.org/licenses/ISC) 
4 |
5 | 
6 |
7 | **Social Media Photo by [Matt Seymour](https://unsplash.com/@mattseymour) on [Unsplash](https://unsplash.com/)**
8 |
9 | A super light (0.5K) and fast circular JSON parser, directly from the creator of [CircularJSON](https://github.com/WebReflection/circular-json/#circularjson).
10 |
11 | Available also for **[PHP](./php/flatted.php)**.
12 |
13 | Available also for **[Python](./python/flatted.py)**.
14 |
15 | - - -
16 |
17 | ## Announcement 📣
18 |
19 | There is a standard approach to recursion and more data-types than what JSON allows, and it's part of the [Structured Clone polyfill](https://github.com/ungap/structured-clone/#readme).
20 |
21 | Beside acting as a polyfill, its `@ungap/structured-clone/json` export provides both `stringify` and `parse`, and it's been tested for being faster than *flatted*, but its produced output is also smaller than *flatted* in general.
22 |
23 | The *@ungap/structured-clone* module is, in short, a drop in replacement for *flatted*, but it's not compatible with *flatted* specialized syntax.
24 |
25 | However, if recursion, as well as more data-types, are what you are after, or interesting for your projects/use cases, consider switching to this new module whenever you can 👍
26 |
27 | - - -
28 |
29 | ```js
30 | npm i flatted
31 | ```
32 |
33 | Usable via [CDN](https://unpkg.com/flatted) or as regular module.
34 |
35 | ```js
36 | // ESM
37 | import {parse, stringify, toJSON, fromJSON} from 'flatted';
38 |
39 | // CJS
40 | const {parse, stringify, toJSON, fromJSON} = require('flatted');
41 |
42 | const a = [{}];
43 | a[0].a = a;
44 | a.push(a);
45 |
46 | stringify(a); // [["1","0"],{"a":"0"}]
47 | ```
48 |
49 | ## toJSON and fromJSON
50 |
51 | If you'd like to implicitly survive JSON serialization, these two helpers helps:
52 |
53 | ```js
54 | import {toJSON, fromJSON} from 'flatted';
55 |
56 | class RecursiveMap extends Map {
57 | static fromJSON(any) {
58 | return new this(fromJSON(any));
59 | }
60 | toJSON() {
61 | return toJSON([...this.entries()]);
62 | }
63 | }
64 |
65 | const recursive = new RecursiveMap;
66 | const same = {};
67 | same.same = same;
68 | recursive.set('same', same);
69 |
70 | const asString = JSON.stringify(recursive);
71 | const asMap = RecursiveMap.fromJSON(JSON.parse(asString));
72 | asMap.get('same') === asMap.get('same').same;
73 | // true
74 | ```
75 |
76 |
77 | ## Flatted VS JSON
78 |
79 | As it is for every other specialized format capable of serializing and deserializing circular data, you should never `JSON.parse(Flatted.stringify(data))`, and you should never `Flatted.parse(JSON.stringify(data))`.
80 |
81 | The only way this could work is to `Flatted.parse(Flatted.stringify(data))`, as it is also for _CircularJSON_ or any other, otherwise there's no granted data integrity.
82 |
83 | Also please note this project serializes and deserializes only data compatible with JSON, so that sockets, or anything else with internal classes different from those allowed by JSON standard, won't be serialized and unserialized as expected.
84 |
85 |
86 | ### New in V1: Exact same JSON API
87 |
88 | * Added a [reviver](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/parse#Syntax) parameter to `.parse(string, reviver)` and revive your own objects.
89 | * Added a [replacer](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify#Syntax) and a `space` parameter to `.stringify(object, replacer, space)` for feature parity with JSON signature.
90 |
91 |
92 | ### Compatibility
93 | All ECMAScript engines compatible with `Map`, `Set`, `Object.keys`, and `Array.prototype.reduce` will work, even if polyfilled.
94 |
95 |
96 | ### How does it work ?
97 | While stringifying, all Objects, including Arrays, and strings, are flattened out and replaced as unique index. `*`
98 |
99 | Once parsed, all indexes will be replaced through the flattened collection.
100 |
101 | `*` represented as string to avoid conflicts with numbers
102 |
103 | ```js
104 | // logic example
105 | var a = [{one: 1}, {two: '2'}];
106 | a[0].a = a;
107 | // a is the main object, will be at index '0'
108 | // {one: 1} is the second object, index '1'
109 | // {two: '2'} the third, in '2', and it has a string
110 | // which will be found at index '3'
111 |
112 | Flatted.stringify(a);
113 | // [["1","2"],{"one":1,"a":"0"},{"two":"3"},"2"]
114 | // a[one,two] {one: 1, a} {two: '2'} '2'
115 | ```
116 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | self.Flatted = (function (exports) {
2 | 'use strict';
3 |
4 | function _typeof(o) {
5 | "@babel/helpers - typeof";
6 |
7 | return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) {
8 | return typeof o;
9 | } : function (o) {
10 | return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o;
11 | }, _typeof(o);
12 | }
13 |
14 | ///
15 |
16 | // (c) 2020-present Andrea Giammarchi
17 |
18 | var $parse = JSON.parse,
19 | $stringify = JSON.stringify;
20 | var keys = Object.keys;
21 | var Primitive = String; // it could be Number
22 | var primitive = 'string'; // it could be 'number'
23 |
24 | var ignore = {};
25 | var object = 'object';
26 | var noop = function noop(_, value) {
27 | return value;
28 | };
29 | var primitives = function primitives(value) {
30 | return value instanceof Primitive ? Primitive(value) : value;
31 | };
32 | var Primitives = function Primitives(_, value) {
33 | return _typeof(value) === primitive ? new Primitive(value) : value;
34 | };
35 | var _revive = function revive(input, parsed, output, $) {
36 | var lazy = [];
37 | for (var ke = keys(output), length = ke.length, y = 0; y < length; y++) {
38 | var k = ke[y];
39 | var value = output[k];
40 | if (value instanceof Primitive) {
41 | var tmp = input[value];
42 | if (_typeof(tmp) === object && !parsed.has(tmp)) {
43 | parsed.add(tmp);
44 | output[k] = ignore;
45 | lazy.push({
46 | k: k,
47 | a: [input, parsed, tmp, $]
48 | });
49 | } else output[k] = $.call(output, k, tmp);
50 | } else if (output[k] !== ignore) output[k] = $.call(output, k, value);
51 | }
52 | for (var _length = lazy.length, i = 0; i < _length; i++) {
53 | var _lazy$i = lazy[i],
54 | _k = _lazy$i.k,
55 | a = _lazy$i.a;
56 | output[_k] = $.call(output, _k, _revive.apply(null, a));
57 | }
58 | return output;
59 | };
60 | var set = function set(known, input, value) {
61 | var index = Primitive(input.push(value) - 1);
62 | known.set(value, index);
63 | return index;
64 | };
65 |
66 | /**
67 | * Converts a specialized flatted string into a JS value.
68 | * @param {string} text
69 | * @param {(this: any, key: string, value: any) => any} [reviver]
70 | * @returns {any}
71 | */
72 | var parse = function parse(text, reviver) {
73 | var input = $parse(text, Primitives).map(primitives);
74 | var value = input[0];
75 | var $ = reviver || noop;
76 | var tmp = _typeof(value) === object && value ? _revive(input, new Set(), value, $) : value;
77 | return $.call({
78 | '': tmp
79 | }, '', tmp);
80 | };
81 |
82 | /**
83 | * Converts a JS value into a specialized flatted string.
84 | * @param {any} value
85 | * @param {((this: any, key: string, value: any) => any) | (string | number)[] | null | undefined} [replacer]
86 | * @param {string | number | undefined} [space]
87 | * @returns {string}
88 | */
89 | var stringify = function stringify(value, replacer, space) {
90 | var $ = replacer && _typeof(replacer) === object ? function (k, v) {
91 | return k === '' || -1 < replacer.indexOf(k) ? v : void 0;
92 | } : replacer || noop;
93 | var known = new Map();
94 | var input = [];
95 | var output = [];
96 | var i = +set(known, input, $.call({
97 | '': value
98 | }, '', value));
99 | var firstRun = !i;
100 | while (i < input.length) {
101 | firstRun = true;
102 | output[i] = $stringify(input[i++], replace, space);
103 | }
104 | return '[' + output.join(',') + ']';
105 | function replace(key, value) {
106 | if (firstRun) {
107 | firstRun = !firstRun;
108 | return value;
109 | }
110 | var after = $.call(this, key, value);
111 | switch (_typeof(after)) {
112 | case object:
113 | if (after === null) return after;
114 | case primitive:
115 | return known.get(after) || set(known, input, after);
116 | }
117 | return after;
118 | }
119 | };
120 |
121 | /**
122 | * Converts a generic value into a JSON serializable object without losing recursion.
123 | * @param {any} value
124 | * @returns {any}
125 | */
126 | var toJSON = function toJSON(value) {
127 | return $parse(stringify(value));
128 | };
129 |
130 | /**
131 | * Converts a previously serialized object with recursion into a recursive one.
132 | * @param {any} value
133 | * @returns {any}
134 | */
135 | var fromJSON = function fromJSON(value) {
136 | return parse($stringify(value));
137 | };
138 |
139 | exports.fromJSON = fromJSON;
140 | exports.parse = parse;
141 | exports.stringify = stringify;
142 | exports.toJSON = toJSON;
143 |
144 | return exports;
145 |
146 | })({});
147 |
--------------------------------------------------------------------------------
/php/flatted.php:
--------------------------------------------------------------------------------
1 | value = $value;
25 | }
26 | }
27 |
28 | class Flatted {
29 |
30 | // public utilities
31 | public static function parse($json, $assoc = false, $depth = 512, $options = 0) {
32 | $input = array_map(
33 | 'Flatted::asString',
34 | array_map(
35 | 'Flatted::wrap',
36 | json_decode($json, $assoc, $depth, $options)
37 | )
38 | );
39 | $value = &$input[0];
40 | $set = array();
41 | $set[] = &$value;
42 | if (is_array($value))
43 | return Flatted::loop(false, array_keys($value), $input, $set, $value);
44 | if (is_object($value))
45 | return Flatted::loop(true, Flatted::keys($value), $input, $set, $value);
46 | return $value;
47 | }
48 |
49 | public static function stringify($value, $options = 0, $depth = 512) {
50 | $known = new stdClass;
51 | $known->key = array();
52 | $known->value = array();
53 | $input = array();
54 | $output = array();
55 | $i = intval(Flatted::index($known, $input, $value));
56 | while ($i < count($input)) {
57 | $output[$i] = Flatted::transform($known, $input, $input[$i]);
58 | $i++;
59 | }
60 | return json_encode($output, $options, $depth);
61 | }
62 |
63 | // private helpers
64 | private static function asString($value) {
65 | return $value instanceof FlattedString ? $value->value : $value;
66 | }
67 |
68 | private static function index(&$known, &$input, &$value) {
69 | $input[] = &$value;
70 | $index = strval(count($input) - 1);
71 | $known->key[] = &$value;
72 | $known->value[] = &$index;
73 | return $index;
74 | }
75 |
76 | private static function keys(&$value) {
77 | $obj = new ReflectionObject($value);
78 | $props = $obj->getProperties();
79 | $keys = array();
80 | foreach ($props as $prop)
81 | $keys[] = $prop->getName();
82 | return $keys;
83 | }
84 |
85 | private static function loop($obj, $keys, &$input, &$set, &$output) {
86 | foreach ($keys as $key) {
87 | $value = $obj ? $output->$key : $output[$key];
88 | if ($value instanceof FlattedString)
89 | Flatted::ref($obj, $key, $input[$value->value], $input, $set, $output);
90 | }
91 | return $output;
92 | }
93 |
94 | private static function relate(&$known, &$input, &$value) {
95 | if (is_string($value) || is_array($value) || is_object($value)) {
96 | $key = array_search($value, $known->key, true);
97 | if ($key !== false)
98 | return $known->value[$key];
99 | return Flatted::index($known, $input, $value);
100 | }
101 | return $value;
102 | }
103 |
104 | private static function ref($obj, &$key, &$value, &$input, &$set, &$output) {
105 | if (is_array($value) && !in_array($value, $set, true)) {
106 | $set[] = $value;
107 | $value = Flatted::loop(false, array_keys($value), $input, $set, $value);
108 | }
109 | elseif (is_object($value) && !in_array($value, $set, true)) {
110 | $set[] = $value;
111 | $value = Flatted::loop(true, Flatted::keys($value), $input, $set, $value);
112 | }
113 | if ($obj) {
114 | $output->$key = &$value;
115 | }
116 | else {
117 | $output[$key] = &$value;
118 | }
119 | }
120 |
121 | private static function transform(&$known, &$input, &$value) {
122 | if (is_array($value)) {
123 | return array_map(
124 | function ($value) use(&$known, &$input) {
125 | return Flatted::relate($known, $input, $value);
126 | },
127 | $value
128 | );
129 | }
130 | if (is_object($value)) {
131 | $object = new stdClass;
132 | $keys = Flatted::keys($value);
133 | foreach ($keys as $key)
134 | $object->$key = Flatted::relate($known, $input, $value->$key);
135 | return $object;
136 | }
137 | return $value;
138 | }
139 |
140 | private static function wrap($value) {
141 | if (is_string($value)) {
142 | return new FlattedString($value);
143 | }
144 | if (is_array($value)) {
145 | return array_map('Flatted::wrap', $value);
146 | }
147 | if (is_object($value)) {
148 | $keys = Flatted::keys($value);
149 | foreach ($keys as $key) {
150 | $value->$key = self::wrap($value->$key);
151 | }
152 | }
153 | return $value;
154 | }
155 | }
156 | ?>
--------------------------------------------------------------------------------
/test/bench.js:
--------------------------------------------------------------------------------
1 | // original file from:
2 | // https://github.com/yyx990803/circular-json-es6
3 |
4 | var dummy100 = require('./data.json');
5 | var dummy50 = dummy100.slice(0, 50)
6 | var dummy10 = dummy100.slice(0, 10)
7 |
8 | function bench(method, dummy) {
9 | var t = Date.now(), i = 0
10 | while ((Date.now() - t) < 1000) {
11 | r = method(dummy)
12 | i++
13 | }
14 | return i
15 | }
16 |
17 | var r
18 | var CircularJSON = require('circular-json')
19 | var jsan = require('jsan')
20 | var cj6 = require('circular-json-es6')
21 | var flatted = require('../cjs')
22 | var SC = require('@ungap/structured-clone/json')
23 |
24 | function run(name, fn, dummy) {
25 | console.log(
26 | name + ' ' + (
27 | typeof dummy === 'string' ?
28 | dummy.length + ' chars' :
29 | (dummy.length || '') + ' objects'
30 | ) + ' parsed ' + (
31 | bench(fn, dummy) / 1000
32 | ).toFixed(2) + ' times per second'
33 | )
34 | }
35 |
36 | //*
37 | console.log('-----------------------------------')
38 | console.log('Object with ' + Object.keys(dummy100[0]).length + ' keys each')
39 | console.log('-----------------------------------')
40 | run('CircularJSON', CircularJSON.stringify, dummy100)
41 | run('CircularJSON', CircularJSON.parse, r)
42 | run('CircularJSON', CircularJSON.stringify, dummy50)
43 | run('CircularJSON', CircularJSON.parse, r)
44 | run('CircularJSON', CircularJSON.stringify, dummy10)
45 | run('CircularJSON', CircularJSON.parse, r)
46 | console.log('-----------------------------------')
47 | run('circular-json-es6', cj6.stringify, dummy100)
48 | run('circular-json-es6', cj6.parse, r)
49 | run('circular-json-es6', cj6.stringify, dummy50)
50 | run('circular-json-es6', cj6.parse, r)
51 | run('circular-json-es6', cj6.stringify, dummy10)
52 | run('circular-json-es6', cj6.parse, r)
53 | console.log('-----------------------------------')
54 | run('jsan', jsan.stringify, dummy100)
55 | run('jsan', jsan.parse, r)
56 | run('jsan', jsan.stringify, dummy50)
57 | run('jsan', jsan.parse, r)
58 | run('jsan', jsan.stringify, dummy10)
59 | run('jsan', jsan.parse, r)
60 | console.log('-----------------------------------')
61 | run('flatted', flatted.stringify, dummy100)
62 | run('flatted', flatted.parse, r)
63 | run('flatted', flatted.stringify, dummy50)
64 | run('flatted', flatted.parse, r)
65 | run('flatted', flatted.stringify, dummy10)
66 | run('flatted', flatted.parse, r)
67 | console.log('-----------------------------------')
68 | run('Structured Clone', SC.stringify, dummy100)
69 | run('Structured Clone', SC.parse, r)
70 | run('Structured Clone', SC.stringify, dummy50)
71 | run('Structured Clone', SC.parse, r)
72 | run('Structured Clone', SC.stringify, dummy10)
73 | run('Structured Clone', SC.parse, r)
74 | console.log('-----------------------------------')
75 | console.log('50% same objects')
76 | dummy100 = dummy50.concat(dummy50)
77 | console.log('-----------------------------------')
78 | run('CircularJSON', CircularJSON.stringify, dummy100)
79 | run('CircularJSON', CircularJSON.parse, r)
80 | run('circular-json-es6', cj6.stringify, dummy100)
81 | run('circular-json-es6', cj6.parse, r)
82 | run('jsan', jsan.stringify, dummy100)
83 | run('jsan', jsan.parse, r)
84 | run('flatted', flatted.stringify, dummy100)
85 | run('flatted', flatted.parse, r)
86 | run('Structured Clone', SC.stringify, dummy100)
87 | run('Structured Clone', SC.parse, r)
88 | console.log('-----------------------------------')
89 | console.log('90% same objects')
90 | dummy100 = [].concat(
91 | dummy10, dummy10, dummy10, dummy10, dummy10,
92 | dummy10, dummy10, dummy10, dummy10, dummy10
93 | )
94 | console.log('-----------------------------------')
95 | run('CircularJSON', CircularJSON.stringify, dummy100)
96 | run('CircularJSON', CircularJSON.parse, r)
97 | run('circular-json-es6', cj6.stringify, dummy100)
98 | run('circular-json-es6', cj6.parse, r)
99 | run('jsan', jsan.stringify, dummy100)
100 | run('jsan', jsan.parse, r)
101 | run('flatted', flatted.stringify, dummy100)
102 | run('flatted', flatted.parse, r)
103 | run('Structured Clone', SC.stringify, dummy100)
104 | run('Structured Clone', SC.parse, r)
105 | console.log('-----------------------------------')
106 | console.log('with circular')
107 | function makeCircularObject () {
108 | var a = {}
109 | a.b = a
110 | return a
111 | }
112 | dummy100 = []
113 | for (var i = 0; i < 100; i++) {
114 | dummy100.push(makeCircularObject())
115 | }
116 | console.log('-----------------------------------')
117 | run('CircularJSON', CircularJSON.stringify, dummy100)
118 | run('CircularJSON', CircularJSON.parse, r)
119 | run('circular-json-es6', cj6.stringify, dummy100)
120 | run('circular-json-es6', cj6.parse, r)
121 | run('jsan', jsan.stringify, dummy100)
122 | run('jsan', jsan.parse, r)
123 | run('flatted', flatted.stringify, dummy100)
124 | run('flatted', flatted.parse, r)
125 | run('Structured Clone', SC.stringify, dummy100)
126 | run('Structured Clone', SC.parse, r)
127 | console.log('-----------------------------------')
128 | console.log('with circular 90% same')
129 | function makeCircularObject () {
130 | var a = {}
131 | a.b = a
132 | return a
133 | }
134 | dummy10 = []
135 | for (var i = 0; i < 10; i++) {
136 | dummy10.push(makeCircularObject())
137 | }
138 | dummy100 = [].concat(
139 | dummy10, dummy10, dummy10, dummy10, dummy10,
140 | dummy10, dummy10, dummy10, dummy10, dummy10
141 | )
142 | console.log('-----------------------------------')
143 | run('CircularJSON', CircularJSON.stringify, dummy100)
144 | run('CircularJSON', CircularJSON.parse, r)
145 | run('circular-json-es6', cj6.stringify, dummy100)
146 | run('circular-json-es6', cj6.parse, r)
147 | run('jsan', jsan.stringify, dummy100)
148 | run('jsan', jsan.parse, r)
149 | run('flatted', flatted.stringify, dummy100)
150 | run('flatted', flatted.parse, r)
151 | run('Structured Clone', SC.stringify, dummy100)
152 | run('Structured Clone', SC.parse, r)
153 | // */
154 | console.log('-----------------------------------')
155 | console.log('Big real-world circular data')
156 | var cirular = CircularJSON.parse(
157 | require('fs').readFileSync(
158 | require('path').join(__dirname, './circular.txt')
159 | ).toString());
160 | console.log('-----------------------------------')
161 | run('CircularJSON', CircularJSON.stringify, cirular)
162 | run('CircularJSON', CircularJSON.parse, r)
163 | run('circular-json-es6', cj6.stringify, cirular)
164 | run('circular-json-es6', cj6.parse, r)
165 | run('jsan', jsan.stringify, cirular)
166 | run('jsan', jsan.parse, r)
167 | run('flatted', flatted.stringify, cirular)
168 | run('flatted', flatted.parse, r)
169 | run('Structured Clone', SC.stringify, cirular)
170 | run('Structured Clone', SC.parse, r)
171 |
--------------------------------------------------------------------------------
/test/index.js:
--------------------------------------------------------------------------------
1 | var Flatted = require('../cjs');
2 |
3 | console.assert(Flatted.stringify([null, null]) === '[[null,null]]', 'multiple null');
4 |
5 | var a = [];
6 | var o = {};
7 |
8 | console.assert(Flatted.stringify(a) === '[[]]', 'empty Array');
9 | console.assert(Flatted.stringify(o) === '[{}]', 'empty Object');
10 |
11 | a.push(a);
12 | o.o = o;
13 |
14 | console.assert(Flatted.stringify(a) === '[["0"]]', 'recursive Array');
15 | console.assert(Flatted.stringify(o) === '[{"o":"0"}]', 'recursive Object');
16 |
17 | var b = Flatted.parse(Flatted.stringify(a));
18 | console.assert(Array.isArray(b) && b[0] === b, 'restoring recursive Array');
19 |
20 | a.push(1, 'two', true);
21 | o.one = 1;
22 | o.two = 'two';
23 | o.three = true;
24 |
25 | console.assert(Flatted.stringify(a) === '[["0",1,"1",true],"two"]', 'values in Array');
26 | console.assert(Flatted.stringify(o) === '[{"o":"0","one":1,"two":"1","three":true},"two"]', 'values in Object');
27 |
28 |
29 | a.push(o);
30 | o.a = a;
31 |
32 | console.assert(Flatted.stringify(a) === '[["0",1,"1",true,"2"],"two",{"o":"2","one":1,"two":"1","three":true,"a":"0"}]', 'object in Array');
33 | console.assert(Flatted.stringify(o) === '[{"o":"0","one":1,"two":"1","three":true,"a":"2"},"two",["2",1,"1",true,"0"]]', 'array in Object');
34 |
35 | a.push({test: 'OK'}, [1, 2, 3]);
36 | o.test = {test: 'OK'};
37 | o.array = [1, 2, 3];
38 |
39 | console.assert(Flatted.stringify(a) === '[["0",1,"1",true,"2","3","4"],"two",{"o":"2","one":1,"two":"1","three":true,"a":"0","test":"5","array":"6"},{"test":"7"},[1,2,3],{"test":"7"},[1,2,3],"OK"]', 'objects in Array');
40 | console.assert(Flatted.stringify(o) === '[{"o":"0","one":1,"two":"1","three":true,"a":"2","test":"3","array":"4"},"two",["2",1,"1",true,"0","5","6"],{"test":"7"},[1,2,3],{"test":"7"},[1,2,3],"OK"]', 'objects in Object');
41 |
42 | a = Flatted.parse(Flatted.stringify(a));
43 | o = Flatted.parse(Flatted.stringify(o));
44 |
45 | console.assert(a[0] === a, 'parsed Array');
46 | console.assert(o.o === o, 'parsed Object');
47 |
48 | console.assert(
49 | a[1] === 1 &&
50 | a[2] === 'two' &&
51 | a[3] === true &&
52 | a[4] instanceof Object &&
53 | JSON.stringify(a[5]) === JSON.stringify({test: 'OK'}) &&
54 | JSON.stringify(a[6]) === JSON.stringify([1, 2, 3]),
55 | 'array values are all OK'
56 | );
57 |
58 | console.assert(a[4] === a[4].o && a === a[4].o.a, 'array recursive values are OK');
59 |
60 | console.assert(
61 | o.one === 1 &&
62 | o.two === 'two' &&
63 | o.three === true &&
64 | Array.isArray(o.a) &&
65 | JSON.stringify(o.test) === JSON.stringify({test: 'OK'}) &&
66 | JSON.stringify(o.array) === JSON.stringify([1, 2, 3]),
67 | 'object values are all OK'
68 | );
69 |
70 | console.assert(o.a === o.a[0] && o === o.a[4], 'object recursive values are OK');
71 |
72 | console.assert(Flatted.parse(Flatted.stringify(1)) === 1, 'numbers can be parsed too');
73 | console.assert(Flatted.parse(Flatted.stringify(false)) === false, 'booleans can be parsed too');
74 | console.assert(Flatted.parse(Flatted.stringify(null)) === null, 'null can be parsed too');
75 | console.assert(Flatted.parse(Flatted.stringify('test')) === 'test', 'strings can be parsed too');
76 |
77 | var d = new Date;
78 | console.assert(Flatted.parse(Flatted.stringify(d)) === d.toISOString(), 'dates can be parsed too');
79 |
80 | console.assert(Flatted.parse(
81 | Flatted.stringify(d),
82 | function (key, value) {
83 | if (typeof value === 'string' && /^[0-9:.ZT-]+$/.test(value))
84 | return new Date(value);
85 | return value;
86 | }
87 | ) instanceof Date, 'dates can be revived too');
88 |
89 | console.assert(Flatted.parse(
90 | Flatted.stringify({
91 | sub: {
92 | one23: 123,
93 | date: d
94 | }
95 | }),
96 | function (key, value) {
97 | if (key !== '' && typeof value === 'string' && /^[0-9:.ZT-]+$/.test(value))
98 | return new Date(value);
99 | return value;
100 | }
101 | ).sub.date instanceof Date, 'dates can be revived too');
102 |
103 |
104 | // borrowed from CircularJSON
105 |
106 |
107 | (function () {
108 | var special = "\\x7e"; // \x7e is ~
109 | //console.log(Flatted.stringify({a:special}));
110 | //console.log(Flatted.parse(Flatted.stringify({a:special})).a);
111 | console.assert(Flatted.parse(Flatted.stringify({a:special})).a === special, 'no problem with simulation');
112 | special = "~\\x7e";
113 | console.assert(Flatted.parse(Flatted.stringify({a:special})).a === special, 'no problem with special char');
114 | }());
115 |
116 | (function () {
117 | var o = {a: 'a', b: 'b', c: function(){}, d: {e: 123}},
118 | a = JSON.stringify(o),
119 | b = Flatted.stringify(o);
120 |
121 | console.assert(
122 | JSON.stringify(JSON.parse(a)) === JSON.stringify(Flatted.parse(b)),
123 | 'works as JSON.parse'
124 | );
125 | console.assert(
126 | Flatted.stringify(o, function(key, value){
127 | if (!key || key === 'a') return value;
128 | }) === '[{"a":"1"},"a"]',
129 | 'accept callback'
130 | );
131 | console.assert(
132 | JSON.stringify(
133 | Flatted.parse('[{"a":"1"},"a"]', function(key, value){
134 | if (key === 'a') return 'b';
135 | return value;
136 | })
137 | ) === '{"a":"b"}',
138 | 'revive callback'
139 | );
140 | }());
141 |
142 | (function () {
143 | var o = {}, before, after;
144 | o.a = o;
145 | o.c = {};
146 | o.d = {
147 | a: 123,
148 | b: o
149 | };
150 | o.c.e = o;
151 | o.c.f = o.d;
152 | o.b = o.c;
153 | before = Flatted.stringify(o);
154 | o = Flatted.parse(before);
155 | console.assert(
156 | o.b === o.c &&
157 | o.c.e === o &&
158 | o.d.a === 123 &&
159 | o.d.b === o &&
160 | o.c.f === o.d &&
161 | o.b === o.c,
162 | 'recreated original structure'
163 | );
164 | }());
165 |
166 | (function () {
167 | var o = {};
168 | o.a = o;
169 | o.b = o;
170 | console.assert(
171 | Flatted.stringify(o, function (key, value) {
172 | if (!key || key === 'a') return value;
173 | }) === '[{"a":"0"}]',
174 | 'callback invoked'
175 | );
176 | o = Flatted.parse('[{"a":"0"}]', function (key, value) {
177 | if (!key) {
178 | value.b = value;
179 | }
180 | return value;
181 | });
182 | console.assert(
183 | o.a === o && o.b === o,
184 | 'reviver invoked'
185 | );
186 | }());
187 |
188 | (function () {
189 | var o = {};
190 | o['~'] = o;
191 | o['\\x7e'] = '\\x7e';
192 | o.test = '~';
193 |
194 | o = Flatted.parse(Flatted.stringify(o));
195 | console.assert(o['~'] === o && o.test === '~', 'still intact');
196 | o = {
197 | a: [
198 | '~', '~~', '~~~'
199 | ]
200 | };
201 | o.a.push(o);
202 | o.o = o;
203 | o['~'] = o.a;
204 | o['~~'] = o.a;
205 | o['~~~'] = o.a;
206 | o = Flatted.parse(Flatted.stringify(o));
207 | console.assert(
208 | o === o.a[3] &&
209 | o === o.o &&
210 | o['~'] === o.a &&
211 | o['~~'] === o.a &&
212 | o['~~~'] === o.a &&
213 | o.a === o.a[3].a &&
214 | o.a.pop() === o &&
215 | o.a.join('') === '~~~~~~',
216 | 'restructured'
217 | );
218 |
219 | }());
220 |
221 | (function () {
222 |
223 | // make sure only own properties are parsed
224 | Object.prototype.shenanigans = true;
225 |
226 | var
227 | item = {
228 | name: 'TEST'
229 | },
230 | original = {
231 | outer: [
232 | {
233 | a: 'b',
234 | c: 'd',
235 | one: item,
236 | many: [item],
237 | e: 'f'
238 | }
239 | ]
240 | },
241 | str,
242 | output
243 | ;
244 | item.value = item;
245 | str = Flatted.stringify(original);
246 | output = Flatted.parse(str);
247 | console.assert(str === '[{"outer":"1"},["2"],{"a":"3","c":"4","one":"5","many":"6","e":"7"},"b","d",{"name":"8","value":"5"},["5"],"f","TEST"]', 'string is correct');
248 | console.assert(
249 | original.outer[0].one.name === output.outer[0].one.name &&
250 | original.outer[0].many[0].name === output.outer[0].many[0].name &&
251 | output.outer[0].many[0] === output.outer[0].one,
252 | 'object too'
253 | );
254 |
255 | delete Object.prototype.shenanigans;
256 |
257 | }());
258 |
259 | (function () {
260 | var
261 | unique = {a:'sup'},
262 | nested = {
263 | prop: {
264 | value: 123
265 | },
266 | a: [
267 | {},
268 | {b: [
269 | {
270 | a: 1,
271 | d: 2,
272 | c: unique,
273 | z: {
274 | g: 2,
275 | a: unique,
276 | b: {
277 | r: 4,
278 | u: unique,
279 | c: 5
280 | },
281 | f: 6
282 | },
283 | h: 1
284 | }
285 | ]}
286 | ],
287 | b: {
288 | e: 'f',
289 | t: unique,
290 | p: 4
291 | }
292 | },
293 | str = Flatted.stringify(nested),
294 | output
295 | ;
296 | console.assert(str === '[{"prop":"1","a":"2","b":"3"},{"value":123},["4","5"],{"e":"6","t":"7","p":4},{},{"b":"8"},"f",{"a":"9"},["10"],"sup",{"a":1,"d":2,"c":"7","z":"11","h":1},{"g":2,"a":"7","b":"12","f":6},{"r":4,"u":"7","c":5}]', 'string is OK');
297 | output = Flatted.parse(str);
298 | console.assert(output.b.t.a === 'sup' && output.a[1].b[0].c === output.b.t, 'so is the object');
299 | }());
300 |
301 | (function () {
302 | var o = {bar: 'something ~ baz'};
303 | var s = Flatted.stringify(o);
304 | console.assert(s === '[{"bar":"1"},"something ~ baz"]', 'string is correct');
305 | var oo = Flatted.parse(s);
306 | console.assert(oo.bar === o.bar, 'parse is correct');
307 | }());
308 |
309 | (function () {
310 | var o = {};
311 | o.a = {
312 | aa: {
313 | aaa: 'value1'
314 | }
315 | };
316 | o.b = o;
317 | o.c = {
318 | ca: {},
319 | cb: {},
320 | cc: {},
321 | cd: {},
322 | ce: 'value2',
323 | cf: 'value3'
324 | };
325 | o.c.ca.caa = o.c.ca;
326 | o.c.cb.cba = o.c.cb;
327 | o.c.cc.cca = o.c;
328 | o.c.cd.cda = o.c.ca.caa;
329 |
330 | var s = Flatted.stringify(o);
331 | console.assert(s === '[{"a":"1","b":"0","c":"2"},{"aa":"3"},{"ca":"4","cb":"5","cc":"6","cd":"7","ce":"8","cf":"9"},{"aaa":"10"},{"caa":"4"},{"cba":"5"},{"cca":"2"},{"cda":"4"},"value2","value3","value1"]', 'string is correct');
332 | var oo = Flatted.parse(s);
333 | console.assert(
334 | oo.a.aa.aaa = 'value1'
335 | && oo === oo.b
336 | && oo.c.ca.caa === oo.c.ca
337 | && oo.c.cb.cba === oo.c.cb
338 | && oo.c.cc.cca === oo.c
339 | && oo.c.cd.cda === oo.c.ca.caa
340 | && oo.c.ce === 'value2'
341 | && oo.c.cf === 'value3',
342 | 'parse is correct'
343 | );
344 | }());
345 |
346 | (function () {
347 | var
348 | original = {
349 | a1: {
350 | a2: [],
351 | a3: [{name: 'whatever'}]
352 | },
353 | a4: []
354 | },
355 | json,
356 | restored
357 | ;
358 |
359 | original.a1.a2[0] = original.a1;
360 | original.a4[0] = original.a1.a3[0];
361 |
362 | json = Flatted.stringify(original);
363 | restored = Flatted.parse(json);
364 |
365 | console.assert(restored.a1.a2[0] === restored.a1, '~a1~a2~0 === ~a1');
366 | console.assert(restored.a4[0] = restored.a1.a3[0], '~a4 === ~a1~a3~0');
367 | }());
368 |
369 | if (typeof Symbol !== 'undefined') {
370 | (function () {
371 | var o = {a: 1};
372 | var a = [1, Symbol('test'), 2];
373 | o[Symbol('test')] = 123;
374 | console.assert(('[' + JSON.stringify(o) + ']') === Flatted.stringify(o), 'Symbol is OK too');
375 | console.assert(('[' + JSON.stringify(a) + ']') === Flatted.stringify(a), 'non symbol is OK too');
376 | }());
377 | }
378 |
379 | (function () {
380 | var args = [{a:[1]}, null, ' '];
381 | console.assert(Flatted.stringify.apply(null, args) === "[{\n \"a\": \"1\"\n},[\n 1\n]]", 'extra args same as JSON');
382 | }());
383 |
384 | (function () {
385 | var o = {a: 1, b: {a: 1, b: 2}};
386 | var json = JSON.stringify(o, ['b']);
387 | console.assert(
388 | Flatted.stringify(o, ['b']) === '[{"b":"1"},{"b":2}]',
389 | 'whitelisted ["b"]: '+ json
390 | );
391 | }());
392 |
393 | (function () {
394 | var a = { b: { '': { c: { d: 1 } } } };
395 | a._circular = a.b[''];
396 | var json = Flatted.stringify(a);
397 | var nosj = Flatted.parse(json);
398 | console.assert(
399 | nosj._circular === nosj.b[''] &&
400 | JSON.stringify(nosj._circular) === JSON.stringify(a._circular),
401 | 'empty keys as non root objects work'
402 | );
403 | delete a._circular;
404 | delete nosj._circular;
405 | console.assert(
406 | JSON.stringify(nosj) === JSON.stringify(a),
407 | 'objects copied with circular empty keys are the same'
408 | );
409 | }());
410 |
411 | ['65515.json', '65518.json'].forEach(fileName => {
412 | let dataString = require('fs').readFileSync('test/' + fileName).toString('utf-8');
413 | let rawJson = JSON.parse(dataString);
414 | let {toolData} = rawJson;
415 | console.assert(typeof Flatted.parse(JSON.stringify(toolData)) === 'object');
416 | });
417 |
418 | class RecursiveMap extends Map {
419 | static fromJSON(any) {
420 | return new this(Flatted.fromJSON(any));
421 | }
422 | toJSON() {
423 | return Flatted.toJSON([...this.entries()]);
424 | }
425 | }
426 |
427 | const jsonMap = new RecursiveMap([['test', 'value']]);
428 | const asJSON = JSON.stringify(jsonMap);
429 | const expected = '[["1"],["2","3"],"test","value"]';
430 | console.assert(asJSON === expected, 'toJSON');
431 | const revived = RecursiveMap.fromJSON(JSON.parse(asJSON));
432 | console.assert(revived.get('test') === 'value', 'fromJSON');
433 |
434 |
435 | const recursive = new RecursiveMap;
436 | const same = {};
437 | same.same = same;
438 | recursive.set('same', same);
439 |
440 | const asString = JSON.stringify(recursive);
441 | const asMap = RecursiveMap.fromJSON(JSON.parse(asString));
442 | console.assert(asMap.get('same').same === asMap.get('same'), 'RecursiveMap');
443 |
--------------------------------------------------------------------------------
/test/data.json:
--------------------------------------------------------------------------------
1 | [{"id":1,"sex":"Female","age":38,"classOfWorker":"Private","maritalStatus":"Married-civilian spouse present","education":"1st 2nd 3rd or 4th grade","race":"White"},{"id":2,"sex":"Female","age":44,"classOfWorker":"Self-employed-not incorporated","maritalStatus":"Married-civilian spouse present","education":"Associates degree-occup /vocational","race":"White"},{"id":3,"sex":"Male","age":2,"classOfWorker":"Not in universe","maritalStatus":"Never married","education":"Children","race":"White"},{"id":4,"sex":"Female","age":35,"classOfWorker":"Private","maritalStatus":"Divorced","education":"High school graduate","race":"White"},{"id":5,"sex":"Male","age":49,"classOfWorker":"Private","maritalStatus":"Divorced","education":"High school graduate","race":"White"},{"id":6,"sex":"Male","age":13,"classOfWorker":"Not in universe","maritalStatus":"Never married","education":"Children","race":"White"},{"id":7,"sex":"Female","age":1,"classOfWorker":"Not in universe","maritalStatus":"Never married","education":"Children","race":"White"},{"id":8,"sex":"Female","age":61,"classOfWorker":"Not in universe","maritalStatus":"Married-civilian spouse present","education":"High school graduate","race":"White"},{"id":9,"sex":"Male","age":38,"classOfWorker":"Private","maritalStatus":"Married-civilian spouse present","education":"Masters degree(MA MS MEng MEd MSW MBA)","race":"Black"},{"id":10,"sex":"Female","age":7,"classOfWorker":"Not in universe","maritalStatus":"Never married","education":"Children","race":"White"},{"id":11,"sex":"Female","age":30,"classOfWorker":"Private","maritalStatus":"Married-civilian spouse present","education":"Bachelors degree(BA AB BS)","race":"White"},{"id":12,"sex":"Male","age":85,"classOfWorker":"Not in universe","maritalStatus":"Married-civilian spouse present","education":"10th grade","race":"White"},{"id":13,"sex":"Female","age":33,"classOfWorker":"Private","maritalStatus":"Married-civilian spouse present","education":"High school graduate","race":"Asian or Pacific Islander"},{"id":14,"sex":"Male","age":26,"classOfWorker":"Private","maritalStatus":"Never married","education":"Some college but no degree","race":"White"},{"id":15,"sex":"Female","age":46,"classOfWorker":"Private","maritalStatus":"Married-civilian spouse present","education":"High school graduate","race":"White"},{"id":16,"sex":"Female","age":19,"classOfWorker":"Private","maritalStatus":"Never married","education":"Some college but no degree","race":"White"},{"id":17,"sex":"Male","age":11,"classOfWorker":"Not in universe","maritalStatus":"Never married","education":"Children","race":"White"},{"id":18,"sex":"Male","age":23,"classOfWorker":"Private","maritalStatus":"Married-civilian spouse present","education":"Bachelors degree(BA AB BS)","race":"White"},{"id":19,"sex":"Male","age":27,"classOfWorker":"Private","maritalStatus":"Married-civilian spouse present","education":"High school graduate","race":"White"},{"id":20,"sex":"Male","age":35,"classOfWorker":"Self-employed-not incorporated","maritalStatus":"Divorced","education":"High school graduate","race":"Black"},{"id":21,"sex":"Male","age":8,"classOfWorker":"Not in universe","maritalStatus":"Never married","education":"Children","race":"White"},{"id":22,"sex":"Female","age":29,"classOfWorker":"Not in universe","maritalStatus":"Married-civilian spouse present","education":"High school graduate","race":"White"},{"id":23,"sex":"Female","age":40,"classOfWorker":"Private","maritalStatus":"Divorced","education":"Some college but no degree","race":"White"},{"id":24,"sex":"Male","age":24,"classOfWorker":"Private","maritalStatus":"Married-civilian spouse present","education":"High school graduate","race":"White"},{"id":25,"sex":"Male","age":45,"classOfWorker":"Private","maritalStatus":"Married-civilian spouse present","education":"Some college but no degree","race":"White"},{"id":26,"sex":"Female","age":27,"classOfWorker":"Private","maritalStatus":"Never married","education":"Some college but no degree","race":"White"},{"id":27,"sex":"Male","age":41,"classOfWorker":"Local government","maritalStatus":"Married-civilian spouse present","education":"High school graduate","race":"White"},{"id":28,"sex":"Female","age":14,"classOfWorker":"Not in universe","maritalStatus":"Never married","education":"Children","race":"White"},{"id":29,"sex":"Male","age":73,"classOfWorker":"Not in universe","maritalStatus":"Married-civilian spouse present","education":"7th and 8th grade","race":"White"},{"id":30,"sex":"Male","age":46,"classOfWorker":"Private","maritalStatus":"Married-civilian spouse present","education":"Some college but no degree","race":"White"},{"id":31,"sex":"Female","age":78,"classOfWorker":"Not in universe","maritalStatus":"Widowed","education":"7th and 8th grade","race":"White"},{"id":32,"sex":"Male","age":27,"classOfWorker":"Private","maritalStatus":"Married-civilian spouse present","education":"High school graduate","race":"White"},{"id":33,"sex":"Female","age":81,"classOfWorker":"Not in universe","maritalStatus":"Married-civilian spouse present","education":"9th grade","race":"White"},{"id":34,"sex":"Male","age":35,"classOfWorker":"Private","maritalStatus":"Never married","education":"Masters degree(MA MS MEng MEd MSW MBA)","race":"White"},{"id":35,"sex":"Female","age":15,"classOfWorker":"Not in universe","maritalStatus":"Never married","education":"7th and 8th grade","race":"White"},{"id":36,"sex":"Female","age":27,"classOfWorker":"State government","maritalStatus":"Never married","education":"12th grade no diploma","race":"Black"},{"id":37,"sex":"Male","age":68,"classOfWorker":"Not in universe","maritalStatus":"Widowed","education":"Less than 1st grade","race":"Other"},{"id":38,"sex":"Female","age":28,"classOfWorker":"Private","maritalStatus":"Never married","education":"11th grade","race":"Black"},{"id":39,"sex":"Male","age":54,"classOfWorker":"Private","maritalStatus":"Married-civilian spouse present","education":"Masters degree(MA MS MEng MEd MSW MBA)","race":"White"},{"id":40,"sex":"Female","age":37,"classOfWorker":"Not in universe","maritalStatus":"Married-civilian spouse present","education":"High school graduate","race":"White"},{"id":41,"sex":"Male","age":82,"classOfWorker":"Not in universe","maritalStatus":"Widowed","education":"7th and 8th grade","race":"White"},{"id":42,"sex":"Female","age":55,"classOfWorker":"Self-employed-not incorporated","maritalStatus":"Married-civilian spouse present","education":"High school graduate","race":"Asian or Pacific Islander"},{"id":43,"sex":"Male","age":77,"classOfWorker":"Not in universe","maritalStatus":"Married-civilian spouse present","education":"Less than 1st grade","race":"White"},{"id":44,"sex":"Male","age":53,"classOfWorker":"Private","maritalStatus":"Divorced","education":"Some college but no degree","race":"White"},{"id":45,"sex":"Male","age":25,"classOfWorker":"Private","maritalStatus":"Never married","education":"Some college but no degree","race":"White"},{"id":46,"sex":"Male","age":23,"classOfWorker":"Private","maritalStatus":"Never married","education":"Associates degree-academic program","race":"White"},{"id":47,"sex":"Female","age":0,"classOfWorker":"Not in universe","maritalStatus":"Never married","education":"Children","race":"White"},{"id":48,"sex":"Female","age":49,"classOfWorker":"Private","maritalStatus":"Married-civilian spouse present","education":"Some college but no degree","race":"White"},{"id":49,"sex":"Female","age":75,"classOfWorker":"Not in universe","maritalStatus":"Widowed","education":"10th grade","race":"White"},{"id":50,"sex":"Male","age":80,"classOfWorker":"Not in universe","maritalStatus":"Never married","education":"High school graduate","race":"White"},{"id":51,"sex":"Female","age":10,"classOfWorker":"Not in universe","maritalStatus":"Never married","education":"Children","race":"Asian or Pacific Islander"},{"id":52,"sex":"Male","age":22,"classOfWorker":"State government","maritalStatus":"Never married","education":"Associates degree-occup /vocational","race":"White"},{"id":53,"sex":"Female","age":61,"classOfWorker":"Not in universe","maritalStatus":"Married-civilian spouse present","education":"9th grade","race":"White"},{"id":54,"sex":"Female","age":1,"classOfWorker":"Not in universe","maritalStatus":"Never married","education":"Children","race":"White"},{"id":55,"sex":"Female","age":43,"classOfWorker":"Self-employed-not incorporated","maritalStatus":"Married-civilian spouse present","education":"High school graduate","race":"White"},{"id":56,"sex":"Male","age":48,"classOfWorker":"Not in universe","maritalStatus":"Married-civilian spouse present","education":"Bachelors degree(BA AB BS)","race":"White"},{"id":57,"sex":"Female","age":5,"classOfWorker":"Not in universe","maritalStatus":"Never married","education":"Children","race":"Black"},{"id":58,"sex":"Female","age":16,"classOfWorker":"Never worked","maritalStatus":"Never married","education":"10th grade","race":"White"},{"id":59,"sex":"Female","age":27,"classOfWorker":"Not in universe","maritalStatus":"Never married","education":"High school graduate","race":"White"},{"id":60,"sex":"Male","age":61,"classOfWorker":"Self-employed-not incorporated","maritalStatus":"Married-civilian spouse present","education":"High school graduate","race":"White"},{"id":61,"sex":"Female","age":18,"classOfWorker":"Private","maritalStatus":"Never married","education":"11th grade","race":"Asian or Pacific Islander"},{"id":62,"sex":"Male","age":54,"classOfWorker":"Local government","maritalStatus":"Married-civilian spouse present","education":"High school graduate","race":"White"},{"id":63,"sex":"Male","age":50,"classOfWorker":"Private","maritalStatus":"Married-civilian spouse present","education":"7th and 8th grade","race":"White"},{"id":64,"sex":"Female","age":64,"classOfWorker":"Private","maritalStatus":"Widowed","education":"High school graduate","race":"White"},{"id":65,"sex":"Male","age":64,"classOfWorker":"Not in universe","maritalStatus":"Married-civilian spouse present","education":"1st 2nd 3rd or 4th grade","race":"Black"},{"id":66,"sex":"Male","age":3,"classOfWorker":"Not in universe","maritalStatus":"Never married","education":"Children","race":"White"},{"id":67,"sex":"Female","age":45,"classOfWorker":"Private","maritalStatus":"Married-civilian spouse present","education":"11th grade","race":"White"},{"id":68,"sex":"Female","age":72,"classOfWorker":"Not in universe","maritalStatus":"Widowed","education":"Bachelors degree(BA AB BS)","race":"White"},{"id":69,"sex":"Male","age":80,"classOfWorker":"Not in universe","maritalStatus":"Married-civilian spouse present","education":"High school graduate","race":"White"},{"id":70,"sex":"Male","age":47,"classOfWorker":"Private","maritalStatus":"Married-civilian spouse present","education":"Bachelors degree(BA AB BS)","race":"Black"},{"id":71,"sex":"Female","age":39,"classOfWorker":"Private","maritalStatus":"Divorced","education":"Associates degree-occup /vocational","race":"White"},{"id":72,"sex":"Male","age":51,"classOfWorker":"Self-employed-not incorporated","maritalStatus":"Married-civilian spouse present","education":"Some college but no degree","race":"White"},{"id":73,"sex":"Male","age":12,"classOfWorker":"Not in universe","maritalStatus":"Never married","education":"Children","race":"Asian or Pacific Islander"},{"id":74,"sex":"Male","age":41,"classOfWorker":"Private","maritalStatus":"Never married","education":"Some college but no degree","race":"Asian or Pacific Islander"},{"id":75,"sex":"Male","age":39,"classOfWorker":"Local government","maritalStatus":"Married-civilian spouse present","education":"High school graduate","race":"White"},{"id":76,"sex":"Female","age":67,"classOfWorker":"Not in universe","maritalStatus":"Married-civilian spouse present","education":"High school graduate","race":"White"},{"id":77,"sex":"Female","age":59,"classOfWorker":"Not in universe","maritalStatus":"Married-civilian spouse present","education":"High school graduate","race":"White"},{"id":78,"sex":"Female","age":48,"classOfWorker":"Not in universe","maritalStatus":"Married-civilian spouse present","education":"10th grade","race":"White"},{"id":79,"sex":"Female","age":42,"classOfWorker":"Private","maritalStatus":"Married-civilian spouse present","education":"7th and 8th grade","race":"White"},{"id":80,"sex":"Male","age":38,"classOfWorker":"Private","maritalStatus":"Married-civilian spouse present","education":"Some college but no degree","race":"White"},{"id":81,"sex":"Female","age":28,"classOfWorker":"Private","maritalStatus":"Married-civilian spouse present","education":"Masters degree(MA MS MEng MEd MSW MBA)","race":"White"},{"id":82,"sex":"Male","age":8,"classOfWorker":"Not in universe","maritalStatus":"Never married","education":"Children","race":"White"},{"id":83,"sex":"Female","age":4,"classOfWorker":"Not in universe","maritalStatus":"Never married","education":"Children","race":"White"},{"id":84,"sex":"Male","age":26,"classOfWorker":"Private","maritalStatus":"Never married","education":"Some college but no degree","race":"White"},{"id":85,"sex":"Female","age":55,"classOfWorker":"Private","maritalStatus":"Married-civilian spouse present","education":"Associates degree-occup /vocational","race":"White"},{"id":86,"sex":"Female","age":42,"classOfWorker":"Private","maritalStatus":"Married-civilian spouse present","education":"High school graduate","race":"White"},{"id":87,"sex":"Male","age":18,"classOfWorker":"Not in universe","maritalStatus":"Never married","education":"12th grade no diploma","race":"White"},{"id":88,"sex":"Female","age":14,"classOfWorker":"Not in universe","maritalStatus":"Never married","education":"Children","race":"White"},{"id":89,"sex":"Male","age":4,"classOfWorker":"Not in universe","maritalStatus":"Never married","education":"Children","race":"White"},{"id":90,"sex":"Female","age":32,"classOfWorker":"Private","maritalStatus":"Never married","education":"Some college but no degree","race":"Black"},{"id":91,"sex":"Female","age":83,"classOfWorker":"Not in universe","maritalStatus":"Widowed","education":"7th and 8th grade","race":"White"},{"id":92,"sex":"Female","age":5,"classOfWorker":"Not in universe","maritalStatus":"Never married","education":"Children","race":"White"},{"id":93,"sex":"Female","age":33,"classOfWorker":"Not in universe","maritalStatus":"Married-civilian spouse present","education":"Bachelors degree(BA AB BS)","race":"White"},{"id":94,"sex":"Female","age":57,"classOfWorker":"Self-employed-incorporated","maritalStatus":"Widowed","education":"12th grade no diploma","race":"White"},{"id":95,"sex":"Female","age":6,"classOfWorker":"Not in universe","maritalStatus":"Never married","education":"Children","race":"White"},{"id":96,"sex":"Female","age":37,"classOfWorker":"Local government","maritalStatus":"Married-civilian spouse present","education":"Bachelors degree(BA AB BS)","race":"White"},{"id":97,"sex":"Female","age":47,"classOfWorker":"Private","maritalStatus":"Married-civilian spouse present","education":"High school graduate","race":"White"},{"id":98,"sex":"Female","age":54,"classOfWorker":"Not in universe","maritalStatus":"Divorced","education":"9th grade","race":"White"},{"id":99,"sex":"Female","age":55,"classOfWorker":"Private","maritalStatus":"Married-civilian spouse present","education":"High school graduate","race":"White"},{"id":100,"sex":"Female","age":44,"classOfWorker":"Private","maritalStatus":"Divorced","education":"Some college but no degree","race":"White"}]
--------------------------------------------------------------------------------