177 lines
6.0 KiB
JavaScript
177 lines
6.0 KiB
JavaScript
|
import { isNode } from '../nodes/identity.js';
|
||
|
import { visit } from '../visit.js';
|
||
|
|
||
|
const escapeChars = {
|
||
|
'!': '%21',
|
||
|
',': '%2C',
|
||
|
'[': '%5B',
|
||
|
']': '%5D',
|
||
|
'{': '%7B',
|
||
|
'}': '%7D'
|
||
|
};
|
||
|
const escapeTagName = (tn) => tn.replace(/[!,[\]{}]/g, ch => escapeChars[ch]);
|
||
|
class Directives {
|
||
|
constructor(yaml, tags) {
|
||
|
/**
|
||
|
* The directives-end/doc-start marker `---`. If `null`, a marker may still be
|
||
|
* included in the document's stringified representation.
|
||
|
*/
|
||
|
this.docStart = null;
|
||
|
/** The doc-end marker `...`. */
|
||
|
this.docEnd = false;
|
||
|
this.yaml = Object.assign({}, Directives.defaultYaml, yaml);
|
||
|
this.tags = Object.assign({}, Directives.defaultTags, tags);
|
||
|
}
|
||
|
clone() {
|
||
|
const copy = new Directives(this.yaml, this.tags);
|
||
|
copy.docStart = this.docStart;
|
||
|
return copy;
|
||
|
}
|
||
|
/**
|
||
|
* During parsing, get a Directives instance for the current document and
|
||
|
* update the stream state according to the current version's spec.
|
||
|
*/
|
||
|
atDocument() {
|
||
|
const res = new Directives(this.yaml, this.tags);
|
||
|
switch (this.yaml.version) {
|
||
|
case '1.1':
|
||
|
this.atNextDocument = true;
|
||
|
break;
|
||
|
case '1.2':
|
||
|
this.atNextDocument = false;
|
||
|
this.yaml = {
|
||
|
explicit: Directives.defaultYaml.explicit,
|
||
|
version: '1.2'
|
||
|
};
|
||
|
this.tags = Object.assign({}, Directives.defaultTags);
|
||
|
break;
|
||
|
}
|
||
|
return res;
|
||
|
}
|
||
|
/**
|
||
|
* @param onError - May be called even if the action was successful
|
||
|
* @returns `true` on success
|
||
|
*/
|
||
|
add(line, onError) {
|
||
|
if (this.atNextDocument) {
|
||
|
this.yaml = { explicit: Directives.defaultYaml.explicit, version: '1.1' };
|
||
|
this.tags = Object.assign({}, Directives.defaultTags);
|
||
|
this.atNextDocument = false;
|
||
|
}
|
||
|
const parts = line.trim().split(/[ \t]+/);
|
||
|
const name = parts.shift();
|
||
|
switch (name) {
|
||
|
case '%TAG': {
|
||
|
if (parts.length !== 2) {
|
||
|
onError(0, '%TAG directive should contain exactly two parts');
|
||
|
if (parts.length < 2)
|
||
|
return false;
|
||
|
}
|
||
|
const [handle, prefix] = parts;
|
||
|
this.tags[handle] = prefix;
|
||
|
return true;
|
||
|
}
|
||
|
case '%YAML': {
|
||
|
this.yaml.explicit = true;
|
||
|
if (parts.length !== 1) {
|
||
|
onError(0, '%YAML directive should contain exactly one part');
|
||
|
return false;
|
||
|
}
|
||
|
const [version] = parts;
|
||
|
if (version === '1.1' || version === '1.2') {
|
||
|
this.yaml.version = version;
|
||
|
return true;
|
||
|
}
|
||
|
else {
|
||
|
const isValid = /^\d+\.\d+$/.test(version);
|
||
|
onError(6, `Unsupported YAML version ${version}`, isValid);
|
||
|
return false;
|
||
|
}
|
||
|
}
|
||
|
default:
|
||
|
onError(0, `Unknown directive ${name}`, true);
|
||
|
return false;
|
||
|
}
|
||
|
}
|
||
|
/**
|
||
|
* Resolves a tag, matching handles to those defined in %TAG directives.
|
||
|
*
|
||
|
* @returns Resolved tag, which may also be the non-specific tag `'!'` or a
|
||
|
* `'!local'` tag, or `null` if unresolvable.
|
||
|
*/
|
||
|
tagName(source, onError) {
|
||
|
if (source === '!')
|
||
|
return '!'; // non-specific tag
|
||
|
if (source[0] !== '!') {
|
||
|
onError(`Not a valid tag: ${source}`);
|
||
|
return null;
|
||
|
}
|
||
|
if (source[1] === '<') {
|
||
|
const verbatim = source.slice(2, -1);
|
||
|
if (verbatim === '!' || verbatim === '!!') {
|
||
|
onError(`Verbatim tags aren't resolved, so ${source} is invalid.`);
|
||
|
return null;
|
||
|
}
|
||
|
if (source[source.length - 1] !== '>')
|
||
|
onError('Verbatim tags must end with a >');
|
||
|
return verbatim;
|
||
|
}
|
||
|
const [, handle, suffix] = source.match(/^(.*!)([^!]*)$/s);
|
||
|
if (!suffix)
|
||
|
onError(`The ${source} tag has no suffix`);
|
||
|
const prefix = this.tags[handle];
|
||
|
if (prefix) {
|
||
|
try {
|
||
|
return prefix + decodeURIComponent(suffix);
|
||
|
}
|
||
|
catch (error) {
|
||
|
onError(String(error));
|
||
|
return null;
|
||
|
}
|
||
|
}
|
||
|
if (handle === '!')
|
||
|
return source; // local tag
|
||
|
onError(`Could not resolve tag: ${source}`);
|
||
|
return null;
|
||
|
}
|
||
|
/**
|
||
|
* Given a fully resolved tag, returns its printable string form,
|
||
|
* taking into account current tag prefixes and defaults.
|
||
|
*/
|
||
|
tagString(tag) {
|
||
|
for (const [handle, prefix] of Object.entries(this.tags)) {
|
||
|
if (tag.startsWith(prefix))
|
||
|
return handle + escapeTagName(tag.substring(prefix.length));
|
||
|
}
|
||
|
return tag[0] === '!' ? tag : `!<${tag}>`;
|
||
|
}
|
||
|
toString(doc) {
|
||
|
const lines = this.yaml.explicit
|
||
|
? [`%YAML ${this.yaml.version || '1.2'}`]
|
||
|
: [];
|
||
|
const tagEntries = Object.entries(this.tags);
|
||
|
let tagNames;
|
||
|
if (doc && tagEntries.length > 0 && isNode(doc.contents)) {
|
||
|
const tags = {};
|
||
|
visit(doc.contents, (_key, node) => {
|
||
|
if (isNode(node) && node.tag)
|
||
|
tags[node.tag] = true;
|
||
|
});
|
||
|
tagNames = Object.keys(tags);
|
||
|
}
|
||
|
else
|
||
|
tagNames = [];
|
||
|
for (const [handle, prefix] of tagEntries) {
|
||
|
if (handle === '!!' && prefix === 'tag:yaml.org,2002:')
|
||
|
continue;
|
||
|
if (!doc || tagNames.some(tn => tn.startsWith(prefix)))
|
||
|
lines.push(`%TAG ${handle} ${prefix}`);
|
||
|
}
|
||
|
return lines.join('\n');
|
||
|
}
|
||
|
}
|
||
|
Directives.defaultYaml = { explicit: false, version: '1.2' };
|
||
|
Directives.defaultTags = { '!!': 'tag:yaml.org,2002:' };
|
||
|
|
||
|
export { Directives };
|