From 359b264c0fbadfae86caff91879bf40e902df690 Mon Sep 17 00:00:00 2001 From: Alex Kocharin Date: Mon, 21 Dec 2020 19:23:56 +0300 Subject: [PATCH] Add replacer similar to one in JSON.stringify fix https://github.com/nodeca/js-yaml/issues/339 --- CHANGELOG.md | 1 + README.md | 1 + lib/dumper.js | 43 ++++++++-- test/units/replacer.js | 183 +++++++++++++++++++++++++++++++++++++++++ 4 files changed, 221 insertions(+), 7 deletions(-) create mode 100644 test/units/replacer.js diff --git a/CHANGELOG.md b/CHANGELOG.md index b42df10a..9ea8c9dc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,6 +33,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 string literal style, #290, #529. - Added `styles: { '!!null': 'empty' }` option for dumper (serializes `{ foo: null }` as "`foo: `"), #570. +- Added `replacer` option (similar to option in JSON.stringify), #339. ### Fixed - Astral characters are no longer encoded by dump/safeDump, #587. diff --git a/README.md b/README.md index 1c72d85c..90a8fe18 100644 --- a/README.md +++ b/README.md @@ -137,6 +137,7 @@ options: - `condenseFlow` _(default: `false`)_ - if `true` flow sequences will be condensed, omitting the space between `a, b`. Eg. `'[a,b]'`, and omitting the space between `key: value` and quoting the key. Eg. `'{"a":b}'` Can be useful when using yaml for pretty URL query params as spaces are %-encoded. - `quotingType` _(`'` or `"`, default: `'`)_ - strings will be quoted using this quoting style. If you specify single quotes, double quotes will still be used for non-printable characters. - `forceQuotes` _(default: `false`)_ - if `true`, all non-key strings will be quoted even if they normally don't need to. +- `replacer` - callback `function (key, value)` called recursively on each key/value in source object (see `replacer` docs for `JSON.stringify`). The following table show availlable styles (e.g. "canonical", "binary"...) available for each tag (.e.g. !!null, !!int ...). Yaml diff --git a/lib/dumper.js b/lib/dumper.js index 48730971..6b32fcb8 100644 --- a/lib/dumper.js +++ b/lib/dumper.js @@ -126,6 +126,7 @@ function State(options) { this.condenseFlow = options['condenseFlow'] || false; this.quotingType = options['quotingType'] === '"' ? QUOTING_TYPE_DOUBLE : QUOTING_TYPE_SINGLE; this.forceQuotes = options['forceQuotes'] || false; + this.replacer = typeof options['replacer'] === 'function' ? options['replacer'] : null; this.implicitTypes = this.schema.compiledImplicit; this.explicitTypes = this.schema.compiledExplicit; @@ -562,12 +563,19 @@ function writeFlowSequence(state, level, object) { var _result = '', _tag = state.tag, index, - length; + length, + value; for (index = 0, length = object.length; index < length; index += 1) { + value = object[index]; + + if (state.replacer) { + value = state.replacer.call(object, String(index), value); + } + // Write only valid elements, put null instead of invalid elements. - if (writeNode(state, level, object[index], false, false) || - (typeof object[index] === 'undefined' && + if (writeNode(state, level, value, false, false) || + (typeof value === 'undefined' && writeNode(state, level, null, false, false))) { if (_result !== '') _result += ',' + (!state.condenseFlow ? ' ' : ''); @@ -583,12 +591,19 @@ function writeBlockSequence(state, level, object, compact) { var _result = '', _tag = state.tag, index, - length; + length, + value; for (index = 0, length = object.length; index < length; index += 1) { + value = object[index]; + + if (state.replacer) { + value = state.replacer.call(object, String(index), value); + } + // Write only valid elements, put null instead of invalid elements. - if (writeNode(state, level + 1, object[index], true, true, false, true) || - (typeof object[index] === 'undefined' && + if (writeNode(state, level + 1, value, true, true, false, true) || + (typeof value === 'undefined' && writeNode(state, level + 1, null, true, true, false, true))) { if (!compact || _result !== '') { @@ -629,6 +644,10 @@ function writeFlowMapping(state, level, object) { objectKey = objectKeyList[index]; objectValue = object[objectKey]; + if (state.replacer) { + objectValue = state.replacer.call(object, objectKey, objectValue); + } + if (!writeNode(state, level, objectKey, false, false)) { continue; // Skip this pair because of invalid key; } @@ -684,6 +703,10 @@ function writeBlockMapping(state, level, object, compact) { objectKey = objectKeyList[index]; objectValue = object[objectKey]; + if (state.replacer) { + objectValue = state.replacer.call(object, objectKey, objectValue); + } + if (!writeNode(state, level + 1, objectKey, true, true, true)) { continue; // Skip this pair because of invalid key. } @@ -894,7 +917,13 @@ function dump(input, options) { if (!state.noRefs) getDuplicateReferences(input, state); - if (writeNode(state, 0, input, true, true)) return state.dump + '\n'; + var value = input; + + if (state.replacer) { + value = state.replacer.call({ '': value }, '', value); + } + + if (writeNode(state, 0, value, true, true)) return state.dump + '\n'; return ''; } diff --git a/test/units/replacer.js b/test/units/replacer.js new file mode 100644 index 00000000..81072d5e --- /dev/null +++ b/test/units/replacer.js @@ -0,0 +1,183 @@ +'use strict'; + +const assert = require('assert'); +const yaml = require('../..'); + + +describe('replacer', function () { + let undef = new yaml.Type('!undefined', { + kind: 'scalar', + resolve: () => true, + construct: () => {}, + predicate: object => typeof object === 'undefined', + represent: () => '' + }); + + let undef_schema = yaml.DEFAULT_SCHEMA.extend(undef); + + + it('should be called on the root of the document', function () { + let called = 0; + + let result = yaml.dump(42, { + replacer(key, value) { + called++; + assert.deepStrictEqual(this, { '': 42 }); + assert.strictEqual(key, ''); + assert.strictEqual(value, 42); + return 123; + } + }); + assert.strictEqual(result, '123\n'); + assert.strictEqual(called, 1); + + assert.strictEqual(yaml.dump(42, { + replacer(/* key, value */) {} + }), ''); + + assert.strictEqual(yaml.dump(42, { + replacer(/* key, value */) { return 'foo'; } + }), 'foo\n'); + }); + + + it('should be called in collections (block)', function () { + let called = 0; + + let result = yaml.dump([ 42 ], { + replacer(key, value) { + called++; + if (key === '' && called === 1) return value; + assert.deepStrictEqual(this, [ 42 ]); + assert.strictEqual(key, '0'); + assert.strictEqual(value, 42); + return 123; + }, + flowLevel: -1 + }); + assert.strictEqual(result, '- 123\n'); + assert.strictEqual(called, 2); + }); + + + it('should be called in collections (flow)', function () { + let called = 0; + + let result = yaml.dump([ 42 ], { + replacer(key, value) { + called++; + if (key === '' && called === 1) return value; + assert.deepStrictEqual(this, [ 42 ]); + assert.strictEqual(key, '0'); + assert.strictEqual(value, 42); + return 123; + }, + flowLevel: 0 + }); + assert.strictEqual(result, '[123]\n'); + assert.strictEqual(called, 2); + }); + + + it('should be called in mappings (block)', function () { + let called = 0; + + let result = yaml.dump({ a: 42 }, { + replacer(key, value) { + called++; + if (key === '' && called === 1) return value; + assert.deepStrictEqual(this, { a: 42 }); + assert.strictEqual(key, 'a'); + assert.strictEqual(value, 42); + return 123; + }, + flowLevel: -1 + }); + assert.strictEqual(result, 'a: 123\n'); + assert.strictEqual(called, 2); + }); + + + it('should be called in mappings (flow)', function () { + let called = 0; + + let result = yaml.dump({ a: 42 }, { + replacer(key, value) { + called++; + if (key === '' && called === 1) return value; + assert.deepStrictEqual(this, { a: 42 }); + assert.strictEqual(key, 'a'); + assert.strictEqual(value, 42); + return 123; + }, + flowLevel: 0 + }); + assert.strictEqual(result, '{a: 123}\n'); + assert.strictEqual(called, 2); + }); + + + it('undefined removes element from a mapping', function () { + let str, result; + + str = yaml.dump({ a: 1, b: 2, c: 3 }, { + replacer(key, value) { + if (key === 'b') return undefined; + return value; + } + }); + result = yaml.load(str); + assert.deepStrictEqual(result, { a: 1, c: 3 }); + + str = yaml.dump({ a: 1, b: 2, c: 3 }, { + replacer(key, value) { + if (key === 'b') return undefined; + return value; + }, + schema: undef_schema + }); + result = yaml.load(str, { schema: undef_schema }); + assert.deepStrictEqual(result, { a: 1, b: undefined, c: 3 }); + }); + + + it('undefined replaces element in an array with null', function () { + let str, result; + + str = yaml.dump([ 1, 2, 3 ], { + replacer(key, value) { + if (key === '1') return undefined; + return value; + } + }); + result = yaml.load(str); + assert.deepStrictEqual(result, [ 1, null, 3 ]); + + str = yaml.dump([ 1, 2, 3 ], { + replacer(key, value) { + if (key === '1') return undefined; + return value; + }, + schema: undef_schema + }); + result = yaml.load(str, { schema: undef_schema }); + assert.deepStrictEqual(result, [ 1, undefined, 3 ]); + }); + + + it('should recursively call replacer', function () { + let count = 0; + + let result = yaml.dump(42, { + replacer(key, value) { + return count++ > 3 ? value : { ['lvl' + count]: value }; + } + }); + assert.strictEqual(result, ` +lvl1: + lvl2: + lvl3: + lvl4: 42 +`.replace(/^\n/, '')); + }); +});