UNPKG

@travetto/yaml

Version:

Simple YAML support, provides only clean subset of yaml

148 lines (136 loc) 3.66 kB
import { Block, ListBlock, MapBlock, TextBlock } from './type/block'; import { SimpleType } from './type/common'; import { Node, TextNode } from './type/node'; /** * Parser state */ export class State { /** * Current stack of blocks in process */ blocks: Block[]; /** * Current block */ top: Block; /** * Collected fields so far */ fields: [number, string][] = []; /** * Collected lines */ lines: [number, number][] = []; /** * Line count */ lineCount: number = 0; constructor(public text: string) { this.top = new ListBlock(-1); this.blocks = [this.top]; } /** * Get active block identifier */ get indent(): number { return !this.top ? 0 : this.top.indent; } /** * Keep popping states until indentation request is satisfied */ popToLevel(indent: number): void { while (indent < this.top.indent) { // Block shift left this.endBlock(); if (this.top.indent < 0) { throw this.buildError('Invalid indentation, could not find matching level'); } } } /** * Keep popping states until at the top of the document */ popToTop(): Block<SimpleType> | TextBlock | { index: 0, value: {} } { let last; while (this.top.indent >= 0) { last = this.endBlock(); } return last ?? { index: 0, value: {} }; // Default to empty object if nothing returned } /** * Start sub item with a given field name and indentation */ nestField(field: string, indent: number): void { if (this.fields.length && this.fields[this.fields.length - 1][0] === indent) { this.fields[this.fields.length - 1][1] = field; } else { this.fields.push([indent, field]); } } /** * Start a new block */ startBlock(block: Block): void { if (this.top instanceof TextBlock) { throw this.buildError(`Cannot nest in current block ${this.top.constructor.name}`); } this.top = block; this.blocks.push(block); } /** * Complete a block */ endBlock(): Block<SimpleType> { const ret = this.blocks.pop()!; this.top = this.blocks[this.blocks.length - 1]; if (ret instanceof TextBlock || ret instanceof TextNode) { ret.value = ret.value.trimRight(); } this.consumeNode(ret); return ret; } /** * Include node in output content, popping as needed */ consumeNode(node: Node): void { if (!this.top.consume) { throw this.buildError(`Cannot consume in current block ${this.top.constructor.name}`); } if (this.top instanceof MapBlock) { const [ind, field] = this.fields.pop()! ?? []; if ('indent' in node && this.top.indent !== ind) { throw this.buildError('Unable to set value, incorrect nesting'); } this.top.consume(node, field); } else { this.top.consume!(node); } } /** * Read a line of tokens */ readTextLine(tokens: string[], indent: number): boolean { if (this.top instanceof TextBlock && ( this.top.indent === undefined || indent === this.top.indent || tokens.length === 0 )) { if (this.top.indent === undefined && tokens.length > 0) { this.top.indent = indent; } this.top.readLine(tokens); return true; } else if (tokens.length === 0) { return true; } return false; } /** * Build error message */ buildError(msg: string): Error { const [start, end] = this.lines[this.lineCount - 1]; const err = new Error(`${msg}, line: ${this.lineCount}\n${this.text.substring(start, end)}`); err.stack = err.stack?.split(/\n/g).slice(2).join('\n'); return err; } }