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