123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176 |
- 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 };
|