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.
950 lines
34 KiB
950 lines
34 KiB
import { tokenType } from './cst.js'; |
|
import { Lexer } from './lexer.js'; |
|
|
|
function includesToken(list, type) { |
|
for (let i = 0; i < list.length; ++i) |
|
if (list[i].type === type) |
|
return true; |
|
return false; |
|
} |
|
function findNonEmptyIndex(list) { |
|
for (let i = 0; i < list.length; ++i) { |
|
switch (list[i].type) { |
|
case 'space': |
|
case 'comment': |
|
case 'newline': |
|
break; |
|
default: |
|
return i; |
|
} |
|
} |
|
return -1; |
|
} |
|
function isFlowToken(token) { |
|
switch (token?.type) { |
|
case 'alias': |
|
case 'scalar': |
|
case 'single-quoted-scalar': |
|
case 'double-quoted-scalar': |
|
case 'flow-collection': |
|
return true; |
|
default: |
|
return false; |
|
} |
|
} |
|
function getPrevProps(parent) { |
|
switch (parent.type) { |
|
case 'document': |
|
return parent.start; |
|
case 'block-map': { |
|
const it = parent.items[parent.items.length - 1]; |
|
return it.sep ?? it.start; |
|
} |
|
case 'block-seq': |
|
return parent.items[parent.items.length - 1].start; |
|
/* istanbul ignore next should not happen */ |
|
default: |
|
return []; |
|
} |
|
} |
|
/** Note: May modify input array */ |
|
function getFirstKeyStartProps(prev) { |
|
if (prev.length === 0) |
|
return []; |
|
let i = prev.length; |
|
loop: while (--i >= 0) { |
|
switch (prev[i].type) { |
|
case 'doc-start': |
|
case 'explicit-key-ind': |
|
case 'map-value-ind': |
|
case 'seq-item-ind': |
|
case 'newline': |
|
break loop; |
|
} |
|
} |
|
while (prev[++i]?.type === 'space') { |
|
/* loop */ |
|
} |
|
return prev.splice(i, prev.length); |
|
} |
|
function fixFlowSeqItems(fc) { |
|
if (fc.start.type === 'flow-seq-start') { |
|
for (const it of fc.items) { |
|
if (it.sep && |
|
!it.value && |
|
!includesToken(it.start, 'explicit-key-ind') && |
|
!includesToken(it.sep, 'map-value-ind')) { |
|
if (it.key) |
|
it.value = it.key; |
|
delete it.key; |
|
if (isFlowToken(it.value)) { |
|
if (it.value.end) |
|
Array.prototype.push.apply(it.value.end, it.sep); |
|
else |
|
it.value.end = it.sep; |
|
} |
|
else |
|
Array.prototype.push.apply(it.start, it.sep); |
|
delete it.sep; |
|
} |
|
} |
|
} |
|
} |
|
/** |
|
* A YAML concrete syntax tree (CST) parser |
|
* |
|
* ```ts |
|
* const src: string = ... |
|
* for (const token of new Parser().parse(src)) { |
|
* // token: Token |
|
* } |
|
* ``` |
|
* |
|
* To use the parser with a user-provided lexer: |
|
* |
|
* ```ts |
|
* function* parse(source: string, lexer: Lexer) { |
|
* const parser = new Parser() |
|
* for (const lexeme of lexer.lex(source)) |
|
* yield* parser.next(lexeme) |
|
* yield* parser.end() |
|
* } |
|
* |
|
* const src: string = ... |
|
* const lexer = new Lexer() |
|
* for (const token of parse(src, lexer)) { |
|
* // token: Token |
|
* } |
|
* ``` |
|
*/ |
|
class Parser { |
|
/** |
|
* @param onNewLine - If defined, called separately with the start position of |
|
* each new line (in `parse()`, including the start of input). |
|
*/ |
|
constructor(onNewLine) { |
|
/** If true, space and sequence indicators count as indentation */ |
|
this.atNewLine = true; |
|
/** If true, next token is a scalar value */ |
|
this.atScalar = false; |
|
/** Current indentation level */ |
|
this.indent = 0; |
|
/** Current offset since the start of parsing */ |
|
this.offset = 0; |
|
/** On the same line with a block map key */ |
|
this.onKeyLine = false; |
|
/** Top indicates the node that's currently being built */ |
|
this.stack = []; |
|
/** The source of the current token, set in parse() */ |
|
this.source = ''; |
|
/** The type of the current token, set in parse() */ |
|
this.type = ''; |
|
// Must be defined after `next()` |
|
this.lexer = new Lexer(); |
|
this.onNewLine = onNewLine; |
|
} |
|
/** |
|
* Parse `source` as a YAML stream. |
|
* If `incomplete`, a part of the last line may be left as a buffer for the next call. |
|
* |
|
* Errors are not thrown, but yielded as `{ type: 'error', message }` tokens. |
|
* |
|
* @returns A generator of tokens representing each directive, document, and other structure. |
|
*/ |
|
*parse(source, incomplete = false) { |
|
if (this.onNewLine && this.offset === 0) |
|
this.onNewLine(0); |
|
for (const lexeme of this.lexer.lex(source, incomplete)) |
|
yield* this.next(lexeme); |
|
if (!incomplete) |
|
yield* this.end(); |
|
} |
|
/** |
|
* Advance the parser by the `source` of one lexical token. |
|
*/ |
|
*next(source) { |
|
this.source = source; |
|
if (this.atScalar) { |
|
this.atScalar = false; |
|
yield* this.step(); |
|
this.offset += source.length; |
|
return; |
|
} |
|
const type = tokenType(source); |
|
if (!type) { |
|
const message = `Not a YAML token: ${source}`; |
|
yield* this.pop({ type: 'error', offset: this.offset, message, source }); |
|
this.offset += source.length; |
|
} |
|
else if (type === 'scalar') { |
|
this.atNewLine = false; |
|
this.atScalar = true; |
|
this.type = 'scalar'; |
|
} |
|
else { |
|
this.type = type; |
|
yield* this.step(); |
|
switch (type) { |
|
case 'newline': |
|
this.atNewLine = true; |
|
this.indent = 0; |
|
if (this.onNewLine) |
|
this.onNewLine(this.offset + source.length); |
|
break; |
|
case 'space': |
|
if (this.atNewLine && source[0] === ' ') |
|
this.indent += source.length; |
|
break; |
|
case 'explicit-key-ind': |
|
case 'map-value-ind': |
|
case 'seq-item-ind': |
|
if (this.atNewLine) |
|
this.indent += source.length; |
|
break; |
|
case 'doc-mode': |
|
case 'flow-error-end': |
|
return; |
|
default: |
|
this.atNewLine = false; |
|
} |
|
this.offset += source.length; |
|
} |
|
} |
|
/** Call at end of input to push out any remaining constructions */ |
|
*end() { |
|
while (this.stack.length > 0) |
|
yield* this.pop(); |
|
} |
|
get sourceToken() { |
|
const st = { |
|
type: this.type, |
|
offset: this.offset, |
|
indent: this.indent, |
|
source: this.source |
|
}; |
|
return st; |
|
} |
|
*step() { |
|
const top = this.peek(1); |
|
if (this.type === 'doc-end' && (!top || top.type !== 'doc-end')) { |
|
while (this.stack.length > 0) |
|
yield* this.pop(); |
|
this.stack.push({ |
|
type: 'doc-end', |
|
offset: this.offset, |
|
source: this.source |
|
}); |
|
return; |
|
} |
|
if (!top) |
|
return yield* this.stream(); |
|
switch (top.type) { |
|
case 'document': |
|
return yield* this.document(top); |
|
case 'alias': |
|
case 'scalar': |
|
case 'single-quoted-scalar': |
|
case 'double-quoted-scalar': |
|
return yield* this.scalar(top); |
|
case 'block-scalar': |
|
return yield* this.blockScalar(top); |
|
case 'block-map': |
|
return yield* this.blockMap(top); |
|
case 'block-seq': |
|
return yield* this.blockSequence(top); |
|
case 'flow-collection': |
|
return yield* this.flowCollection(top); |
|
case 'doc-end': |
|
return yield* this.documentEnd(top); |
|
} |
|
/* istanbul ignore next should not happen */ |
|
yield* this.pop(); |
|
} |
|
peek(n) { |
|
return this.stack[this.stack.length - n]; |
|
} |
|
*pop(error) { |
|
const token = error ?? this.stack.pop(); |
|
/* istanbul ignore if should not happen */ |
|
if (!token) { |
|
const message = 'Tried to pop an empty stack'; |
|
yield { type: 'error', offset: this.offset, source: '', message }; |
|
} |
|
else if (this.stack.length === 0) { |
|
yield token; |
|
} |
|
else { |
|
const top = this.peek(1); |
|
if (token.type === 'block-scalar') { |
|
// Block scalars use their parent rather than header indent |
|
token.indent = 'indent' in top ? top.indent : 0; |
|
} |
|
else if (token.type === 'flow-collection' && top.type === 'document') { |
|
// Ignore all indent for top-level flow collections |
|
token.indent = 0; |
|
} |
|
if (token.type === 'flow-collection') |
|
fixFlowSeqItems(token); |
|
switch (top.type) { |
|
case 'document': |
|
top.value = token; |
|
break; |
|
case 'block-scalar': |
|
top.props.push(token); // error |
|
break; |
|
case 'block-map': { |
|
const it = top.items[top.items.length - 1]; |
|
if (it.value) { |
|
top.items.push({ start: [], key: token, sep: [] }); |
|
this.onKeyLine = true; |
|
return; |
|
} |
|
else if (it.sep) { |
|
it.value = token; |
|
} |
|
else { |
|
Object.assign(it, { key: token, sep: [] }); |
|
this.onKeyLine = !includesToken(it.start, 'explicit-key-ind'); |
|
return; |
|
} |
|
break; |
|
} |
|
case 'block-seq': { |
|
const it = top.items[top.items.length - 1]; |
|
if (it.value) |
|
top.items.push({ start: [], value: token }); |
|
else |
|
it.value = token; |
|
break; |
|
} |
|
case 'flow-collection': { |
|
const it = top.items[top.items.length - 1]; |
|
if (!it || it.value) |
|
top.items.push({ start: [], key: token, sep: [] }); |
|
else if (it.sep) |
|
it.value = token; |
|
else |
|
Object.assign(it, { key: token, sep: [] }); |
|
return; |
|
} |
|
/* istanbul ignore next should not happen */ |
|
default: |
|
yield* this.pop(); |
|
yield* this.pop(token); |
|
} |
|
if ((top.type === 'document' || |
|
top.type === 'block-map' || |
|
top.type === 'block-seq') && |
|
(token.type === 'block-map' || token.type === 'block-seq')) { |
|
const last = token.items[token.items.length - 1]; |
|
if (last && |
|
!last.sep && |
|
!last.value && |
|
last.start.length > 0 && |
|
findNonEmptyIndex(last.start) === -1 && |
|
(token.indent === 0 || |
|
last.start.every(st => st.type !== 'comment' || st.indent < token.indent))) { |
|
if (top.type === 'document') |
|
top.end = last.start; |
|
else |
|
top.items.push({ start: last.start }); |
|
token.items.splice(-1, 1); |
|
} |
|
} |
|
} |
|
} |
|
*stream() { |
|
switch (this.type) { |
|
case 'directive-line': |
|
yield { type: 'directive', offset: this.offset, source: this.source }; |
|
return; |
|
case 'byte-order-mark': |
|
case 'space': |
|
case 'comment': |
|
case 'newline': |
|
yield this.sourceToken; |
|
return; |
|
case 'doc-mode': |
|
case 'doc-start': { |
|
const doc = { |
|
type: 'document', |
|
offset: this.offset, |
|
start: [] |
|
}; |
|
if (this.type === 'doc-start') |
|
doc.start.push(this.sourceToken); |
|
this.stack.push(doc); |
|
return; |
|
} |
|
} |
|
yield { |
|
type: 'error', |
|
offset: this.offset, |
|
message: `Unexpected ${this.type} token in YAML stream`, |
|
source: this.source |
|
}; |
|
} |
|
*document(doc) { |
|
if (doc.value) |
|
return yield* this.lineEnd(doc); |
|
switch (this.type) { |
|
case 'doc-start': { |
|
if (findNonEmptyIndex(doc.start) !== -1) { |
|
yield* this.pop(); |
|
yield* this.step(); |
|
} |
|
else |
|
doc.start.push(this.sourceToken); |
|
return; |
|
} |
|
case 'anchor': |
|
case 'tag': |
|
case 'space': |
|
case 'comment': |
|
case 'newline': |
|
doc.start.push(this.sourceToken); |
|
return; |
|
} |
|
const bv = this.startBlockValue(doc); |
|
if (bv) |
|
this.stack.push(bv); |
|
else { |
|
yield { |
|
type: 'error', |
|
offset: this.offset, |
|
message: `Unexpected ${this.type} token in YAML document`, |
|
source: this.source |
|
}; |
|
} |
|
} |
|
*scalar(scalar) { |
|
if (this.type === 'map-value-ind') { |
|
const prev = getPrevProps(this.peek(2)); |
|
const start = getFirstKeyStartProps(prev); |
|
let sep; |
|
if (scalar.end) { |
|
sep = scalar.end; |
|
sep.push(this.sourceToken); |
|
delete scalar.end; |
|
} |
|
else |
|
sep = [this.sourceToken]; |
|
const map = { |
|
type: 'block-map', |
|
offset: scalar.offset, |
|
indent: scalar.indent, |
|
items: [{ start, key: scalar, sep }] |
|
}; |
|
this.onKeyLine = true; |
|
this.stack[this.stack.length - 1] = map; |
|
} |
|
else |
|
yield* this.lineEnd(scalar); |
|
} |
|
*blockScalar(scalar) { |
|
switch (this.type) { |
|
case 'space': |
|
case 'comment': |
|
case 'newline': |
|
scalar.props.push(this.sourceToken); |
|
return; |
|
case 'scalar': |
|
scalar.source = this.source; |
|
// block-scalar source includes trailing newline |
|
this.atNewLine = true; |
|
this.indent = 0; |
|
if (this.onNewLine) { |
|
let nl = this.source.indexOf('\n') + 1; |
|
while (nl !== 0) { |
|
this.onNewLine(this.offset + nl); |
|
nl = this.source.indexOf('\n', nl) + 1; |
|
} |
|
} |
|
yield* this.pop(); |
|
break; |
|
/* istanbul ignore next should not happen */ |
|
default: |
|
yield* this.pop(); |
|
yield* this.step(); |
|
} |
|
} |
|
*blockMap(map) { |
|
const it = map.items[map.items.length - 1]; |
|
// it.sep is true-ish if pair already has key or : separator |
|
switch (this.type) { |
|
case 'newline': |
|
this.onKeyLine = false; |
|
if (it.value) { |
|
const end = 'end' in it.value ? it.value.end : undefined; |
|
const last = Array.isArray(end) ? end[end.length - 1] : undefined; |
|
if (last?.type === 'comment') |
|
end?.push(this.sourceToken); |
|
else |
|
map.items.push({ start: [this.sourceToken] }); |
|
} |
|
else if (it.sep) { |
|
it.sep.push(this.sourceToken); |
|
} |
|
else { |
|
it.start.push(this.sourceToken); |
|
} |
|
return; |
|
case 'space': |
|
case 'comment': |
|
if (it.value) { |
|
map.items.push({ start: [this.sourceToken] }); |
|
} |
|
else if (it.sep) { |
|
it.sep.push(this.sourceToken); |
|
} |
|
else { |
|
if (this.atIndentedComment(it.start, map.indent)) { |
|
const prev = map.items[map.items.length - 2]; |
|
const end = prev?.value?.end; |
|
if (Array.isArray(end)) { |
|
Array.prototype.push.apply(end, it.start); |
|
end.push(this.sourceToken); |
|
map.items.pop(); |
|
return; |
|
} |
|
} |
|
it.start.push(this.sourceToken); |
|
} |
|
return; |
|
} |
|
if (this.indent >= map.indent) { |
|
const atNextItem = !this.onKeyLine && this.indent === map.indent && it.sep; |
|
// For empty nodes, assign newline-separated not indented empty tokens to following node |
|
let start = []; |
|
if (atNextItem && it.sep && !it.value) { |
|
const nl = []; |
|
for (let i = 0; i < it.sep.length; ++i) { |
|
const st = it.sep[i]; |
|
switch (st.type) { |
|
case 'newline': |
|
nl.push(i); |
|
break; |
|
case 'space': |
|
break; |
|
case 'comment': |
|
if (st.indent > map.indent) |
|
nl.length = 0; |
|
break; |
|
default: |
|
nl.length = 0; |
|
} |
|
} |
|
if (nl.length >= 2) |
|
start = it.sep.splice(nl[1]); |
|
} |
|
switch (this.type) { |
|
case 'anchor': |
|
case 'tag': |
|
if (atNextItem || it.value) { |
|
start.push(this.sourceToken); |
|
map.items.push({ start }); |
|
this.onKeyLine = true; |
|
} |
|
else if (it.sep) { |
|
it.sep.push(this.sourceToken); |
|
} |
|
else { |
|
it.start.push(this.sourceToken); |
|
} |
|
return; |
|
case 'explicit-key-ind': |
|
if (!it.sep && !includesToken(it.start, 'explicit-key-ind')) { |
|
it.start.push(this.sourceToken); |
|
} |
|
else if (atNextItem || it.value) { |
|
start.push(this.sourceToken); |
|
map.items.push({ start }); |
|
} |
|
else { |
|
this.stack.push({ |
|
type: 'block-map', |
|
offset: this.offset, |
|
indent: this.indent, |
|
items: [{ start: [this.sourceToken] }] |
|
}); |
|
} |
|
this.onKeyLine = true; |
|
return; |
|
case 'map-value-ind': |
|
if (includesToken(it.start, 'explicit-key-ind')) { |
|
if (!it.sep) { |
|
if (includesToken(it.start, 'newline')) { |
|
Object.assign(it, { key: null, sep: [this.sourceToken] }); |
|
} |
|
else { |
|
const start = getFirstKeyStartProps(it.start); |
|
this.stack.push({ |
|
type: 'block-map', |
|
offset: this.offset, |
|
indent: this.indent, |
|
items: [{ start, key: null, sep: [this.sourceToken] }] |
|
}); |
|
} |
|
} |
|
else if (it.value) { |
|
map.items.push({ start: [], key: null, sep: [this.sourceToken] }); |
|
} |
|
else if (includesToken(it.sep, 'map-value-ind')) { |
|
this.stack.push({ |
|
type: 'block-map', |
|
offset: this.offset, |
|
indent: this.indent, |
|
items: [{ start, key: null, sep: [this.sourceToken] }] |
|
}); |
|
} |
|
else if (isFlowToken(it.key) && |
|
!includesToken(it.sep, 'newline')) { |
|
const start = getFirstKeyStartProps(it.start); |
|
const key = it.key; |
|
const sep = it.sep; |
|
sep.push(this.sourceToken); |
|
// @ts-expect-error type guard is wrong here |
|
delete it.key, delete it.sep; |
|
this.stack.push({ |
|
type: 'block-map', |
|
offset: this.offset, |
|
indent: this.indent, |
|
items: [{ start, key, sep }] |
|
}); |
|
} |
|
else if (start.length > 0) { |
|
// Not actually at next item |
|
it.sep = it.sep.concat(start, this.sourceToken); |
|
} |
|
else { |
|
it.sep.push(this.sourceToken); |
|
} |
|
} |
|
else { |
|
if (!it.sep) { |
|
Object.assign(it, { key: null, sep: [this.sourceToken] }); |
|
} |
|
else if (it.value || atNextItem) { |
|
map.items.push({ start, key: null, sep: [this.sourceToken] }); |
|
} |
|
else if (includesToken(it.sep, 'map-value-ind')) { |
|
this.stack.push({ |
|
type: 'block-map', |
|
offset: this.offset, |
|
indent: this.indent, |
|
items: [{ start: [], key: null, sep: [this.sourceToken] }] |
|
}); |
|
} |
|
else { |
|
it.sep.push(this.sourceToken); |
|
} |
|
} |
|
this.onKeyLine = true; |
|
return; |
|
case 'alias': |
|
case 'scalar': |
|
case 'single-quoted-scalar': |
|
case 'double-quoted-scalar': { |
|
const fs = this.flowScalar(this.type); |
|
if (atNextItem || it.value) { |
|
map.items.push({ start, key: fs, sep: [] }); |
|
this.onKeyLine = true; |
|
} |
|
else if (it.sep) { |
|
this.stack.push(fs); |
|
} |
|
else { |
|
Object.assign(it, { key: fs, sep: [] }); |
|
this.onKeyLine = true; |
|
} |
|
return; |
|
} |
|
default: { |
|
const bv = this.startBlockValue(map); |
|
if (bv) { |
|
if (atNextItem && |
|
bv.type !== 'block-seq' && |
|
includesToken(it.start, 'explicit-key-ind')) { |
|
map.items.push({ start }); |
|
} |
|
this.stack.push(bv); |
|
return; |
|
} |
|
} |
|
} |
|
} |
|
yield* this.pop(); |
|
yield* this.step(); |
|
} |
|
*blockSequence(seq) { |
|
const it = seq.items[seq.items.length - 1]; |
|
switch (this.type) { |
|
case 'newline': |
|
if (it.value) { |
|
const end = 'end' in it.value ? it.value.end : undefined; |
|
const last = Array.isArray(end) ? end[end.length - 1] : undefined; |
|
if (last?.type === 'comment') |
|
end?.push(this.sourceToken); |
|
else |
|
seq.items.push({ start: [this.sourceToken] }); |
|
} |
|
else |
|
it.start.push(this.sourceToken); |
|
return; |
|
case 'space': |
|
case 'comment': |
|
if (it.value) |
|
seq.items.push({ start: [this.sourceToken] }); |
|
else { |
|
if (this.atIndentedComment(it.start, seq.indent)) { |
|
const prev = seq.items[seq.items.length - 2]; |
|
const end = prev?.value?.end; |
|
if (Array.isArray(end)) { |
|
Array.prototype.push.apply(end, it.start); |
|
end.push(this.sourceToken); |
|
seq.items.pop(); |
|
return; |
|
} |
|
} |
|
it.start.push(this.sourceToken); |
|
} |
|
return; |
|
case 'anchor': |
|
case 'tag': |
|
if (it.value || this.indent <= seq.indent) |
|
break; |
|
it.start.push(this.sourceToken); |
|
return; |
|
case 'seq-item-ind': |
|
if (this.indent !== seq.indent) |
|
break; |
|
if (it.value || includesToken(it.start, 'seq-item-ind')) |
|
seq.items.push({ start: [this.sourceToken] }); |
|
else |
|
it.start.push(this.sourceToken); |
|
return; |
|
} |
|
if (this.indent > seq.indent) { |
|
const bv = this.startBlockValue(seq); |
|
if (bv) { |
|
this.stack.push(bv); |
|
return; |
|
} |
|
} |
|
yield* this.pop(); |
|
yield* this.step(); |
|
} |
|
*flowCollection(fc) { |
|
const it = fc.items[fc.items.length - 1]; |
|
if (this.type === 'flow-error-end') { |
|
let top; |
|
do { |
|
yield* this.pop(); |
|
top = this.peek(1); |
|
} while (top && top.type === 'flow-collection'); |
|
} |
|
else if (fc.end.length === 0) { |
|
switch (this.type) { |
|
case 'comma': |
|
case 'explicit-key-ind': |
|
if (!it || it.sep) |
|
fc.items.push({ start: [this.sourceToken] }); |
|
else |
|
it.start.push(this.sourceToken); |
|
return; |
|
case 'map-value-ind': |
|
if (!it || it.value) |
|
fc.items.push({ start: [], key: null, sep: [this.sourceToken] }); |
|
else if (it.sep) |
|
it.sep.push(this.sourceToken); |
|
else |
|
Object.assign(it, { key: null, sep: [this.sourceToken] }); |
|
return; |
|
case 'space': |
|
case 'comment': |
|
case 'newline': |
|
case 'anchor': |
|
case 'tag': |
|
if (!it || it.value) |
|
fc.items.push({ start: [this.sourceToken] }); |
|
else if (it.sep) |
|
it.sep.push(this.sourceToken); |
|
else |
|
it.start.push(this.sourceToken); |
|
return; |
|
case 'alias': |
|
case 'scalar': |
|
case 'single-quoted-scalar': |
|
case 'double-quoted-scalar': { |
|
const fs = this.flowScalar(this.type); |
|
if (!it || it.value) |
|
fc.items.push({ start: [], key: fs, sep: [] }); |
|
else if (it.sep) |
|
this.stack.push(fs); |
|
else |
|
Object.assign(it, { key: fs, sep: [] }); |
|
return; |
|
} |
|
case 'flow-map-end': |
|
case 'flow-seq-end': |
|
fc.end.push(this.sourceToken); |
|
return; |
|
} |
|
const bv = this.startBlockValue(fc); |
|
/* istanbul ignore else should not happen */ |
|
if (bv) |
|
this.stack.push(bv); |
|
else { |
|
yield* this.pop(); |
|
yield* this.step(); |
|
} |
|
} |
|
else { |
|
const parent = this.peek(2); |
|
if (parent.type === 'block-map' && |
|
((this.type === 'map-value-ind' && parent.indent === fc.indent) || |
|
(this.type === 'newline' && |
|
!parent.items[parent.items.length - 1].sep))) { |
|
yield* this.pop(); |
|
yield* this.step(); |
|
} |
|
else if (this.type === 'map-value-ind' && |
|
parent.type !== 'flow-collection') { |
|
const prev = getPrevProps(parent); |
|
const start = getFirstKeyStartProps(prev); |
|
fixFlowSeqItems(fc); |
|
const sep = fc.end.splice(1, fc.end.length); |
|
sep.push(this.sourceToken); |
|
const map = { |
|
type: 'block-map', |
|
offset: fc.offset, |
|
indent: fc.indent, |
|
items: [{ start, key: fc, sep }] |
|
}; |
|
this.onKeyLine = true; |
|
this.stack[this.stack.length - 1] = map; |
|
} |
|
else { |
|
yield* this.lineEnd(fc); |
|
} |
|
} |
|
} |
|
flowScalar(type) { |
|
if (this.onNewLine) { |
|
let nl = this.source.indexOf('\n') + 1; |
|
while (nl !== 0) { |
|
this.onNewLine(this.offset + nl); |
|
nl = this.source.indexOf('\n', nl) + 1; |
|
} |
|
} |
|
return { |
|
type, |
|
offset: this.offset, |
|
indent: this.indent, |
|
source: this.source |
|
}; |
|
} |
|
startBlockValue(parent) { |
|
switch (this.type) { |
|
case 'alias': |
|
case 'scalar': |
|
case 'single-quoted-scalar': |
|
case 'double-quoted-scalar': |
|
return this.flowScalar(this.type); |
|
case 'block-scalar-header': |
|
return { |
|
type: 'block-scalar', |
|
offset: this.offset, |
|
indent: this.indent, |
|
props: [this.sourceToken], |
|
source: '' |
|
}; |
|
case 'flow-map-start': |
|
case 'flow-seq-start': |
|
return { |
|
type: 'flow-collection', |
|
offset: this.offset, |
|
indent: this.indent, |
|
start: this.sourceToken, |
|
items: [], |
|
end: [] |
|
}; |
|
case 'seq-item-ind': |
|
return { |
|
type: 'block-seq', |
|
offset: this.offset, |
|
indent: this.indent, |
|
items: [{ start: [this.sourceToken] }] |
|
}; |
|
case 'explicit-key-ind': { |
|
this.onKeyLine = true; |
|
const prev = getPrevProps(parent); |
|
const start = getFirstKeyStartProps(prev); |
|
start.push(this.sourceToken); |
|
return { |
|
type: 'block-map', |
|
offset: this.offset, |
|
indent: this.indent, |
|
items: [{ start }] |
|
}; |
|
} |
|
case 'map-value-ind': { |
|
this.onKeyLine = true; |
|
const prev = getPrevProps(parent); |
|
const start = getFirstKeyStartProps(prev); |
|
return { |
|
type: 'block-map', |
|
offset: this.offset, |
|
indent: this.indent, |
|
items: [{ start, key: null, sep: [this.sourceToken] }] |
|
}; |
|
} |
|
} |
|
return null; |
|
} |
|
atIndentedComment(start, indent) { |
|
if (this.type !== 'comment') |
|
return false; |
|
if (this.indent <= indent) |
|
return false; |
|
return start.every(st => st.type === 'newline' || st.type === 'space'); |
|
} |
|
*documentEnd(docEnd) { |
|
if (this.type !== 'doc-mode') { |
|
if (docEnd.end) |
|
docEnd.end.push(this.sourceToken); |
|
else |
|
docEnd.end = [this.sourceToken]; |
|
if (this.type === 'newline') |
|
yield* this.pop(); |
|
} |
|
} |
|
*lineEnd(token) { |
|
switch (this.type) { |
|
case 'comma': |
|
case 'doc-start': |
|
case 'doc-end': |
|
case 'flow-seq-end': |
|
case 'flow-map-end': |
|
case 'map-value-ind': |
|
yield* this.pop(); |
|
yield* this.step(); |
|
break; |
|
case 'newline': |
|
this.onKeyLine = false; |
|
// fallthrough |
|
case 'space': |
|
case 'comment': |
|
default: |
|
// all other values are errors |
|
if (token.end) |
|
token.end.push(this.sourceToken); |
|
else |
|
token.end = [this.sourceToken]; |
|
if (this.type === 'newline') |
|
yield* this.pop(); |
|
} |
|
} |
|
} |
|
|
|
export { Parser };
|
|
|