Skip to content

Commit a0d0caa

Browse files
committed
Dump custom tags starting with ! as !tag instead of !<!tag>
1 parent 1ea8370 commit a0d0caa

File tree

7 files changed

+166
-15
lines changed

7 files changed

+166
-15
lines changed

CHANGELOG.md

+4-1
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2626
- `dump()` now serializes `undefined` as `null` in collections and removes keys with
2727
`undefined` in mappings, #571.
2828
- `dump()` with `skipInvalid=true` now serializes invalid items in collections as null.
29+
- Custom tags starting with `!` are now dumped as `!tag` instead of `!<!tag>`, #576.
2930

3031
### Added
3132
- Added `.mjs` (es modules) support.
@@ -37,12 +38,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
3738
- Custom `Tag` can now handle all tags or multiple tags with the same prefix, #385.
3839

3940
### Fixed
40-
- Astral characters are no longer encoded by dump/safeDump, #587.
41+
- Astral characters are no longer encoded by `dump()`, #587.
4142
- "duplicate mapping key" exception now points at the correct column, #452.
4243
- Extra commas in flow collections (e.g. `[foo,,bar]`) now throw an exception
4344
instead of producing null, #321.
4445
- `__proto__` key no longer overrides object prototype, #164.
4546
- Removed `bower.json`.
47+
- Tags are now url-decoded in `load()` and url-encoded in `dump()`
48+
(previously usage of custom non-ascii tags may have led to invalid YAML that can't be parsed).
4649

4750

4851
## [3.14.1] - 2020-12-07

examples/handle_unknown_types.js

+26-2
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,28 @@ const util = require('util');
66
const yaml = require('../');
77

88

9+
class CustomTag {
10+
constructor(type, data) {
11+
this.type = type;
12+
this.data = data;
13+
}
14+
}
15+
16+
917
const tags = [ 'scalar', 'sequence', 'mapping' ].map(function (kind) {
1018
// first argument here is a prefix, so this type will handle anything starting with !
1119
return new yaml.Type('!', {
1220
kind: kind,
1321
multi: true,
22+
representName: function (object) {
23+
return object.type;
24+
},
25+
represent: function (object) {
26+
return object.data;
27+
},
28+
instanceOf: CustomTag,
1429
construct: function (data, type) {
15-
return { type: type, data: data };
30+
return new CustomTag(type, data);
1631
}
1732
});
1833
});
@@ -21,10 +36,19 @@ const SCHEMA = yaml.DEFAULT_SCHEMA.extend(tags);
2136

2237
const data = `
2338
subject: Handling unknown types in JS-YAML
24-
scalar: !unknown_scalar_tag 123
39+
scalar: !unknown_scalar_tag foo bar
2540
sequence: !unknown_sequence_tag [ 1, 2, 3 ]
2641
mapping: !unknown_mapping_tag { foo: 1, bar: 2 }
2742
`;
2843

2944
const loaded = yaml.load(data, { schema: SCHEMA });
45+
46+
console.log('Parsed as:');
47+
console.log('-'.repeat(70));
3048
console.log(util.inspect(loaded, false, 20, true));
49+
50+
console.log('');
51+
console.log('');
52+
console.log('Dumped as:');
53+
console.log('-'.repeat(70));
54+
console.log(yaml.dump(loaded, { schema: SCHEMA }));

lib/dumper.js

+34-2
Original file line numberDiff line numberDiff line change
@@ -760,7 +760,15 @@ function detectType(state, object, explicit) {
760760
(!type.instanceOf || ((typeof object === 'object') && (object instanceof type.instanceOf))) &&
761761
(!type.predicate || type.predicate(object))) {
762762

763-
state.tag = explicit ? type.tag : '?';
763+
if (explicit) {
764+
if (type.multi && type.representName) {
765+
state.tag = type.representName(object);
766+
} else {
767+
state.tag = type.tag;
768+
}
769+
} else {
770+
state.tag = '?';
771+
}
764772

765773
if (type.represent) {
766774
style = state.styleMap[type.tag] || type.defaultStyle;
@@ -796,6 +804,7 @@ function writeNode(state, level, object, block, compact, iskey, isblockseq) {
796804

797805
var type = _toString.call(state.dump);
798806
var inblock = block;
807+
var tagStr;
799808

800809
if (block) {
801810
block = (state.flowLevel < 0 || state.flowLevel > level);
@@ -860,7 +869,30 @@ function writeNode(state, level, object, block, compact, iskey, isblockseq) {
860869
}
861870

862871
if (state.tag !== null && state.tag !== '?') {
863-
state.dump = '!<' + state.tag + '> ' + state.dump;
872+
// Need to encode all characters except those allowed by the spec:
873+
//
874+
// [35] ns-dec-digit ::= [#x30-#x39] /* 0-9 */
875+
// [36] ns-hex-digit ::= ns-dec-digit
876+
// | [#x41-#x46] /* A-F */ | [#x61-#x66] /* a-f */
877+
// [37] ns-ascii-letter ::= [#x41-#x5A] /* A-Z */ | [#x61-#x7A] /* a-z */
878+
// [38] ns-word-char ::= ns-dec-digit | ns-ascii-letter | “-”
879+
// [39] ns-uri-char ::= “%” ns-hex-digit ns-hex-digit | ns-word-char | “#”
880+
// | “;” | “/” | “?” | “:” | “@” | “&” | “=” | “+” | “$” | “,”
881+
// | “_” | “.” | “!” | “~” | “*” | “'” | “(” | “)” | “[” | “]”
882+
//
883+
// Also need to encode '!' because it has special meaning (end of tag prefix).
884+
//
885+
tagStr = encodeURI(
886+
state.tag[0] === '!' ? state.tag.slice(1) : state.tag
887+
).replace(/!/g, '%21');
888+
889+
if (state.tag[0] === '!') {
890+
tagStr = '!' + tagStr;
891+
} else {
892+
tagStr = '!<' + tagStr + '>';
893+
}
894+
895+
state.dump = tagStr + ' ' + state.dump;
864896
}
865897
}
866898

lib/loader.js

+12
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,12 @@ var directiveHandlers = {
248248
throwError(state, 'ill-formed tag prefix (second argument) of the TAG directive');
249249
}
250250

251+
try {
252+
prefix = decodeURIComponent(prefix);
253+
} catch (err) {
254+
throwError(state, 'tag prefix is malformed: ' + prefix);
255+
}
256+
251257
state.tagMap[handle] = prefix;
252258
}
253259
};
@@ -1249,6 +1255,12 @@ function readTagProperty(state) {
12491255
throwError(state, 'tag name cannot contain such characters: ' + tagName);
12501256
}
12511257

1258+
try {
1259+
tagName = decodeURIComponent(tagName);
1260+
} catch (err) {
1261+
throwError(state, 'tag name is malformed: ' + tagName);
1262+
}
1263+
12521264
if (isVerbatim) {
12531265
state.tag = tagName;
12541266

lib/type.js

+12-10
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ var TYPE_CONSTRUCTOR_OPTIONS = [
1010
'instanceOf',
1111
'predicate',
1212
'represent',
13+
'representName',
1314
'defaultStyle',
1415
'styleAliases'
1516
];
@@ -44,16 +45,17 @@ function Type(tag, options) {
4445
});
4546

4647
// TODO: Add tag format check.
47-
this.tag = tag;
48-
this.kind = options['kind'] || null;
49-
this.resolve = options['resolve'] || function () { return true; };
50-
this.construct = options['construct'] || function (data) { return data; };
51-
this.instanceOf = options['instanceOf'] || null;
52-
this.predicate = options['predicate'] || null;
53-
this.represent = options['represent'] || null;
54-
this.defaultStyle = options['defaultStyle'] || null;
55-
this.multi = options['multi'] || false;
56-
this.styleAliases = compileStyleAliases(options['styleAliases'] || null);
48+
this.tag = tag;
49+
this.kind = options['kind'] || null;
50+
this.resolve = options['resolve'] || function () { return true; };
51+
this.construct = options['construct'] || function (data) { return data; };
52+
this.instanceOf = options['instanceOf'] || null;
53+
this.predicate = options['predicate'] || null;
54+
this.represent = options['represent'] || null;
55+
this.representName = options['representName'] || null;
56+
this.defaultStyle = options['defaultStyle'] || null;
57+
this.multi = options['multi'] || false;
58+
this.styleAliases = compileStyleAliases(options['styleAliases'] || null);
5759

5860
if (YAML_NODE_KINDS.indexOf(this.kind) === -1) {
5961
throw new YAMLException('Unknown kind "' + this.kind + '" is specified for "' + tag + '" YAML type.');

test/issues/0385.js

+25
Original file line numberDiff line numberDiff line change
@@ -96,4 +96,29 @@ describe('Multi tag', function () {
9696
schema: schema
9797
}), expected);
9898
});
99+
100+
101+
it('should dump multi types with custom tag', function () {
102+
let tags = [
103+
new yaml.Type('!', {
104+
kind: 'scalar',
105+
multi: true,
106+
predicate: function (obj) {
107+
return !!obj.tag;
108+
},
109+
representName: function (obj) {
110+
return obj.tag;
111+
},
112+
represent: function (obj) {
113+
return obj.value;
114+
}
115+
})
116+
];
117+
118+
let schema = yaml.DEFAULT_SCHEMA.extend(tags);
119+
120+
assert.strictEqual(yaml.dump({ test: { tag: 'foo', value: 'bar' } }, {
121+
schema: schema
122+
}), 'test: !<foo> bar\n');
123+
});
99124
});

test/issues/0576.js

+53
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
'use strict';
2+
3+
4+
const assert = require('assert');
5+
const yaml = require('../../');
6+
7+
8+
describe('Custom tags', function () {
9+
let tag_names = [ 'tag', '!tag', '!!tag', '!<!tag>', 'tag*-!< >{\n}', '!tagαβγ' ];
10+
let encoded = [ '!<tag>', '!tag', '!%21tag', '!%3C%21tag%3E',
11+
'!<tag*-%21%3C%20%3E%7B%0A%7D>', '!tag%CE%B1%CE%B2%CE%B3' ];
12+
13+
let tags = tag_names.map(tag =>
14+
new yaml.Type(tag, {
15+
kind: 'scalar',
16+
resolve: () => true,
17+
construct: object => [ tag, object ],
18+
predicate: object => object.tag === tag,
19+
represent: () => 'value'
20+
})
21+
);
22+
23+
let schema = yaml.DEFAULT_SCHEMA.extend(tags);
24+
25+
26+
it('Should dump tags with proper encoding', function () {
27+
tag_names.forEach(function (tag, idx) {
28+
assert.strictEqual(yaml.dump({ tag }, { schema }), encoded[idx] + ' value\n');
29+
});
30+
});
31+
32+
33+
it('Should decode tags when loading', function () {
34+
encoded.forEach(function (tag, idx) {
35+
assert.deepStrictEqual(yaml.load(tag + ' value', { schema }), [ tag_names[idx], 'value' ]);
36+
});
37+
});
38+
39+
40+
it('Should url-decode built-in tags', function () {
41+
assert.strictEqual(yaml.load('!!%69nt 123'), 123);
42+
assert.strictEqual(yaml.load('!!%73tr 123'), '123');
43+
});
44+
45+
46+
it('Should url-decode %TAG prefix', function () {
47+
assert.deepStrictEqual(yaml.load(`
48+
%TAG !xx! %74a
49+
---
50+
!xx!g 123
51+
`, { schema }), [ 'tag', '123' ]);
52+
});
53+
});

0 commit comments

Comments
 (0)