201 lines
7.3 KiB
JavaScript
201 lines
7.3 KiB
JavaScript
|
'use strict';
|
||
|
|
||
|
var Scalar = require('../nodes/Scalar.js');
|
||
|
|
||
|
function resolveBlockScalar(ctx, scalar, onError) {
|
||
|
const start = scalar.offset;
|
||
|
const header = parseBlockScalarHeader(scalar, ctx.options.strict, onError);
|
||
|
if (!header)
|
||
|
return { value: '', type: null, comment: '', range: [start, start, start] };
|
||
|
const type = header.mode === '>' ? Scalar.Scalar.BLOCK_FOLDED : Scalar.Scalar.BLOCK_LITERAL;
|
||
|
const lines = scalar.source ? splitLines(scalar.source) : [];
|
||
|
// determine the end of content & start of chomping
|
||
|
let chompStart = lines.length;
|
||
|
for (let i = lines.length - 1; i >= 0; --i) {
|
||
|
const content = lines[i][1];
|
||
|
if (content === '' || content === '\r')
|
||
|
chompStart = i;
|
||
|
else
|
||
|
break;
|
||
|
}
|
||
|
// shortcut for empty contents
|
||
|
if (chompStart === 0) {
|
||
|
const value = header.chomp === '+' && lines.length > 0
|
||
|
? '\n'.repeat(Math.max(1, lines.length - 1))
|
||
|
: '';
|
||
|
let end = start + header.length;
|
||
|
if (scalar.source)
|
||
|
end += scalar.source.length;
|
||
|
return { value, type, comment: header.comment, range: [start, end, end] };
|
||
|
}
|
||
|
// find the indentation level to trim from start
|
||
|
let trimIndent = scalar.indent + header.indent;
|
||
|
let offset = scalar.offset + header.length;
|
||
|
let contentStart = 0;
|
||
|
for (let i = 0; i < chompStart; ++i) {
|
||
|
const [indent, content] = lines[i];
|
||
|
if (content === '' || content === '\r') {
|
||
|
if (header.indent === 0 && indent.length > trimIndent)
|
||
|
trimIndent = indent.length;
|
||
|
}
|
||
|
else {
|
||
|
if (indent.length < trimIndent) {
|
||
|
const message = 'Block scalars with more-indented leading empty lines must use an explicit indentation indicator';
|
||
|
onError(offset + indent.length, 'MISSING_CHAR', message);
|
||
|
}
|
||
|
if (header.indent === 0)
|
||
|
trimIndent = indent.length;
|
||
|
contentStart = i;
|
||
|
if (trimIndent === 0 && !ctx.atRoot) {
|
||
|
const message = 'Block scalar values in collections must be indented';
|
||
|
onError(offset, 'BAD_INDENT', message);
|
||
|
}
|
||
|
break;
|
||
|
}
|
||
|
offset += indent.length + content.length + 1;
|
||
|
}
|
||
|
// include trailing more-indented empty lines in content
|
||
|
for (let i = lines.length - 1; i >= chompStart; --i) {
|
||
|
if (lines[i][0].length > trimIndent)
|
||
|
chompStart = i + 1;
|
||
|
}
|
||
|
let value = '';
|
||
|
let sep = '';
|
||
|
let prevMoreIndented = false;
|
||
|
// leading whitespace is kept intact
|
||
|
for (let i = 0; i < contentStart; ++i)
|
||
|
value += lines[i][0].slice(trimIndent) + '\n';
|
||
|
for (let i = contentStart; i < chompStart; ++i) {
|
||
|
let [indent, content] = lines[i];
|
||
|
offset += indent.length + content.length + 1;
|
||
|
const crlf = content[content.length - 1] === '\r';
|
||
|
if (crlf)
|
||
|
content = content.slice(0, -1);
|
||
|
/* istanbul ignore if already caught in lexer */
|
||
|
if (content && indent.length < trimIndent) {
|
||
|
const src = header.indent
|
||
|
? 'explicit indentation indicator'
|
||
|
: 'first line';
|
||
|
const message = `Block scalar lines must not be less indented than their ${src}`;
|
||
|
onError(offset - content.length - (crlf ? 2 : 1), 'BAD_INDENT', message);
|
||
|
indent = '';
|
||
|
}
|
||
|
if (type === Scalar.Scalar.BLOCK_LITERAL) {
|
||
|
value += sep + indent.slice(trimIndent) + content;
|
||
|
sep = '\n';
|
||
|
}
|
||
|
else if (indent.length > trimIndent || content[0] === '\t') {
|
||
|
// more-indented content within a folded block
|
||
|
if (sep === ' ')
|
||
|
sep = '\n';
|
||
|
else if (!prevMoreIndented && sep === '\n')
|
||
|
sep = '\n\n';
|
||
|
value += sep + indent.slice(trimIndent) + content;
|
||
|
sep = '\n';
|
||
|
prevMoreIndented = true;
|
||
|
}
|
||
|
else if (content === '') {
|
||
|
// empty line
|
||
|
if (sep === '\n')
|
||
|
value += '\n';
|
||
|
else
|
||
|
sep = '\n';
|
||
|
}
|
||
|
else {
|
||
|
value += sep + content;
|
||
|
sep = ' ';
|
||
|
prevMoreIndented = false;
|
||
|
}
|
||
|
}
|
||
|
switch (header.chomp) {
|
||
|
case '-':
|
||
|
break;
|
||
|
case '+':
|
||
|
for (let i = chompStart; i < lines.length; ++i)
|
||
|
value += '\n' + lines[i][0].slice(trimIndent);
|
||
|
if (value[value.length - 1] !== '\n')
|
||
|
value += '\n';
|
||
|
break;
|
||
|
default:
|
||
|
value += '\n';
|
||
|
}
|
||
|
const end = start + header.length + scalar.source.length;
|
||
|
return { value, type, comment: header.comment, range: [start, end, end] };
|
||
|
}
|
||
|
function parseBlockScalarHeader({ offset, props }, strict, onError) {
|
||
|
/* istanbul ignore if should not happen */
|
||
|
if (props[0].type !== 'block-scalar-header') {
|
||
|
onError(props[0], 'IMPOSSIBLE', 'Block scalar header not found');
|
||
|
return null;
|
||
|
}
|
||
|
const { source } = props[0];
|
||
|
const mode = source[0];
|
||
|
let indent = 0;
|
||
|
let chomp = '';
|
||
|
let error = -1;
|
||
|
for (let i = 1; i < source.length; ++i) {
|
||
|
const ch = source[i];
|
||
|
if (!chomp && (ch === '-' || ch === '+'))
|
||
|
chomp = ch;
|
||
|
else {
|
||
|
const n = Number(ch);
|
||
|
if (!indent && n)
|
||
|
indent = n;
|
||
|
else if (error === -1)
|
||
|
error = offset + i;
|
||
|
}
|
||
|
}
|
||
|
if (error !== -1)
|
||
|
onError(error, 'UNEXPECTED_TOKEN', `Block scalar header includes extra characters: ${source}`);
|
||
|
let hasSpace = false;
|
||
|
let comment = '';
|
||
|
let length = source.length;
|
||
|
for (let i = 1; i < props.length; ++i) {
|
||
|
const token = props[i];
|
||
|
switch (token.type) {
|
||
|
case 'space':
|
||
|
hasSpace = true;
|
||
|
// fallthrough
|
||
|
case 'newline':
|
||
|
length += token.source.length;
|
||
|
break;
|
||
|
case 'comment':
|
||
|
if (strict && !hasSpace) {
|
||
|
const message = 'Comments must be separated from other tokens by white space characters';
|
||
|
onError(token, 'MISSING_CHAR', message);
|
||
|
}
|
||
|
length += token.source.length;
|
||
|
comment = token.source.substring(1);
|
||
|
break;
|
||
|
case 'error':
|
||
|
onError(token, 'UNEXPECTED_TOKEN', token.message);
|
||
|
length += token.source.length;
|
||
|
break;
|
||
|
/* istanbul ignore next should not happen */
|
||
|
default: {
|
||
|
const message = `Unexpected token in block scalar header: ${token.type}`;
|
||
|
onError(token, 'UNEXPECTED_TOKEN', message);
|
||
|
const ts = token.source;
|
||
|
if (ts && typeof ts === 'string')
|
||
|
length += ts.length;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
return { mode, indent, chomp, comment, length };
|
||
|
}
|
||
|
/** @returns Array of lines split up as `[indent, content]` */
|
||
|
function splitLines(source) {
|
||
|
const split = source.split(/\n( *)/);
|
||
|
const first = split[0];
|
||
|
const m = first.match(/^( *)/);
|
||
|
const line0 = m?.[1]
|
||
|
? [m[1], first.slice(m[1].length)]
|
||
|
: ['', first];
|
||
|
const lines = [line0];
|
||
|
for (let i = 1; i < split.length; i += 2)
|
||
|
lines.push([split[i], split[i + 1]]);
|
||
|
return lines;
|
||
|
}
|
||
|
|
||
|
exports.resolveBlockScalar = resolveBlockScalar;
|