|
|
'use strict';
const Char = { ANCHOR: '&', COMMENT: '#', TAG: '!', DIRECTIVES_END: '-', DOCUMENT_END: '.' }; const Type = { ALIAS: 'ALIAS', BLANK_LINE: 'BLANK_LINE', BLOCK_FOLDED: 'BLOCK_FOLDED', BLOCK_LITERAL: 'BLOCK_LITERAL', COMMENT: 'COMMENT', DIRECTIVE: 'DIRECTIVE', DOCUMENT: 'DOCUMENT', FLOW_MAP: 'FLOW_MAP', FLOW_SEQ: 'FLOW_SEQ', MAP: 'MAP', MAP_KEY: 'MAP_KEY', MAP_VALUE: 'MAP_VALUE', PLAIN: 'PLAIN', QUOTE_DOUBLE: 'QUOTE_DOUBLE', QUOTE_SINGLE: 'QUOTE_SINGLE', SEQ: 'SEQ', SEQ_ITEM: 'SEQ_ITEM' }; const defaultTagPrefix = 'tag:yaml.org,2002:'; const defaultTags = { MAP: 'tag:yaml.org,2002:map', SEQ: 'tag:yaml.org,2002:seq', STR: 'tag:yaml.org,2002:str' };
function findLineStarts(src) { const ls = [0]; let offset = src.indexOf('\n');
while (offset !== -1) { offset += 1; ls.push(offset); offset = src.indexOf('\n', offset); }
return ls; }
function getSrcInfo(cst) { let lineStarts, src;
if (typeof cst === 'string') { lineStarts = findLineStarts(cst); src = cst; } else { if (Array.isArray(cst)) cst = cst[0];
if (cst && cst.context) { if (!cst.lineStarts) cst.lineStarts = findLineStarts(cst.context.src); lineStarts = cst.lineStarts; src = cst.context.src; } }
return { lineStarts, src }; } /** * @typedef {Object} LinePos - One-indexed position in the source * @property {number} line * @property {number} col */
/** * Determine the line/col position matching a character offset. * * Accepts a source string or a CST document as the second parameter. With * the latter, starting indices for lines are cached in the document as * `lineStarts: number[]`. * * Returns a one-indexed `{ line, col }` location if found, or * `undefined` otherwise. * * @param {number} offset * @param {string|Document|Document[]} cst * @returns {?LinePos} */
function getLinePos(offset, cst) { if (typeof offset !== 'number' || offset < 0) return null; const { lineStarts, src } = getSrcInfo(cst); if (!lineStarts || !src || offset > src.length) return null;
for (let i = 0; i < lineStarts.length; ++i) { const start = lineStarts[i];
if (offset < start) { return { line: i, col: offset - lineStarts[i - 1] + 1 }; }
if (offset === start) return { line: i + 1, col: 1 }; }
const line = lineStarts.length; return { line, col: offset - lineStarts[line - 1] + 1 }; } /** * Get a specified line from the source. * * Accepts a source string or a CST document as the second parameter. With * the latter, starting indices for lines are cached in the document as * `lineStarts: number[]`. * * Returns the line as a string if found, or `null` otherwise. * * @param {number} line One-indexed line number * @param {string|Document|Document[]} cst * @returns {?string} */
function getLine(line, cst) { const { lineStarts, src } = getSrcInfo(cst); if (!lineStarts || !(line >= 1) || line > lineStarts.length) return null; const start = lineStarts[line - 1]; let end = lineStarts[line]; // undefined for last line; that's ok for slice()
while (end && end > start && src[end - 1] === '\n') --end;
return src.slice(start, end); } /** * Pretty-print the starting line from the source indicated by the range `pos` * * Trims output to `maxWidth` chars while keeping the starting column visible, * using `…` at either end to indicate dropped characters. * * Returns a two-line string (or `null`) with `\n` as separator; the second line * will hold appropriately indented `^` marks indicating the column range. * * @param {Object} pos * @param {LinePos} pos.start * @param {LinePos} [pos.end] * @param {string|Document|Document[]*} cst * @param {number} [maxWidth=80] * @returns {?string} */
function getPrettyContext({ start, end }, cst, maxWidth = 80) { let src = getLine(start.line, cst); if (!src) return null; let { col } = start;
if (src.length > maxWidth) { if (col <= maxWidth - 10) { src = src.substr(0, maxWidth - 1) + '…'; } else { const halfWidth = Math.round(maxWidth / 2); if (src.length > col + halfWidth) src = src.substr(0, col + halfWidth - 1) + '…'; col -= src.length - maxWidth; src = '…' + src.substr(1 - maxWidth); } }
let errLen = 1; let errEnd = '';
if (end) { if (end.line === start.line && col + (end.col - start.col) <= maxWidth + 1) { errLen = end.col - start.col; } else { errLen = Math.min(src.length + 1, maxWidth) - col; errEnd = '…'; } }
const offset = col > 1 ? ' '.repeat(col - 1) : ''; const err = '^'.repeat(errLen); return `${src}\n${offset}${err}${errEnd}`; }
class Range { static copy(orig) { return new Range(orig.start, orig.end); }
constructor(start, end) { this.start = start; this.end = end || start; }
isEmpty() { return typeof this.start !== 'number' || !this.end || this.end <= this.start; } /** * Set `origStart` and `origEnd` to point to the original source range for * this node, which may differ due to dropped CR characters. * * @param {number[]} cr - Positions of dropped CR characters * @param {number} offset - Starting index of `cr` from the last call * @returns {number} - The next offset, matching the one found for `origStart` */
setOrigRange(cr, offset) { const { start, end } = this;
if (cr.length === 0 || end <= cr[0]) { this.origStart = start; this.origEnd = end; return offset; }
let i = offset;
while (i < cr.length) { if (cr[i] > start) break;else ++i; }
this.origStart = start + i; const nextOffset = i;
while (i < cr.length) { // if end was at \n, it should now be at \r
if (cr[i] >= end) break;else ++i; }
this.origEnd = end + i; return nextOffset; }
}
/** Root class of all nodes */
class Node { static addStringTerminator(src, offset, str) { if (str[str.length - 1] === '\n') return str; const next = Node.endOfWhiteSpace(src, offset); return next >= src.length || src[next] === '\n' ? str + '\n' : str; } // ^(---|...)
static atDocumentBoundary(src, offset, sep) { const ch0 = src[offset]; if (!ch0) return true; const prev = src[offset - 1]; if (prev && prev !== '\n') return false;
if (sep) { if (ch0 !== sep) return false; } else { if (ch0 !== Char.DIRECTIVES_END && ch0 !== Char.DOCUMENT_END) return false; }
const ch1 = src[offset + 1]; const ch2 = src[offset + 2]; if (ch1 !== ch0 || ch2 !== ch0) return false; const ch3 = src[offset + 3]; return !ch3 || ch3 === '\n' || ch3 === '\t' || ch3 === ' '; }
static endOfIdentifier(src, offset) { let ch = src[offset]; const isVerbatim = ch === '<'; const notOk = isVerbatim ? ['\n', '\t', ' ', '>'] : ['\n', '\t', ' ', '[', ']', '{', '}', ','];
while (ch && notOk.indexOf(ch) === -1) ch = src[offset += 1];
if (isVerbatim && ch === '>') offset += 1; return offset; }
static endOfIndent(src, offset) { let ch = src[offset];
while (ch === ' ') ch = src[offset += 1];
return offset; }
static endOfLine(src, offset) { let ch = src[offset];
while (ch && ch !== '\n') ch = src[offset += 1];
return offset; }
static endOfWhiteSpace(src, offset) { let ch = src[offset];
while (ch === '\t' || ch === ' ') ch = src[offset += 1];
return offset; }
static startOfLine(src, offset) { let ch = src[offset - 1]; if (ch === '\n') return offset;
while (ch && ch !== '\n') ch = src[offset -= 1];
return offset + 1; } /** * End of indentation, or null if the line's indent level is not more * than `indent` * * @param {string} src * @param {number} indent * @param {number} lineStart * @returns {?number} */
static endOfBlockIndent(src, indent, lineStart) { const inEnd = Node.endOfIndent(src, lineStart);
if (inEnd > lineStart + indent) { return inEnd; } else { const wsEnd = Node.endOfWhiteSpace(src, inEnd); const ch = src[wsEnd]; if (!ch || ch === '\n') return wsEnd; }
return null; }
static atBlank(src, offset, endAsBlank) { const ch = src[offset]; return ch === '\n' || ch === '\t' || ch === ' ' || endAsBlank && !ch; }
static nextNodeIsIndented(ch, indentDiff, indicatorAsIndent) { if (!ch || indentDiff < 0) return false; if (indentDiff > 0) return true; return indicatorAsIndent && ch === '-'; } // should be at line or string end, or at next non-whitespace char
static normalizeOffset(src, offset) { const ch = src[offset]; return !ch ? offset : ch !== '\n' && src[offset - 1] === '\n' ? offset - 1 : Node.endOfWhiteSpace(src, offset); } // fold single newline into space, multiple newlines to N - 1 newlines
// presumes src[offset] === '\n'
static foldNewline(src, offset, indent) { let inCount = 0; let error = false; let fold = ''; let ch = src[offset + 1];
while (ch === ' ' || ch === '\t' || ch === '\n') { switch (ch) { case '\n': inCount = 0; offset += 1; fold += '\n'; break;
case '\t': if (inCount <= indent) error = true; offset = Node.endOfWhiteSpace(src, offset + 2) - 1; break;
case ' ': inCount += 1; offset += 1; break; }
ch = src[offset + 1]; }
if (!fold) fold = ' '; if (ch && inCount <= indent) error = true; return { fold, offset, error }; }
constructor(type, props, context) { Object.defineProperty(this, 'context', { value: context || null, writable: true }); this.error = null; this.range = null; this.valueRange = null; this.props = props || []; this.type = type; this.value = null; }
getPropValue(idx, key, skipKey) { if (!this.context) return null; const { src } = this.context; const prop = this.props[idx]; return prop && src[prop.start] === key ? src.slice(prop.start + (skipKey ? 1 : 0), prop.end) : null; }
get anchor() { for (let i = 0; i < this.props.length; ++i) { const anchor = this.getPropValue(i, Char.ANCHOR, true); if (anchor != null) return anchor; }
return null; }
get comment() { const comments = [];
for (let i = 0; i < this.props.length; ++i) { const comment = this.getPropValue(i, Char.COMMENT, true); if (comment != null) comments.push(comment); }
return comments.length > 0 ? comments.join('\n') : null; }
commentHasRequiredWhitespace(start) { const { src } = this.context; if (this.header && start === this.header.end) return false; if (!this.valueRange) return false; const { end } = this.valueRange; return start !== end || Node.atBlank(src, end - 1); }
get hasComment() { if (this.context) { const { src } = this.context;
for (let i = 0; i < this.props.length; ++i) { if (src[this.props[i].start] === Char.COMMENT) return true; } }
return false; }
get hasProps() { if (this.context) { const { src } = this.context;
for (let i = 0; i < this.props.length; ++i) { if (src[this.props[i].start] !== Char.COMMENT) return true; } }
return false; }
get includesTrailingLines() { return false; }
get jsonLike() { const jsonLikeTypes = [Type.FLOW_MAP, Type.FLOW_SEQ, Type.QUOTE_DOUBLE, Type.QUOTE_SINGLE]; return jsonLikeTypes.indexOf(this.type) !== -1; }
get rangeAsLinePos() { if (!this.range || !this.context) return undefined; const start = getLinePos(this.range.start, this.context.root); if (!start) return undefined; const end = getLinePos(this.range.end, this.context.root); return { start, end }; }
get rawValue() { if (!this.valueRange || !this.context) return null; const { start, end } = this.valueRange; return this.context.src.slice(start, end); }
get tag() { for (let i = 0; i < this.props.length; ++i) { const tag = this.getPropValue(i, Char.TAG, false);
if (tag != null) { if (tag[1] === '<') { return { verbatim: tag.slice(2, -1) }; } else { // eslint-disable-next-line no-unused-vars
const [_, handle, suffix] = tag.match(/^(.*!)([^!]*)$/); return { handle, suffix }; } } }
return null; }
get valueRangeContainsNewline() { if (!this.valueRange || !this.context) return false; const { start, end } = this.valueRange; const { src } = this.context;
for (let i = start; i < end; ++i) { if (src[i] === '\n') return true; }
return false; }
parseComment(start) { const { src } = this.context;
if (src[start] === Char.COMMENT) { const end = Node.endOfLine(src, start + 1); const commentRange = new Range(start, end); this.props.push(commentRange); return end; }
return start; } /** * Populates the `origStart` and `origEnd` values of all ranges for this * node. Extended by child classes to handle descendant nodes. * * @param {number[]} cr - Positions of dropped CR characters * @param {number} offset - Starting index of `cr` from the last call * @returns {number} - The next offset, matching the one found for `origStart` */
setOrigRanges(cr, offset) { if (this.range) offset = this.range.setOrigRange(cr, offset); if (this.valueRange) this.valueRange.setOrigRange(cr, offset); this.props.forEach(prop => prop.setOrigRange(cr, offset)); return offset; }
toString() { const { context: { src }, range, value } = this; if (value != null) return value; const str = src.slice(range.start, range.end); return Node.addStringTerminator(src, range.end, str); }
}
class YAMLError extends Error { constructor(name, source, message) { if (!message || !(source instanceof Node)) throw new Error(`Invalid arguments for new ${name}`); super(); this.name = name; this.message = message; this.source = source; }
makePretty() { if (!this.source) return; this.nodeType = this.source.type; const cst = this.source.context && this.source.context.root;
if (typeof this.offset === 'number') { this.range = new Range(this.offset, this.offset + 1); const start = cst && getLinePos(this.offset, cst);
if (start) { const end = { line: start.line, col: start.col + 1 }; this.linePos = { start, end }; }
delete this.offset; } else { this.range = this.source.range; this.linePos = this.source.rangeAsLinePos; }
if (this.linePos) { const { line, col } = this.linePos.start; this.message += ` at line ${line}, column ${col}`; const ctx = cst && getPrettyContext(this.linePos, cst); if (ctx) this.message += `:\n\n${ctx}\n`; }
delete this.source; }
} class YAMLReferenceError extends YAMLError { constructor(source, message) { super('YAMLReferenceError', source, message); }
} class YAMLSemanticError extends YAMLError { constructor(source, message) { super('YAMLSemanticError', source, message); }
} class YAMLSyntaxError extends YAMLError { constructor(source, message) { super('YAMLSyntaxError', source, message); }
} class YAMLWarning extends YAMLError { constructor(source, message) { super('YAMLWarning', source, message); }
}
function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; }
return obj; }
class PlainValue extends Node { static endOfLine(src, start, inFlow) { let ch = src[start]; let offset = start;
while (ch && ch !== '\n') { if (inFlow && (ch === '[' || ch === ']' || ch === '{' || ch === '}' || ch === ',')) break; const next = src[offset + 1]; if (ch === ':' && (!next || next === '\n' || next === '\t' || next === ' ' || inFlow && next === ',')) break; if ((ch === ' ' || ch === '\t') && next === '#') break; offset += 1; ch = next; }
return offset; }
get strValue() { if (!this.valueRange || !this.context) return null; let { start, end } = this.valueRange; const { src } = this.context; let ch = src[end - 1];
while (start < end && (ch === '\n' || ch === '\t' || ch === ' ')) ch = src[--end - 1];
let str = '';
for (let i = start; i < end; ++i) { const ch = src[i];
if (ch === '\n') { const { fold, offset } = Node.foldNewline(src, i, -1); str += fold; i = offset; } else if (ch === ' ' || ch === '\t') { // trim trailing whitespace
const wsStart = i; let next = src[i + 1];
while (i < end && (next === ' ' || next === '\t')) { i += 1; next = src[i + 1]; }
if (next !== '\n') str += i > wsStart ? src.slice(wsStart, i + 1) : ch; } else { str += ch; } }
const ch0 = src[start];
switch (ch0) { case '\t': { const msg = 'Plain value cannot start with a tab character'; const errors = [new YAMLSemanticError(this, msg)]; return { errors, str }; }
case '@': case '`': { const msg = `Plain value cannot start with reserved character ${ch0}`; const errors = [new YAMLSemanticError(this, msg)]; return { errors, str }; }
default: return str; } }
parseBlockValue(start) { const { indent, inFlow, src } = this.context; let offset = start; let valueEnd = start;
for (let ch = src[offset]; ch === '\n'; ch = src[offset]) { if (Node.atDocumentBoundary(src, offset + 1)) break; const end = Node.endOfBlockIndent(src, indent, offset + 1); if (end === null || src[end] === '#') break;
if (src[end] === '\n') { offset = end; } else { valueEnd = PlainValue.endOfLine(src, end, inFlow); offset = valueEnd; } }
if (this.valueRange.isEmpty()) this.valueRange.start = start; this.valueRange.end = valueEnd; return valueEnd; } /** * Parses a plain value from the source * * Accepted forms are: * ```
* #comment * * first line * * first line #comment * * first line * block * lines * * #comment * block * lines * ```
* where block lines are empty or have an indent level greater than `indent`. * * @param {ParseContext} context * @param {number} start - Index of first character * @returns {number} - Index of the character after this scalar, may be `\n` */
parse(context, start) { this.context = context; const { inFlow, src } = context; let offset = start; const ch = src[offset];
if (ch && ch !== '#' && ch !== '\n') { offset = PlainValue.endOfLine(src, start, inFlow); }
this.valueRange = new Range(start, offset); offset = Node.endOfWhiteSpace(src, offset); offset = this.parseComment(offset);
if (!this.hasComment || this.valueRange.isEmpty()) { offset = this.parseBlockValue(offset); }
return offset; }
}
exports.Char = Char; exports.Node = Node; exports.PlainValue = PlainValue; exports.Range = Range; exports.Type = Type; exports.YAMLError = YAMLError; exports.YAMLReferenceError = YAMLReferenceError; exports.YAMLSemanticError = YAMLSemanticError; exports.YAMLSyntaxError = YAMLSyntaxError; exports.YAMLWarning = YAMLWarning; exports._defineProperty = _defineProperty; exports.defaultTagPrefix = defaultTagPrefix; exports.defaultTags = defaultTags;
|