Skip to content

Commit

Permalink
Add replacer similar to one in JSON.stringify
Browse files Browse the repository at this point in the history
fix #339
  • Loading branch information
rlidwka committed Dec 22, 2020
1 parent 56d5616 commit 359b264
Show file tree
Hide file tree
Showing 4 changed files with 221 additions and 7 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Expand Up @@ -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.
Expand Down
1 change: 1 addition & 0 deletions README.md
Expand Up @@ -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
Expand Down
43 changes: 36 additions & 7 deletions lib/dumper.js
Expand Up @@ -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;
Expand Down Expand Up @@ -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 ? ' ' : '');
Expand All @@ -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 !== '') {
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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.
}
Expand Down Expand Up @@ -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 '';
}
Expand Down
183 changes: 183 additions & 0 deletions 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/, ''));
});
});

0 comments on commit 359b264

Please sign in to comment.