Skip to content

Commit 359b264

Browse files
committed
Add replacer similar to one in JSON.stringify
fix #339
1 parent 56d5616 commit 359b264

File tree

4 files changed

+221
-7
lines changed

4 files changed

+221
-7
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
3333
string literal style, #290, #529.
3434
- Added `styles: { '!!null': 'empty' }` option for dumper
3535
(serializes `{ foo: null }` as "`foo: `"), #570.
36+
- Added `replacer` option (similar to option in JSON.stringify), #339.
3637

3738
### Fixed
3839
- Astral characters are no longer encoded by dump/safeDump, #587.

README.md

+1
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,7 @@ options:
137137
- `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.
138138
- `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.
139139
- `forceQuotes` _(default: `false`)_ - if `true`, all non-key strings will be quoted even if they normally don't need to.
140+
- `replacer` - callback `function (key, value)` called recursively on each key/value in source object (see `replacer` docs for `JSON.stringify`).
140141

141142
The following table show availlable styles (e.g. "canonical",
142143
"binary"...) available for each tag (.e.g. !!null, !!int ...). Yaml

lib/dumper.js

+36-7
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,7 @@ function State(options) {
126126
this.condenseFlow = options['condenseFlow'] || false;
127127
this.quotingType = options['quotingType'] === '"' ? QUOTING_TYPE_DOUBLE : QUOTING_TYPE_SINGLE;
128128
this.forceQuotes = options['forceQuotes'] || false;
129+
this.replacer = typeof options['replacer'] === 'function' ? options['replacer'] : null;
129130

130131
this.implicitTypes = this.schema.compiledImplicit;
131132
this.explicitTypes = this.schema.compiledExplicit;
@@ -562,12 +563,19 @@ function writeFlowSequence(state, level, object) {
562563
var _result = '',
563564
_tag = state.tag,
564565
index,
565-
length;
566+
length,
567+
value;
566568

567569
for (index = 0, length = object.length; index < length; index += 1) {
570+
value = object[index];
571+
572+
if (state.replacer) {
573+
value = state.replacer.call(object, String(index), value);
574+
}
575+
568576
// Write only valid elements, put null instead of invalid elements.
569-
if (writeNode(state, level, object[index], false, false) ||
570-
(typeof object[index] === 'undefined' &&
577+
if (writeNode(state, level, value, false, false) ||
578+
(typeof value === 'undefined' &&
571579
writeNode(state, level, null, false, false))) {
572580

573581
if (_result !== '') _result += ',' + (!state.condenseFlow ? ' ' : '');
@@ -583,12 +591,19 @@ function writeBlockSequence(state, level, object, compact) {
583591
var _result = '',
584592
_tag = state.tag,
585593
index,
586-
length;
594+
length,
595+
value;
587596

588597
for (index = 0, length = object.length; index < length; index += 1) {
598+
value = object[index];
599+
600+
if (state.replacer) {
601+
value = state.replacer.call(object, String(index), value);
602+
}
603+
589604
// Write only valid elements, put null instead of invalid elements.
590-
if (writeNode(state, level + 1, object[index], true, true, false, true) ||
591-
(typeof object[index] === 'undefined' &&
605+
if (writeNode(state, level + 1, value, true, true, false, true) ||
606+
(typeof value === 'undefined' &&
592607
writeNode(state, level + 1, null, true, true, false, true))) {
593608

594609
if (!compact || _result !== '') {
@@ -629,6 +644,10 @@ function writeFlowMapping(state, level, object) {
629644
objectKey = objectKeyList[index];
630645
objectValue = object[objectKey];
631646

647+
if (state.replacer) {
648+
objectValue = state.replacer.call(object, objectKey, objectValue);
649+
}
650+
632651
if (!writeNode(state, level, objectKey, false, false)) {
633652
continue; // Skip this pair because of invalid key;
634653
}
@@ -684,6 +703,10 @@ function writeBlockMapping(state, level, object, compact) {
684703
objectKey = objectKeyList[index];
685704
objectValue = object[objectKey];
686705

706+
if (state.replacer) {
707+
objectValue = state.replacer.call(object, objectKey, objectValue);
708+
}
709+
687710
if (!writeNode(state, level + 1, objectKey, true, true, true)) {
688711
continue; // Skip this pair because of invalid key.
689712
}
@@ -894,7 +917,13 @@ function dump(input, options) {
894917

895918
if (!state.noRefs) getDuplicateReferences(input, state);
896919

897-
if (writeNode(state, 0, input, true, true)) return state.dump + '\n';
920+
var value = input;
921+
922+
if (state.replacer) {
923+
value = state.replacer.call({ '': value }, '', value);
924+
}
925+
926+
if (writeNode(state, 0, value, true, true)) return state.dump + '\n';
898927

899928
return '';
900929
}

test/units/replacer.js

+183
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
'use strict';
2+
3+
const assert = require('assert');
4+
const yaml = require('../..');
5+
6+
7+
describe('replacer', function () {
8+
let undef = new yaml.Type('!undefined', {
9+
kind: 'scalar',
10+
resolve: () => true,
11+
construct: () => {},
12+
predicate: object => typeof object === 'undefined',
13+
represent: () => ''
14+
});
15+
16+
let undef_schema = yaml.DEFAULT_SCHEMA.extend(undef);
17+
18+
19+
it('should be called on the root of the document', function () {
20+
let called = 0;
21+
22+
let result = yaml.dump(42, {
23+
replacer(key, value) {
24+
called++;
25+
assert.deepStrictEqual(this, { '': 42 });
26+
assert.strictEqual(key, '');
27+
assert.strictEqual(value, 42);
28+
return 123;
29+
}
30+
});
31+
assert.strictEqual(result, '123\n');
32+
assert.strictEqual(called, 1);
33+
34+
assert.strictEqual(yaml.dump(42, {
35+
replacer(/* key, value */) {}
36+
}), '');
37+
38+
assert.strictEqual(yaml.dump(42, {
39+
replacer(/* key, value */) { return 'foo'; }
40+
}), 'foo\n');
41+
});
42+
43+
44+
it('should be called in collections (block)', function () {
45+
let called = 0;
46+
47+
let result = yaml.dump([ 42 ], {
48+
replacer(key, value) {
49+
called++;
50+
if (key === '' && called === 1) return value;
51+
assert.deepStrictEqual(this, [ 42 ]);
52+
assert.strictEqual(key, '0');
53+
assert.strictEqual(value, 42);
54+
return 123;
55+
},
56+
flowLevel: -1
57+
});
58+
assert.strictEqual(result, '- 123\n');
59+
assert.strictEqual(called, 2);
60+
});
61+
62+
63+
it('should be called in collections (flow)', function () {
64+
let called = 0;
65+
66+
let result = yaml.dump([ 42 ], {
67+
replacer(key, value) {
68+
called++;
69+
if (key === '' && called === 1) return value;
70+
assert.deepStrictEqual(this, [ 42 ]);
71+
assert.strictEqual(key, '0');
72+
assert.strictEqual(value, 42);
73+
return 123;
74+
},
75+
flowLevel: 0
76+
});
77+
assert.strictEqual(result, '[123]\n');
78+
assert.strictEqual(called, 2);
79+
});
80+
81+
82+
it('should be called in mappings (block)', function () {
83+
let called = 0;
84+
85+
let result = yaml.dump({ a: 42 }, {
86+
replacer(key, value) {
87+
called++;
88+
if (key === '' && called === 1) return value;
89+
assert.deepStrictEqual(this, { a: 42 });
90+
assert.strictEqual(key, 'a');
91+
assert.strictEqual(value, 42);
92+
return 123;
93+
},
94+
flowLevel: -1
95+
});
96+
assert.strictEqual(result, 'a: 123\n');
97+
assert.strictEqual(called, 2);
98+
});
99+
100+
101+
it('should be called in mappings (flow)', function () {
102+
let called = 0;
103+
104+
let result = yaml.dump({ a: 42 }, {
105+
replacer(key, value) {
106+
called++;
107+
if (key === '' && called === 1) return value;
108+
assert.deepStrictEqual(this, { a: 42 });
109+
assert.strictEqual(key, 'a');
110+
assert.strictEqual(value, 42);
111+
return 123;
112+
},
113+
flowLevel: 0
114+
});
115+
assert.strictEqual(result, '{a: 123}\n');
116+
assert.strictEqual(called, 2);
117+
});
118+
119+
120+
it('undefined removes element from a mapping', function () {
121+
let str, result;
122+
123+
str = yaml.dump({ a: 1, b: 2, c: 3 }, {
124+
replacer(key, value) {
125+
if (key === 'b') return undefined;
126+
return value;
127+
}
128+
});
129+
result = yaml.load(str);
130+
assert.deepStrictEqual(result, { a: 1, c: 3 });
131+
132+
str = yaml.dump({ a: 1, b: 2, c: 3 }, {
133+
replacer(key, value) {
134+
if (key === 'b') return undefined;
135+
return value;
136+
},
137+
schema: undef_schema
138+
});
139+
result = yaml.load(str, { schema: undef_schema });
140+
assert.deepStrictEqual(result, { a: 1, b: undefined, c: 3 });
141+
});
142+
143+
144+
it('undefined replaces element in an array with null', function () {
145+
let str, result;
146+
147+
str = yaml.dump([ 1, 2, 3 ], {
148+
replacer(key, value) {
149+
if (key === '1') return undefined;
150+
return value;
151+
}
152+
});
153+
result = yaml.load(str);
154+
assert.deepStrictEqual(result, [ 1, null, 3 ]);
155+
156+
str = yaml.dump([ 1, 2, 3 ], {
157+
replacer(key, value) {
158+
if (key === '1') return undefined;
159+
return value;
160+
},
161+
schema: undef_schema
162+
});
163+
result = yaml.load(str, { schema: undef_schema });
164+
assert.deepStrictEqual(result, [ 1, undefined, 3 ]);
165+
});
166+
167+
168+
it('should recursively call replacer', function () {
169+
let count = 0;
170+
171+
let result = yaml.dump(42, {
172+
replacer(key, value) {
173+
return count++ > 3 ? value : { ['lvl' + count]: value };
174+
}
175+
});
176+
assert.strictEqual(result, `
177+
lvl1:
178+
lvl2:
179+
lvl3:
180+
lvl4: 42
181+
`.replace(/^\n/, ''));
182+
});
183+
});

0 commit comments

Comments
 (0)