959 lines
35 KiB
D
959 lines
35 KiB
D
|
|
// Copyright Ferdinand Majerech 2011-2014.
|
|
// Distributed under the Boost Software License, Version 1.0.
|
|
// (See accompanying file LICENSE_1_0.txt or copy at
|
|
// http://www.boost.org/LICENSE_1_0.txt)
|
|
|
|
/**
|
|
* YAML parser.
|
|
* Code based on PyYAML: http://www.pyyaml.org
|
|
*/
|
|
module dyaml.parser;
|
|
|
|
|
|
import std.algorithm;
|
|
import std.array;
|
|
import std.conv;
|
|
import std.exception;
|
|
import std.typecons;
|
|
|
|
import dyaml.event;
|
|
import dyaml.exception;
|
|
import dyaml.scanner;
|
|
import dyaml.style;
|
|
import dyaml.token;
|
|
import dyaml.tagdirective;
|
|
|
|
|
|
package:
|
|
/**
|
|
* The following YAML grammar is LL(1) and is parsed by a recursive descent
|
|
* parser.
|
|
*
|
|
* stream ::= STREAM-START implicit_document? explicit_document* STREAM-END
|
|
* implicit_document ::= block_node DOCUMENT-END*
|
|
* explicit_document ::= DIRECTIVE* DOCUMENT-START block_node? DOCUMENT-END*
|
|
* block_node_or_indentless_sequence ::=
|
|
* ALIAS
|
|
* | properties (block_content | indentless_block_sequence)?
|
|
* | block_content
|
|
* | indentless_block_sequence
|
|
* block_node ::= ALIAS
|
|
* | properties block_content?
|
|
* | block_content
|
|
* flow_node ::= ALIAS
|
|
* | properties flow_content?
|
|
* | flow_content
|
|
* properties ::= TAG ANCHOR? | ANCHOR TAG?
|
|
* block_content ::= block_collection | flow_collection | SCALAR
|
|
* flow_content ::= flow_collection | SCALAR
|
|
* block_collection ::= block_sequence | block_mapping
|
|
* flow_collection ::= flow_sequence | flow_mapping
|
|
* block_sequence ::= BLOCK-SEQUENCE-START (BLOCK-ENTRY block_node?)* BLOCK-END
|
|
* indentless_sequence ::= (BLOCK-ENTRY block_node?)+
|
|
* block_mapping ::= BLOCK-MAPPING_START
|
|
* ((KEY block_node_or_indentless_sequence?)?
|
|
* (VALUE block_node_or_indentless_sequence?)?)*
|
|
* BLOCK-END
|
|
* flow_sequence ::= FLOW-SEQUENCE-START
|
|
* (flow_sequence_entry FLOW-ENTRY)*
|
|
* flow_sequence_entry?
|
|
* FLOW-SEQUENCE-END
|
|
* flow_sequence_entry ::= flow_node | KEY flow_node? (VALUE flow_node?)?
|
|
* flow_mapping ::= FLOW-MAPPING-START
|
|
* (flow_mapping_entry FLOW-ENTRY)*
|
|
* flow_mapping_entry?
|
|
* FLOW-MAPPING-END
|
|
* flow_mapping_entry ::= flow_node | KEY flow_node? (VALUE flow_node?)?
|
|
*
|
|
* FIRST sets:
|
|
*
|
|
* stream: { STREAM-START }
|
|
* explicit_document: { DIRECTIVE DOCUMENT-START }
|
|
* implicit_document: FIRST(block_node)
|
|
* block_node: { ALIAS TAG ANCHOR SCALAR BLOCK-SEQUENCE-START BLOCK-MAPPING-START FLOW-SEQUENCE-START FLOW-MAPPING-START }
|
|
* flow_node: { ALIAS ANCHOR TAG SCALAR FLOW-SEQUENCE-START FLOW-MAPPING-START }
|
|
* block_content: { BLOCK-SEQUENCE-START BLOCK-MAPPING-START FLOW-SEQUENCE-START FLOW-MAPPING-START SCALAR }
|
|
* flow_content: { FLOW-SEQUENCE-START FLOW-MAPPING-START SCALAR }
|
|
* block_collection: { BLOCK-SEQUENCE-START BLOCK-MAPPING-START }
|
|
* flow_collection: { FLOW-SEQUENCE-START FLOW-MAPPING-START }
|
|
* block_sequence: { BLOCK-SEQUENCE-START }
|
|
* block_mapping: { BLOCK-MAPPING-START }
|
|
* block_node_or_indentless_sequence: { ALIAS ANCHOR TAG SCALAR BLOCK-SEQUENCE-START BLOCK-MAPPING-START FLOW-SEQUENCE-START FLOW-MAPPING-START BLOCK-ENTRY }
|
|
* indentless_sequence: { ENTRY }
|
|
* flow_collection: { FLOW-SEQUENCE-START FLOW-MAPPING-START }
|
|
* flow_sequence: { FLOW-SEQUENCE-START }
|
|
* flow_mapping: { FLOW-MAPPING-START }
|
|
* flow_sequence_entry: { ALIAS ANCHOR TAG SCALAR FLOW-SEQUENCE-START FLOW-MAPPING-START KEY }
|
|
* flow_mapping_entry: { ALIAS ANCHOR TAG SCALAR FLOW-SEQUENCE-START FLOW-MAPPING-START KEY }
|
|
*/
|
|
|
|
|
|
/**
|
|
* Marked exception thrown at parser errors.
|
|
*
|
|
* See_Also: MarkedYAMLException
|
|
*/
|
|
class ParserException : MarkedYAMLException
|
|
{
|
|
mixin MarkedExceptionCtors;
|
|
}
|
|
|
|
/// Generates events from tokens provided by a Scanner.
|
|
///
|
|
/// While Parser receives tokens with non-const character slices, the events it
|
|
/// produces are immutable strings, which are usually the same slices, cast to string.
|
|
/// Parser is the last layer of D:YAML that may possibly do any modifications to these
|
|
/// slices.
|
|
final class Parser
|
|
{
|
|
private:
|
|
///Default tag handle shortcuts and replacements.
|
|
static TagDirective[] defaultTagDirectives_ =
|
|
[TagDirective("!", "!"), TagDirective("!!", "tag:yaml.org,2002:")];
|
|
|
|
///Scanner providing YAML tokens.
|
|
Scanner scanner_;
|
|
|
|
///Event produced by the most recent state.
|
|
Event currentEvent_;
|
|
|
|
///YAML version string.
|
|
string YAMLVersion_ = null;
|
|
///Tag handle shortcuts and replacements.
|
|
TagDirective[] tagDirectives_;
|
|
|
|
///Stack of states.
|
|
Appender!(Event delegate() @safe[]) states_;
|
|
///Stack of marks used to keep track of extents of e.g. YAML collections.
|
|
Appender!(Mark[]) marks_;
|
|
|
|
///Current state.
|
|
Event delegate() @safe state_;
|
|
|
|
public:
|
|
///Construct a Parser using specified Scanner.
|
|
this(Scanner scanner) @safe
|
|
{
|
|
state_ = &parseStreamStart;
|
|
scanner_ = scanner;
|
|
states_.reserve(32);
|
|
marks_.reserve(32);
|
|
}
|
|
|
|
/**
|
|
* Check if any events are left. May have side effects in some cases.
|
|
*/
|
|
bool empty() @safe
|
|
{
|
|
ensureState();
|
|
return currentEvent_.isNull;
|
|
}
|
|
|
|
/**
|
|
* Return the current event.
|
|
*
|
|
* Must not be called if there are no events left.
|
|
*/
|
|
Event front() @safe
|
|
{
|
|
ensureState();
|
|
assert(!currentEvent_.isNull, "No event left to peek");
|
|
return currentEvent_;
|
|
}
|
|
|
|
/**
|
|
* Skip to the next event.
|
|
*
|
|
* Must not be called if there are no events left.
|
|
*/
|
|
void popFront() @safe
|
|
{
|
|
currentEvent_.id = EventID.invalid;
|
|
ensureState();
|
|
}
|
|
|
|
private:
|
|
/// If current event is invalid, load the next valid one if possible.
|
|
void ensureState() @safe
|
|
{
|
|
if(currentEvent_.isNull && state_ !is null)
|
|
{
|
|
currentEvent_ = state_();
|
|
}
|
|
}
|
|
///Pop and return the newest state in states_.
|
|
Event delegate() @safe popState() @safe
|
|
{
|
|
enforce(states_.data.length > 0,
|
|
new YAMLException("Parser: Need to pop state but no states left to pop"));
|
|
const result = states_.data.back;
|
|
states_.shrinkTo(states_.data.length - 1);
|
|
return result;
|
|
}
|
|
|
|
///Pop and return the newest mark in marks_.
|
|
Mark popMark() @safe
|
|
{
|
|
enforce(marks_.data.length > 0,
|
|
new YAMLException("Parser: Need to pop mark but no marks left to pop"));
|
|
const result = marks_.data.back;
|
|
marks_.shrinkTo(marks_.data.length - 1);
|
|
return result;
|
|
}
|
|
|
|
/// Push a state on the stack
|
|
void pushState(Event delegate() @safe state) @safe
|
|
{
|
|
states_ ~= state;
|
|
}
|
|
/// Push a mark on the stack
|
|
void pushMark(Mark mark) @safe
|
|
{
|
|
marks_ ~= mark;
|
|
}
|
|
|
|
/**
|
|
* stream ::= STREAM-START implicit_document? explicit_document* STREAM-END
|
|
* implicit_document ::= block_node DOCUMENT-END*
|
|
* explicit_document ::= DIRECTIVE* DOCUMENT-START block_node? DOCUMENT-END*
|
|
*/
|
|
|
|
///Parse stream start.
|
|
Event parseStreamStart() @safe
|
|
{
|
|
const token = scanner_.front;
|
|
scanner_.popFront();
|
|
state_ = &parseImplicitDocumentStart;
|
|
return streamStartEvent(token.startMark, token.endMark);
|
|
}
|
|
|
|
/// Parse implicit document start, unless explicit detected: if so, parse explicit.
|
|
Event parseImplicitDocumentStart() @safe
|
|
{
|
|
// Parse an implicit document.
|
|
if(!scanner_.front.id.among!(TokenID.directive, TokenID.documentStart,
|
|
TokenID.streamEnd))
|
|
{
|
|
tagDirectives_ = defaultTagDirectives_;
|
|
const token = scanner_.front;
|
|
|
|
pushState(&parseDocumentEnd);
|
|
state_ = &parseBlockNode;
|
|
|
|
return documentStartEvent(token.startMark, token.endMark, false, null, null);
|
|
}
|
|
return parseDocumentStart();
|
|
}
|
|
|
|
///Parse explicit document start.
|
|
Event parseDocumentStart() @trusted
|
|
{
|
|
//Parse any extra document end indicators.
|
|
while(scanner_.front.id == TokenID.documentEnd)
|
|
{
|
|
scanner_.popFront();
|
|
}
|
|
|
|
//Parse an explicit document.
|
|
if(scanner_.front.id != TokenID.streamEnd)
|
|
{
|
|
const startMark = scanner_.front.startMark;
|
|
|
|
auto tagDirectives = processDirectives();
|
|
enforce(scanner_.front.id == TokenID.documentStart,
|
|
new ParserException("Expected document start but found " ~
|
|
scanner_.front.idString,
|
|
scanner_.front.startMark));
|
|
|
|
const endMark = scanner_.front.endMark;
|
|
scanner_.popFront();
|
|
pushState(&parseDocumentEnd);
|
|
state_ = &parseDocumentContent;
|
|
return documentStartEvent(startMark, endMark, true, YAMLVersion_, tagDirectives);
|
|
}
|
|
else
|
|
{
|
|
//Parse the end of the stream.
|
|
const token = scanner_.front;
|
|
scanner_.popFront();
|
|
assert(states_.data.length == 0);
|
|
assert(marks_.data.length == 0);
|
|
state_ = null;
|
|
return streamEndEvent(token.startMark, token.endMark);
|
|
}
|
|
}
|
|
|
|
///Parse document end (explicit or implicit).
|
|
Event parseDocumentEnd() @safe
|
|
{
|
|
Mark startMark = scanner_.front.startMark;
|
|
const bool explicit = scanner_.front.id == TokenID.documentEnd;
|
|
Mark endMark = startMark;
|
|
if (explicit)
|
|
{
|
|
endMark = scanner_.front.endMark;
|
|
scanner_.popFront();
|
|
}
|
|
|
|
state_ = &parseDocumentStart;
|
|
|
|
return documentEndEvent(startMark, endMark, explicit);
|
|
}
|
|
|
|
///Parse document content.
|
|
Event parseDocumentContent() @safe
|
|
{
|
|
if(scanner_.front.id.among!(TokenID.directive, TokenID.documentStart,
|
|
TokenID.documentEnd, TokenID.streamEnd))
|
|
{
|
|
state_ = popState();
|
|
return processEmptyScalar(scanner_.front.startMark);
|
|
}
|
|
return parseBlockNode();
|
|
}
|
|
|
|
/// Process directives at the beginning of a document.
|
|
TagDirective[] processDirectives() @safe
|
|
{
|
|
// Destroy version and tag handles from previous document.
|
|
YAMLVersion_ = null;
|
|
tagDirectives_.length = 0;
|
|
|
|
// Process directives.
|
|
while(scanner_.front.id == TokenID.directive)
|
|
{
|
|
const token = scanner_.front;
|
|
scanner_.popFront();
|
|
string value = token.value.idup;
|
|
if(token.directive == DirectiveType.yaml)
|
|
{
|
|
enforce(YAMLVersion_ is null,
|
|
new ParserException("Duplicate YAML directive", token.startMark));
|
|
const minor = value.split(".")[0];
|
|
enforce(minor == "1",
|
|
new ParserException("Incompatible document (version 1.x is required)",
|
|
token.startMark));
|
|
YAMLVersion_ = value;
|
|
}
|
|
else if(token.directive == DirectiveType.tag)
|
|
{
|
|
auto handle = value[0 .. token.valueDivider];
|
|
|
|
foreach(ref pair; tagDirectives_)
|
|
{
|
|
// handle
|
|
const h = pair.handle;
|
|
enforce(h != handle, new ParserException("Duplicate tag handle: " ~ handle,
|
|
token.startMark));
|
|
}
|
|
tagDirectives_ ~=
|
|
TagDirective(handle, value[token.valueDivider .. $]);
|
|
}
|
|
// Any other directive type is ignored (only YAML and TAG are in YAML
|
|
// 1.1/1.2, any other directives are "reserved")
|
|
}
|
|
|
|
TagDirective[] value = tagDirectives_;
|
|
|
|
//Add any default tag handles that haven't been overridden.
|
|
foreach(ref defaultPair; defaultTagDirectives_)
|
|
{
|
|
bool found;
|
|
foreach(ref pair; tagDirectives_) if(defaultPair.handle == pair.handle)
|
|
{
|
|
found = true;
|
|
break;
|
|
}
|
|
if(!found) {tagDirectives_ ~= defaultPair; }
|
|
}
|
|
|
|
return value;
|
|
}
|
|
|
|
/**
|
|
* block_node_or_indentless_sequence ::= ALIAS
|
|
* | properties (block_content | indentless_block_sequence)?
|
|
* | block_content
|
|
* | indentless_block_sequence
|
|
* block_node ::= ALIAS
|
|
* | properties block_content?
|
|
* | block_content
|
|
* flow_node ::= ALIAS
|
|
* | properties flow_content?
|
|
* | flow_content
|
|
* properties ::= TAG ANCHOR? | ANCHOR TAG?
|
|
* block_content ::= block_collection | flow_collection | SCALAR
|
|
* flow_content ::= flow_collection | SCALAR
|
|
* block_collection ::= block_sequence | block_mapping
|
|
* flow_collection ::= flow_sequence | flow_mapping
|
|
*/
|
|
|
|
///Parse a node.
|
|
Event parseNode(const Flag!"block" block,
|
|
const Flag!"indentlessSequence" indentlessSequence = No.indentlessSequence)
|
|
@trusted
|
|
{
|
|
if(scanner_.front.id == TokenID.alias_)
|
|
{
|
|
const token = scanner_.front;
|
|
scanner_.popFront();
|
|
state_ = popState();
|
|
return aliasEvent(token.startMark, token.endMark,
|
|
cast(string)token.value);
|
|
}
|
|
|
|
string anchor;
|
|
string tag;
|
|
Mark startMark, endMark, tagMark;
|
|
bool invalidMarks = true;
|
|
// The index in the tag string where tag handle ends and tag suffix starts.
|
|
uint tagHandleEnd;
|
|
|
|
//Get anchor/tag if detected. Return false otherwise.
|
|
bool get(const TokenID id, const Flag!"first" first, ref string target) @safe
|
|
{
|
|
if(scanner_.front.id != id){return false;}
|
|
invalidMarks = false;
|
|
const token = scanner_.front;
|
|
scanner_.popFront();
|
|
if(first){startMark = token.startMark;}
|
|
if(id == TokenID.tag)
|
|
{
|
|
tagMark = token.startMark;
|
|
tagHandleEnd = token.valueDivider;
|
|
}
|
|
endMark = token.endMark;
|
|
target = token.value.idup;
|
|
return true;
|
|
}
|
|
|
|
//Anchor and/or tag can be in any order.
|
|
if(get(TokenID.anchor, Yes.first, anchor)){get(TokenID.tag, No.first, tag);}
|
|
else if(get(TokenID.tag, Yes.first, tag)) {get(TokenID.anchor, No.first, anchor);}
|
|
|
|
if(tag !is null){tag = processTag(tag, tagHandleEnd, startMark, tagMark);}
|
|
|
|
if(invalidMarks)
|
|
{
|
|
startMark = endMark = scanner_.front.startMark;
|
|
}
|
|
|
|
bool implicit = (tag is null || tag == "!");
|
|
|
|
if(indentlessSequence && scanner_.front.id == TokenID.blockEntry)
|
|
{
|
|
state_ = &parseIndentlessSequenceEntry;
|
|
return sequenceStartEvent
|
|
(startMark, scanner_.front.endMark, anchor,
|
|
tag, implicit, CollectionStyle.block);
|
|
}
|
|
|
|
if(scanner_.front.id == TokenID.scalar)
|
|
{
|
|
auto token = scanner_.front;
|
|
scanner_.popFront();
|
|
auto value = token.style == ScalarStyle.doubleQuoted
|
|
? handleDoubleQuotedScalarEscapes(token.value)
|
|
: cast(string)token.value;
|
|
|
|
implicit = (token.style == ScalarStyle.plain && tag is null) || tag == "!";
|
|
state_ = popState();
|
|
return scalarEvent(startMark, token.endMark, anchor, tag,
|
|
implicit, value, token.style);
|
|
}
|
|
|
|
if(scanner_.front.id == TokenID.flowSequenceStart)
|
|
{
|
|
endMark = scanner_.front.endMark;
|
|
state_ = &parseFlowSequenceEntry!(Yes.first);
|
|
return sequenceStartEvent(startMark, endMark, anchor, tag,
|
|
implicit, CollectionStyle.flow);
|
|
}
|
|
|
|
if(scanner_.front.id == TokenID.flowMappingStart)
|
|
{
|
|
endMark = scanner_.front.endMark;
|
|
state_ = &parseFlowMappingKey!(Yes.first);
|
|
return mappingStartEvent(startMark, endMark, anchor, tag,
|
|
implicit, CollectionStyle.flow);
|
|
}
|
|
|
|
if(block && scanner_.front.id == TokenID.blockSequenceStart)
|
|
{
|
|
endMark = scanner_.front.endMark;
|
|
state_ = &parseBlockSequenceEntry!(Yes.first);
|
|
return sequenceStartEvent(startMark, endMark, anchor, tag,
|
|
implicit, CollectionStyle.block);
|
|
}
|
|
|
|
if(block && scanner_.front.id == TokenID.blockMappingStart)
|
|
{
|
|
endMark = scanner_.front.endMark;
|
|
state_ = &parseBlockMappingKey!(Yes.first);
|
|
return mappingStartEvent(startMark, endMark, anchor, tag,
|
|
implicit, CollectionStyle.block);
|
|
}
|
|
|
|
if(anchor !is null || tag !is null)
|
|
{
|
|
state_ = popState();
|
|
|
|
//PyYAML uses a tuple(implicit, false) for the second last arg here,
|
|
//but the second bool is never used after that - so we don't use it.
|
|
|
|
//Empty scalars are allowed even if a tag or an anchor is specified.
|
|
return scalarEvent(startMark, endMark, anchor, tag,
|
|
implicit , "");
|
|
}
|
|
|
|
const token = scanner_.front;
|
|
throw new ParserException("While parsing a " ~ (block ? "block" : "flow") ~ " node",
|
|
startMark, "expected node content, but found: "
|
|
~ token.idString, token.startMark);
|
|
}
|
|
|
|
/// Handle escape sequences in a double quoted scalar.
|
|
///
|
|
/// Moved here from scanner as it can't always be done in-place with slices.
|
|
string handleDoubleQuotedScalarEscapes(const(char)[] tokenValue) const @safe
|
|
{
|
|
string notInPlace;
|
|
bool inEscape;
|
|
auto appender = appender!(string)();
|
|
for(const(char)[] oldValue = tokenValue; !oldValue.empty();)
|
|
{
|
|
const dchar c = oldValue.front();
|
|
oldValue.popFront();
|
|
|
|
if(!inEscape)
|
|
{
|
|
if(c != '\\')
|
|
{
|
|
if(notInPlace is null) { appender.put(c); }
|
|
else { notInPlace ~= c; }
|
|
continue;
|
|
}
|
|
// Escape sequence starts with a '\'
|
|
inEscape = true;
|
|
continue;
|
|
}
|
|
|
|
import dyaml.escapes;
|
|
scope(exit) { inEscape = false; }
|
|
|
|
// 'Normal' escape sequence.
|
|
if(c.among!(escapes))
|
|
{
|
|
if(notInPlace is null)
|
|
{
|
|
// \L and \C can't be handled in place as the expand into
|
|
// many-byte unicode chars
|
|
if(c != 'L' && c != 'P')
|
|
{
|
|
appender.put(dyaml.escapes.fromEscape(c));
|
|
continue;
|
|
}
|
|
// Need to duplicate as we won't fit into
|
|
// token.value - which is what appender uses
|
|
notInPlace = appender.data.dup;
|
|
notInPlace ~= dyaml.escapes.fromEscape(c);
|
|
continue;
|
|
}
|
|
notInPlace ~= dyaml.escapes.fromEscape(c);
|
|
continue;
|
|
}
|
|
|
|
// Unicode char written in hexadecimal in an escape sequence.
|
|
if(c.among!(escapeHexCodeList))
|
|
{
|
|
// Scanner has already checked that the hex string is valid.
|
|
|
|
const hexLength = dyaml.escapes.escapeHexLength(c);
|
|
// Any hex digits are 1-byte so this works.
|
|
const(char)[] hex = oldValue[0 .. hexLength];
|
|
oldValue = oldValue[hexLength .. $];
|
|
import std.ascii : isHexDigit;
|
|
assert(!hex.canFind!(d => !d.isHexDigit),
|
|
"Scanner must ensure the hex string is valid");
|
|
|
|
const decoded = cast(dchar)parse!int(hex, 16u);
|
|
if(notInPlace is null) { appender.put(decoded); }
|
|
else { notInPlace ~= decoded; }
|
|
continue;
|
|
}
|
|
|
|
assert(false, "Scanner must handle unsupported escapes");
|
|
}
|
|
|
|
return notInPlace is null ? appender.data : notInPlace;
|
|
}
|
|
|
|
/**
|
|
* Process a tag string retrieved from a tag token.
|
|
*
|
|
* Params: tag = Tag before processing.
|
|
* handleEnd = Index in tag where tag handle ends and tag suffix
|
|
* starts.
|
|
* startMark = Position of the node the tag belongs to.
|
|
* tagMark = Position of the tag.
|
|
*/
|
|
string processTag(const string tag, const uint handleEnd,
|
|
const Mark startMark, const Mark tagMark)
|
|
const @safe
|
|
{
|
|
const handle = tag[0 .. handleEnd];
|
|
const suffix = tag[handleEnd .. $];
|
|
|
|
if(handle.length > 0)
|
|
{
|
|
string replacement;
|
|
foreach(ref pair; tagDirectives_)
|
|
{
|
|
if(pair.handle == handle)
|
|
{
|
|
replacement = pair.prefix;
|
|
break;
|
|
}
|
|
}
|
|
//handle must be in tagDirectives_
|
|
enforce(replacement !is null,
|
|
new ParserException("While parsing a node", startMark,
|
|
"found undefined tag handle: " ~ handle, tagMark));
|
|
return replacement ~ suffix;
|
|
}
|
|
return suffix;
|
|
}
|
|
|
|
///Wrappers to parse nodes.
|
|
Event parseBlockNode() @safe {return parseNode(Yes.block);}
|
|
Event parseFlowNode() @safe {return parseNode(No.block);}
|
|
Event parseBlockNodeOrIndentlessSequence() @safe {return parseNode(Yes.block, Yes.indentlessSequence);}
|
|
|
|
///block_sequence ::= BLOCK-SEQUENCE-START (BLOCK-ENTRY block_node?)* BLOCK-END
|
|
|
|
///Parse an entry of a block sequence. If first is true, this is the first entry.
|
|
Event parseBlockSequenceEntry(Flag!"first" first)() @safe
|
|
{
|
|
static if(first)
|
|
{
|
|
pushMark(scanner_.front.startMark);
|
|
scanner_.popFront();
|
|
}
|
|
|
|
if(scanner_.front.id == TokenID.blockEntry)
|
|
{
|
|
const token = scanner_.front;
|
|
scanner_.popFront();
|
|
if(!scanner_.front.id.among!(TokenID.blockEntry, TokenID.blockEnd))
|
|
{
|
|
pushState(&parseBlockSequenceEntry!(No.first));
|
|
return parseBlockNode();
|
|
}
|
|
|
|
state_ = &parseBlockSequenceEntry!(No.first);
|
|
return processEmptyScalar(token.endMark);
|
|
}
|
|
|
|
if(scanner_.front.id != TokenID.blockEnd)
|
|
{
|
|
const token = scanner_.front;
|
|
throw new ParserException("While parsing a block collection", marks_.data.back,
|
|
"expected block end, but found " ~ token.idString,
|
|
token.startMark);
|
|
}
|
|
|
|
state_ = popState();
|
|
popMark();
|
|
const token = scanner_.front;
|
|
scanner_.popFront();
|
|
return sequenceEndEvent(token.startMark, token.endMark);
|
|
}
|
|
|
|
///indentless_sequence ::= (BLOCK-ENTRY block_node?)+
|
|
|
|
///Parse an entry of an indentless sequence.
|
|
Event parseIndentlessSequenceEntry() @safe
|
|
{
|
|
if(scanner_.front.id == TokenID.blockEntry)
|
|
{
|
|
const token = scanner_.front;
|
|
scanner_.popFront();
|
|
|
|
if(!scanner_.front.id.among!(TokenID.blockEntry, TokenID.key,
|
|
TokenID.value, TokenID.blockEnd))
|
|
{
|
|
pushState(&parseIndentlessSequenceEntry);
|
|
return parseBlockNode();
|
|
}
|
|
|
|
state_ = &parseIndentlessSequenceEntry;
|
|
return processEmptyScalar(token.endMark);
|
|
}
|
|
|
|
state_ = popState();
|
|
const token = scanner_.front;
|
|
return sequenceEndEvent(token.startMark, token.endMark);
|
|
}
|
|
|
|
/**
|
|
* block_mapping ::= BLOCK-MAPPING_START
|
|
* ((KEY block_node_or_indentless_sequence?)?
|
|
* (VALUE block_node_or_indentless_sequence?)?)*
|
|
* BLOCK-END
|
|
*/
|
|
|
|
///Parse a key in a block mapping. If first is true, this is the first key.
|
|
Event parseBlockMappingKey(Flag!"first" first)() @safe
|
|
{
|
|
static if(first)
|
|
{
|
|
pushMark(scanner_.front.startMark);
|
|
scanner_.popFront();
|
|
}
|
|
|
|
if(scanner_.front.id == TokenID.key)
|
|
{
|
|
const token = scanner_.front;
|
|
scanner_.popFront();
|
|
|
|
if(!scanner_.front.id.among!(TokenID.key, TokenID.value, TokenID.blockEnd))
|
|
{
|
|
pushState(&parseBlockMappingValue);
|
|
return parseBlockNodeOrIndentlessSequence();
|
|
}
|
|
|
|
state_ = &parseBlockMappingValue;
|
|
return processEmptyScalar(token.endMark);
|
|
}
|
|
|
|
if(scanner_.front.id != TokenID.blockEnd)
|
|
{
|
|
const token = scanner_.front;
|
|
throw new ParserException("While parsing a block mapping", marks_.data.back,
|
|
"expected block end, but found: " ~ token.idString,
|
|
token.startMark);
|
|
}
|
|
|
|
state_ = popState();
|
|
popMark();
|
|
const token = scanner_.front;
|
|
scanner_.popFront();
|
|
return mappingEndEvent(token.startMark, token.endMark);
|
|
}
|
|
|
|
///Parse a value in a block mapping.
|
|
Event parseBlockMappingValue() @safe
|
|
{
|
|
if(scanner_.front.id == TokenID.value)
|
|
{
|
|
const token = scanner_.front;
|
|
scanner_.popFront();
|
|
|
|
if(!scanner_.front.id.among!(TokenID.key, TokenID.value, TokenID.blockEnd))
|
|
{
|
|
pushState(&parseBlockMappingKey!(No.first));
|
|
return parseBlockNodeOrIndentlessSequence();
|
|
}
|
|
|
|
state_ = &parseBlockMappingKey!(No.first);
|
|
return processEmptyScalar(token.endMark);
|
|
}
|
|
|
|
state_= &parseBlockMappingKey!(No.first);
|
|
return processEmptyScalar(scanner_.front.startMark);
|
|
}
|
|
|
|
/**
|
|
* flow_sequence ::= FLOW-SEQUENCE-START
|
|
* (flow_sequence_entry FLOW-ENTRY)*
|
|
* flow_sequence_entry?
|
|
* FLOW-SEQUENCE-END
|
|
* flow_sequence_entry ::= flow_node | KEY flow_node? (VALUE flow_node?)?
|
|
*
|
|
* Note that while production rules for both flow_sequence_entry and
|
|
* flow_mapping_entry are equal, their interpretations are different.
|
|
* For `flow_sequence_entry`, the part `KEY flow_node? (VALUE flow_node?)?`
|
|
* generate an inline mapping (set syntax).
|
|
*/
|
|
|
|
///Parse an entry in a flow sequence. If first is true, this is the first entry.
|
|
Event parseFlowSequenceEntry(Flag!"first" first)() @safe
|
|
{
|
|
static if(first)
|
|
{
|
|
pushMark(scanner_.front.startMark);
|
|
scanner_.popFront();
|
|
}
|
|
|
|
if(scanner_.front.id != TokenID.flowSequenceEnd)
|
|
{
|
|
static if(!first)
|
|
{
|
|
if(scanner_.front.id == TokenID.flowEntry)
|
|
{
|
|
scanner_.popFront();
|
|
}
|
|
else
|
|
{
|
|
const token = scanner_.front;
|
|
throw new ParserException("While parsing a flow sequence", marks_.data.back,
|
|
"expected ',' or ']', but got: " ~
|
|
token.idString, token.startMark);
|
|
}
|
|
}
|
|
|
|
if(scanner_.front.id == TokenID.key)
|
|
{
|
|
const token = scanner_.front;
|
|
state_ = &parseFlowSequenceEntryMappingKey;
|
|
return mappingStartEvent(token.startMark, token.endMark,
|
|
null, null, true, CollectionStyle.flow);
|
|
}
|
|
else if(scanner_.front.id != TokenID.flowSequenceEnd)
|
|
{
|
|
pushState(&parseFlowSequenceEntry!(No.first));
|
|
return parseFlowNode();
|
|
}
|
|
}
|
|
|
|
const token = scanner_.front;
|
|
scanner_.popFront();
|
|
state_ = popState();
|
|
popMark();
|
|
return sequenceEndEvent(token.startMark, token.endMark);
|
|
}
|
|
|
|
///Parse a key in flow context.
|
|
Event parseFlowKey(in Event delegate() @safe nextState) @safe
|
|
{
|
|
const token = scanner_.front;
|
|
scanner_.popFront();
|
|
|
|
if(!scanner_.front.id.among!(TokenID.value, TokenID.flowEntry,
|
|
TokenID.flowSequenceEnd))
|
|
{
|
|
pushState(nextState);
|
|
return parseFlowNode();
|
|
}
|
|
|
|
state_ = nextState;
|
|
return processEmptyScalar(token.endMark);
|
|
}
|
|
|
|
///Parse a mapping key in an entry in a flow sequence.
|
|
Event parseFlowSequenceEntryMappingKey() @safe
|
|
{
|
|
return parseFlowKey(&parseFlowSequenceEntryMappingValue);
|
|
}
|
|
|
|
///Parse a mapping value in a flow context.
|
|
Event parseFlowValue(TokenID checkId, in Event delegate() @safe nextState)
|
|
@safe
|
|
{
|
|
if(scanner_.front.id == TokenID.value)
|
|
{
|
|
const token = scanner_.front;
|
|
scanner_.popFront();
|
|
if(!scanner_.front.id.among(TokenID.flowEntry, checkId))
|
|
{
|
|
pushState(nextState);
|
|
return parseFlowNode();
|
|
}
|
|
|
|
state_ = nextState;
|
|
return processEmptyScalar(token.endMark);
|
|
}
|
|
|
|
state_ = nextState;
|
|
return processEmptyScalar(scanner_.front.startMark);
|
|
}
|
|
|
|
///Parse a mapping value in an entry in a flow sequence.
|
|
Event parseFlowSequenceEntryMappingValue() @safe
|
|
{
|
|
return parseFlowValue(TokenID.flowSequenceEnd,
|
|
&parseFlowSequenceEntryMappingEnd);
|
|
}
|
|
|
|
///Parse end of a mapping in a flow sequence entry.
|
|
Event parseFlowSequenceEntryMappingEnd() @safe
|
|
{
|
|
state_ = &parseFlowSequenceEntry!(No.first);
|
|
const token = scanner_.front;
|
|
return mappingEndEvent(token.startMark, token.startMark);
|
|
}
|
|
|
|
/**
|
|
* flow_mapping ::= FLOW-MAPPING-START
|
|
* (flow_mapping_entry FLOW-ENTRY)*
|
|
* flow_mapping_entry?
|
|
* FLOW-MAPPING-END
|
|
* flow_mapping_entry ::= flow_node | KEY flow_node? (VALUE flow_node?)?
|
|
*/
|
|
|
|
///Parse a key in a flow mapping.
|
|
Event parseFlowMappingKey(Flag!"first" first)() @safe
|
|
{
|
|
static if(first)
|
|
{
|
|
pushMark(scanner_.front.startMark);
|
|
scanner_.popFront();
|
|
}
|
|
|
|
if(scanner_.front.id != TokenID.flowMappingEnd)
|
|
{
|
|
static if(!first)
|
|
{
|
|
if(scanner_.front.id == TokenID.flowEntry)
|
|
{
|
|
scanner_.popFront();
|
|
}
|
|
else
|
|
{
|
|
const token = scanner_.front;
|
|
throw new ParserException("While parsing a flow mapping", marks_.data.back,
|
|
"expected ',' or '}', but got: " ~
|
|
token.idString, token.startMark);
|
|
}
|
|
}
|
|
|
|
if(scanner_.front.id == TokenID.key)
|
|
{
|
|
return parseFlowKey(&parseFlowMappingValue);
|
|
}
|
|
|
|
if(scanner_.front.id != TokenID.flowMappingEnd)
|
|
{
|
|
pushState(&parseFlowMappingEmptyValue);
|
|
return parseFlowNode();
|
|
}
|
|
}
|
|
|
|
const token = scanner_.front;
|
|
scanner_.popFront();
|
|
state_ = popState();
|
|
popMark();
|
|
return mappingEndEvent(token.startMark, token.endMark);
|
|
}
|
|
|
|
///Parse a value in a flow mapping.
|
|
Event parseFlowMappingValue() @safe
|
|
{
|
|
return parseFlowValue(TokenID.flowMappingEnd, &parseFlowMappingKey!(No.first));
|
|
}
|
|
|
|
///Parse an empty value in a flow mapping.
|
|
Event parseFlowMappingEmptyValue() @safe
|
|
{
|
|
state_ = &parseFlowMappingKey!(No.first);
|
|
return processEmptyScalar(scanner_.front.startMark);
|
|
}
|
|
|
|
///Return an empty scalar.
|
|
Event processEmptyScalar(const Mark mark) @safe pure nothrow const @nogc
|
|
{
|
|
return scalarEvent(mark, mark, null, null, true, "");
|
|
}
|
|
}
|