You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
169 lines
5.9 KiB
169 lines
5.9 KiB
import { isNode } from '../nodes/Node.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(/^(.*!)([^!]*)$/); |
|
if (!suffix) |
|
onError(`The ${source} tag has no suffix`); |
|
const prefix = this.tags[handle]; |
|
if (prefix) |
|
return prefix + decodeURIComponent(suffix); |
|
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 };
|
|
|