UNPKG

6.24 kBJavaScriptView Raw
1'use strict';
2
3var identity = require('../nodes/identity.js');
4var visit = require('../visit.js');
5
6const escapeChars = {
7 '!': '%21',
8 ',': '%2C',
9 '[': '%5B',
10 ']': '%5D',
11 '{': '%7B',
12 '}': '%7D'
13};
14const escapeTagName = (tn) => tn.replace(/[!,[\]{}]/g, ch => escapeChars[ch]);
15class Directives {
16 constructor(yaml, tags) {
17 /**
18 * The directives-end/doc-start marker `---`. If `null`, a marker may still be
19 * included in the document's stringified representation.
20 */
21 this.docStart = null;
22 /** The doc-end marker `...`. */
23 this.docEnd = false;
24 this.yaml = Object.assign({}, Directives.defaultYaml, yaml);
25 this.tags = Object.assign({}, Directives.defaultTags, tags);
26 }
27 clone() {
28 const copy = new Directives(this.yaml, this.tags);
29 copy.docStart = this.docStart;
30 return copy;
31 }
32 /**
33 * During parsing, get a Directives instance for the current document and
34 * update the stream state according to the current version's spec.
35 */
36 atDocument() {
37 const res = new Directives(this.yaml, this.tags);
38 switch (this.yaml.version) {
39 case '1.1':
40 this.atNextDocument = true;
41 break;
42 case '1.2':
43 this.atNextDocument = false;
44 this.yaml = {
45 explicit: Directives.defaultYaml.explicit,
46 version: '1.2'
47 };
48 this.tags = Object.assign({}, Directives.defaultTags);
49 break;
50 }
51 return res;
52 }
53 /**
54 * @param onError - May be called even if the action was successful
55 * @returns `true` on success
56 */
57 add(line, onError) {
58 if (this.atNextDocument) {
59 this.yaml = { explicit: Directives.defaultYaml.explicit, version: '1.1' };
60 this.tags = Object.assign({}, Directives.defaultTags);
61 this.atNextDocument = false;
62 }
63 const parts = line.trim().split(/[ \t]+/);
64 const name = parts.shift();
65 switch (name) {
66 case '%TAG': {
67 if (parts.length !== 2) {
68 onError(0, '%TAG directive should contain exactly two parts');
69 if (parts.length < 2)
70 return false;
71 }
72 const [handle, prefix] = parts;
73 this.tags[handle] = prefix;
74 return true;
75 }
76 case '%YAML': {
77 this.yaml.explicit = true;
78 if (parts.length !== 1) {
79 onError(0, '%YAML directive should contain exactly one part');
80 return false;
81 }
82 const [version] = parts;
83 if (version === '1.1' || version === '1.2') {
84 this.yaml.version = version;
85 return true;
86 }
87 else {
88 const isValid = /^\d+\.\d+$/.test(version);
89 onError(6, `Unsupported YAML version ${version}`, isValid);
90 return false;
91 }
92 }
93 default:
94 onError(0, `Unknown directive ${name}`, true);
95 return false;
96 }
97 }
98 /**
99 * Resolves a tag, matching handles to those defined in %TAG directives.
100 *
101 * @returns Resolved tag, which may also be the non-specific tag `'!'` or a
102 * `'!local'` tag, or `null` if unresolvable.
103 */
104 tagName(source, onError) {
105 if (source === '!')
106 return '!'; // non-specific tag
107 if (source[0] !== '!') {
108 onError(`Not a valid tag: ${source}`);
109 return null;
110 }
111 if (source[1] === '<') {
112 const verbatim = source.slice(2, -1);
113 if (verbatim === '!' || verbatim === '!!') {
114 onError(`Verbatim tags aren't resolved, so ${source} is invalid.`);
115 return null;
116 }
117 if (source[source.length - 1] !== '>')
118 onError('Verbatim tags must end with a >');
119 return verbatim;
120 }
121 const [, handle, suffix] = source.match(/^(.*!)([^!]*)$/s);
122 if (!suffix)
123 onError(`The ${source} tag has no suffix`);
124 const prefix = this.tags[handle];
125 if (prefix) {
126 try {
127 return prefix + decodeURIComponent(suffix);
128 }
129 catch (error) {
130 onError(String(error));
131 return null;
132 }
133 }
134 if (handle === '!')
135 return source; // local tag
136 onError(`Could not resolve tag: ${source}`);
137 return null;
138 }
139 /**
140 * Given a fully resolved tag, returns its printable string form,
141 * taking into account current tag prefixes and defaults.
142 */
143 tagString(tag) {
144 for (const [handle, prefix] of Object.entries(this.tags)) {
145 if (tag.startsWith(prefix))
146 return handle + escapeTagName(tag.substring(prefix.length));
147 }
148 return tag[0] === '!' ? tag : `!<${tag}>`;
149 }
150 toString(doc) {
151 const lines = this.yaml.explicit
152 ? [`%YAML ${this.yaml.version || '1.2'}`]
153 : [];
154 const tagEntries = Object.entries(this.tags);
155 let tagNames;
156 if (doc && tagEntries.length > 0 && identity.isNode(doc.contents)) {
157 const tags = {};
158 visit.visit(doc.contents, (_key, node) => {
159 if (identity.isNode(node) && node.tag)
160 tags[node.tag] = true;
161 });
162 tagNames = Object.keys(tags);
163 }
164 else
165 tagNames = [];
166 for (const [handle, prefix] of tagEntries) {
167 if (handle === '!!' && prefix === 'tag:yaml.org,2002:')
168 continue;
169 if (!doc || tagNames.some(tn => tn.startsWith(prefix)))
170 lines.push(`%TAG ${handle} ${prefix}`);
171 }
172 return lines.join('\n');
173 }
174}
175Directives.defaultYaml = { explicit: false, version: '1.2' };
176Directives.defaultTags = { '!!': 'tag:yaml.org,2002:' };
177
178exports.Directives = Directives;