diff --git a/lp/stable-stringify.js b/lp/stable-stringify.js new file mode 100644 index 00000000..c1f1b302 --- /dev/null +++ b/lp/stable-stringify.js @@ -0,0 +1,75 @@ +function stableStringify(obj, opts) +{ + if (!opts) + opts = {}; + if (typeof opts === 'function') + opts = { cmp: opts }; + let space = opts.space || ''; + if (typeof space === 'number') + space = Array(space+1).join(' '); + const cycles = (typeof opts.cycles === 'boolean') ? opts.cycles : false; + const cmp = opts.cmp && (function (f) + { + return function (node) + { + return function (a, b) + { + let aobj = { key: a, value: node[a] }; + let bobj = { key: b, value: node[b] }; + return f(aobj, bobj); + }; + }; + })(opts.cmp); + const seen = new Map(); + return (function stringify (parent, key, node, level) + { + const indent = space ? ('\n' + new Array(level + 1).join(space)) : ''; + const colonSeparator = space ? ': ' : ':'; + if (node === undefined) + { + return; + } + if (typeof node !== 'object' || node === null) + { + return JSON.stringify(node); + } + if (node instanceof Array) + { + const out = []; + for (let i = 0; i < node.length; i++) + { + const item = stringify(node, i, node[i], level+1) || JSON.stringify(null); + out.push(indent + space + item); + } + return '[' + out.join(',') + indent + ']'; + } + else + { + if (seen.has(node)) + { + if (cycles) + return JSON.stringify('__cycle__'); + throw new TypeError('Converting circular structure to JSON'); + } + else + seen.set(node, true); + const keys = Object.keys(node).sort(cmp && cmp(node)); + const out = []; + for (let i = 0; i < keys.length; i++) + { + const key = keys[i]; + const value = stringify(node, key, node[key], level+1); + if (!value) + continue; + const keyValue = JSON.stringify(key) + + colonSeparator + + value; + out.push(indent + space + keyValue); + } + seen.delete(node); + return '{' + out.join(',') + indent + '}'; + } + })({ '': obj }, '', obj, 0); +} + +module.exports = stableStringify;