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.
194 lines
7.1 KiB
194 lines
7.1 KiB
import { Scalar } from '../nodes/Scalar.js'; |
|
|
|
function resolveBlockScalar(scalar, strict, onError) { |
|
const start = scalar.offset; |
|
const header = parseBlockScalarHeader(scalar, strict, onError); |
|
if (!header) |
|
return { value: '', type: null, comment: '', range: [start, start, start] }; |
|
const type = header.mode === '>' ? Scalar.BLOCK_FOLDED : 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; |
|
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.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; |
|
} |
|
|
|
export { resolveBlockScalar };
|
|
|