// This file is part of YamlDotNet - A .NET library for YAML. // Copyright (c) Antoine Aubry and contributors // // Permission is hereby granted, free of charge, to any person obtaining a copy of // this software and associated documentation files (the "Software"), to deal in // the Software without restriction, including without limitation the rights to // use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies // of the Software, and to permit persons to whom the Software is furnished to do // so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Text; using YamlDotNet.Core.Tokens; namespace YamlDotNet.Core { /// /// Converts a sequence of characters into a sequence of YAML tokens. /// public class Scanner : IScanner { private const int MaxVersionNumberLength = 9; private static readonly IDictionary SimpleEscapeCodes = new SortedDictionary { { '0', '\0' }, { 'a', '\x07' }, { 'b', '\x08' }, { 't', '\x09' }, { '\t', '\x09' }, { 'n', '\x0A' }, { 'v', '\x0B' }, { 'f', '\x0C' }, { 'r', '\x0D' }, { 'e', '\x1B' }, { ' ', '\x20' }, { '"', '"' }, { '\\', '\\' }, { '/', '/' }, { 'N', '\x85' }, { '_', '\xA0' }, { 'L', '\x2028' }, { 'P', '\x2029' } }; private readonly Stack indents = new Stack(); private readonly InsertionQueue tokens = new InsertionQueue(); private readonly Stack simpleKeys = new Stack(); private readonly CharacterAnalyzer analyzer; private readonly Cursor cursor; private bool streamStartProduced; private bool streamEndProduced; private bool plainScalarFollowedByComment; private int flowSequenceStartLine; private int indent = -1; private bool simpleKeyAllowed; private int flowLevel; private int tokensParsed; private bool tokenAvailable; private Token? previous; private Anchor? previousAnchor; private bool IsDocumentStart() => !analyzer.EndOfInput && cursor.LineOffset == 0 && analyzer.Check('-', 0) && analyzer.Check('-', 1) && analyzer.Check('-', 2) && analyzer.IsWhiteBreakOrZero(3); private bool IsDocumentEnd() => !analyzer.EndOfInput && cursor.LineOffset == 0 && analyzer.Check('.', 0) && analyzer.Check('.', 1) && analyzer.Check('.', 2) && analyzer.IsWhiteBreakOrZero(3); private bool IsDocumentIndicator() => IsDocumentStart() || IsDocumentEnd(); public bool SkipComments { get; private set; } /// /// Gets the current token. /// public Token? Current { get; private set; } /// /// Initializes a new instance of the class. /// /// The input. /// Indicates whether comments should be ignored public Scanner(TextReader input, bool skipComments = true) { analyzer = new CharacterAnalyzer(new LookAheadBuffer(input, 1024)); cursor = new Cursor(); SkipComments = skipComments; } /// /// Gets the current position inside the input stream. /// /// The current position. public Mark CurrentPosition { get { return cursor.Mark(); } } /// /// Moves to the next token. /// /// public bool MoveNext() { if (Current != null) { ConsumeCurrent(); } return MoveNextWithoutConsuming(); } public bool MoveNextWithoutConsuming() { if (!tokenAvailable && !streamEndProduced) { FetchMoreTokens(); } if (tokens.Count > 0) { Current = tokens.Dequeue(); tokenAvailable = false; return true; } else { Current = null; return false; } } /// /// Consumes the current token and increments the parsed token count /// public void ConsumeCurrent() { ++tokensParsed; tokenAvailable = false; previous = Current; Current = null; } private char ReadCurrentCharacter() { var currentCharacter = analyzer.Peek(0); Skip(); return currentCharacter; } private char ReadLine() { if (analyzer.Check("\r\n\x85")) // CR LF -> LF --- CR|LF|NEL -> LF { SkipLine(); return '\n'; } var nextChar = analyzer.Peek(0); // LS|PS -> LS|PS SkipLine(); return nextChar; } private void FetchMoreTokens() { // While we need more tokens to fetch, do it. while (true) { // Check if we really need to fetch more tokens. var needsMoreTokens = false; if (tokens.Count == 0) { // Queue is empty. needsMoreTokens = true; } else { // Check if any potential simple key may occupy the head position. foreach (var simpleKey in simpleKeys) { if (simpleKey.IsPossible && simpleKey.TokenNumber == tokensParsed) { needsMoreTokens = true; break; } } } // We are finished. if (!needsMoreTokens) { break; } // Fetch the next token. FetchNextToken(); } tokenAvailable = true; } private static bool StartsWith(StringBuilder what, char start) { return what.Length > 0 && what[0] == start; } /// /// Check the list of potential simple keys and remove the positions that /// cannot contain simple keys anymore. /// private void StaleSimpleKeys() { // Check for a potential simple key for each flow level. foreach (var key in simpleKeys) { // The specification requires that a simple key // - is limited to a single line, // - is shorter than 1024 characters. if (key.IsPossible && (key.Line < cursor.Line || key.Index + 1024 < cursor.Index)) { // Check if the potential simple key to be removed is required. if (key.IsRequired) { var mark = cursor.Mark(); tokens.Enqueue(new Error("While scanning a simple key, could not find expected ':'.", mark, mark)); } key.MarkAsImpossible(); } } } private void FetchNextToken() { // Check if we just started scanning. Fetch STREAM-START then. if (!streamStartProduced) { FetchStreamStart(); return; } // Eat whitespaces and comments until we reach the next token. ScanToNextToken(); // Remove obsolete potential simple keys. StaleSimpleKeys(); // Check the indentation level against the current column. UnrollIndent(cursor.LineOffset); // Ensure that the buffer contains at least 4 characters. 4 is the length // of the longest indicators ('--- ' and '... '). analyzer.Buffer.Cache(4); // Is it the end of the stream? if (analyzer.Buffer.EndOfInput) { FetchStreamEnd(); return; } // Is it a directive? if (cursor.LineOffset == 0 && analyzer.Check('%')) { FetchDirective(); return; } // Is it the document start indicator? if (IsDocumentStart()) { FetchDocumentIndicator(true); return; } // Is it the document end indicator? if (IsDocumentEnd()) { FetchDocumentIndicator(false); return; } // Is it the flow sequence start indicator? if (analyzer.Check('[')) { FetchFlowCollectionStart(true); return; } // Is it the flow mapping start indicator? if (analyzer.Check('{')) { FetchFlowCollectionStart(false); return; } // Is it the flow sequence end indicator? if (analyzer.Check(']')) { FetchFlowCollectionEnd(true); return; } // Is it the flow mapping end indicator? if (analyzer.Check('}')) { FetchFlowCollectionEnd(false); return; } // Is it the flow entry indicator? if (analyzer.Check(',')) { FetchFlowEntry(); return; } // Is it the block entry indicator? if (analyzer.Check('-') && analyzer.IsWhiteBreakOrZero(1)) { FetchBlockEntry(); return; } // Is it the key indicator? if (analyzer.Check('?') && (flowLevel > 0 || analyzer.IsWhiteBreakOrZero(1))) { FetchKey(); return; } // Is it the value indicator? if (analyzer.Check(':') && (flowLevel > 0 || analyzer.IsWhiteBreakOrZero(1)) && !(simpleKeyAllowed && flowLevel > 0)) { FetchValue(); return; } // Is it an alias? if (analyzer.Check('*')) { FetchAnchor(true); return; } // Is it an anchor? if (analyzer.Check('&')) { FetchAnchor(false); return; } // Is it a tag? if (analyzer.Check('!')) { FetchTag(); return; } // Is it a literal scalar? if (analyzer.Check('|') && flowLevel == 0) { FetchBlockScalar(true); return; } // Is it a folded scalar? if (analyzer.Check('>') && flowLevel == 0) { FetchBlockScalar(false); return; } // Is it a single-quoted scalar? if (analyzer.Check('\'')) { FetchFlowScalar(true); return; } // Is it a double-quoted scalar? if (analyzer.Check('"')) { FetchFlowScalar(false); return; } // Is it a plain scalar? // A plain scalar may start with any non-blank characters except // '-', '?', ':', ',', '[', ']', '{', '}', // '#', '&', '*', '!', '|', '>', '\'', '\"', // '%', '@', '`'. // In the block context (and, for the '-' indicator, in the flow context // too), it may also start with the characters // '-', '?', ':' // if it is followed by a non-space character. // The last rule is more restrictive than the specification requires. var isInvalidPlainScalarCharacter = analyzer.IsWhiteBreakOrZero() || analyzer.Check("-?:,[]{}#&*!|>'\"%@`"); var isPlainScalar = !isInvalidPlainScalarCharacter || (analyzer.Check('-') && !analyzer.IsWhite(1)) || (flowLevel == 0 && analyzer.Check("?:") && !analyzer.IsWhiteBreakOrZero(1)) || (simpleKeyAllowed && flowLevel > 0 && analyzer.Check("?:")); if (isPlainScalar) { if (plainScalarFollowedByComment) { var startMark = cursor.Mark(); tokens.Enqueue(new Error("While scanning plain scalar, found a comment between adjacent scalars.", startMark, startMark)); } plainScalarFollowedByComment = false; FetchPlainScalar(); return; } if (simpleKeyAllowed && indent >= cursor.LineOffset && analyzer.IsTab()) { throw new SyntaxErrorException("While scanning a mapping, found invalid tab as indentation."); } if (analyzer.IsWhiteBreakOrZero()) { Skip(); return; } // If we don't determine the token type so far, it is an error. var start = cursor.Mark(); Skip(); var end = cursor.Mark(); throw new SyntaxErrorException(start, end, "While scanning for the next token, found character that cannot start any token."); } private bool CheckWhiteSpace() { return analyzer.Check(' ') || ((flowLevel > 0 || !simpleKeyAllowed) && analyzer.Check('\t')); } private void Skip() { cursor.Skip(); analyzer.Buffer.Skip(1); } private void SkipLine() { if (analyzer.IsCrLf()) { cursor.SkipLineByOffset(2); analyzer.Buffer.Skip(2); } else if (analyzer.IsBreak()) { cursor.SkipLineByOffset(1); analyzer.Buffer.Skip(1); } else if (!analyzer.IsZero()) { throw new InvalidOperationException("Not at a break."); } } private void ScanToNextToken() { // Until the next token is not find. while (true) { // Eat whitespaces. // Tabs are allowed: // - in the flow context; // - in the block context, but not at the beginning of the line or // after '-', '?', or ':' (complex value). while (CheckWhiteSpace()) { Skip(); } ProcessComment(); // If it is a line break, eat it. if (analyzer.IsBreak()) { SkipLine(); // In the block context, a new line may start a simple key. if (flowLevel == 0) { simpleKeyAllowed = true; } } else { // We have find a token. break; } } } private void ProcessComment() { if (analyzer.Check('#')) { var start = cursor.Mark(); // Eat '#' Skip(); // Eat leading whitespace while (analyzer.IsSpace()) { Skip(); } var text = new StringBuilder(); while (!analyzer.IsBreakOrZero()) { text.Append(ReadCurrentCharacter()); } if (!SkipComments) { var isInline = previous != null && previous.End.Line == start.Line && previous.End.Column != 1 && !(previous is StreamStart); tokens.Enqueue(new Comment(text.ToString(), isInline, start, cursor.Mark())); } } } private void FetchStreamStart() { // Initialize the simple key stack. simpleKeys.Push(new SimpleKey()); // A simple key is allowed at the beginning of the stream. simpleKeyAllowed = true; // We have started. streamStartProduced = true; // Create the STREAM-START token and append it to the queue. var mark = cursor.Mark(); tokens.Enqueue(new StreamStart(mark, mark)); } /// /// Pop indentation levels from the indents stack until the current level /// becomes less or equal to the column. For each indentation level, append /// the BLOCK-END token. /// private void UnrollIndent(int column) { // In the flow context, do nothing. if (flowLevel != 0) { return; } // Loop through the indentation levels in the stack. while (indent > column) { // Create a token and append it to the queue. var mark = cursor.Mark(); tokens.Enqueue(new BlockEnd(mark, mark)); // Pop the indentation level. indent = indents.Pop(); } } /// /// Produce the STREAM-END token and shut down the scanner. /// private void FetchStreamEnd() { cursor.ForceSkipLineAfterNonBreak(); // Reset the indentation level. UnrollIndent(-1); // Reset simple keys. RemoveSimpleKey(); simpleKeyAllowed = false; // Create the STREAM-END token and append it to the queue. streamEndProduced = true; var mark = cursor.Mark(); tokens.Enqueue(new StreamEnd(mark, mark)); } private void FetchDirective() { // Reset the indentation level. UnrollIndent(-1); // Reset simple keys. RemoveSimpleKey(); simpleKeyAllowed = false; // Create the YAML-DIRECTIVE or TAG-DIRECTIVE token. var token = ScanDirective(); // Append the token to the queue. if (token != null) { tokens.Enqueue(token); } } /// /// Scan a YAML-DIRECTIVE or TAG-DIRECTIVE token. /// /// Scope: /// %YAML 1.1 # a comment \n /// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ /// %TAG !yaml! tag:yaml.org,2002: \n /// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ /// private Token? ScanDirective() { // Eat '%'. var start = cursor.Mark(); Skip(); // Scan the directive name. var name = ScanDirectiveName(start); // Is it a YAML directive? Token directive; switch (name) { case "YAML": if (previous is DocumentStart || previous is StreamStart || previous is DocumentEnd) { directive = ScanVersionDirectiveValue(start); } else { throw new SemanticErrorException(start, cursor.Mark(), "While scanning a version directive, did not find preceding ."); } break; case "TAG": directive = ScanTagDirectiveValue(start); break; default: // warning: skipping reserved directive line while (!analyzer.Check('#') && !analyzer.IsBreak()) { Skip(); } return null; } // Eat the rest of the line including any comments. while (analyzer.IsWhite()) { Skip(); } ProcessComment(); // Check if we are at the end of the line. if (!analyzer.IsBreakOrZero()) { throw new SyntaxErrorException(start, cursor.Mark(), "While scanning a directive, did not find expected comment or line break."); } // Eat a line break. if (analyzer.IsBreak()) { SkipLine(); } return directive; } /// /// Produce the DOCUMENT-START or DOCUMENT-END token. /// private void FetchDocumentIndicator(bool isStartToken) { // Reset the indentation level. UnrollIndent(-1); // Reset simple keys. RemoveSimpleKey(); simpleKeyAllowed = false; // Consume the token. var start = cursor.Mark(); Skip(); Skip(); Skip(); if (isStartToken) { tokens.Enqueue(new DocumentStart(start, cursor.Mark())); } else { Token? errorToken = null; while (!analyzer.EndOfInput && !analyzer.IsBreak() && !analyzer.Check('#')) { if (!analyzer.IsWhite()) { errorToken = new Error("While scanning a document end, found invalid content after '...' marker.", start, cursor.Mark()); break; } Skip(); } tokens.Enqueue(new DocumentEnd(start, start)); if (errorToken != null) { tokens.Enqueue(errorToken); } } } /// /// Produce the FLOW-SEQUENCE-START or FLOW-MAPPING-START token. /// private void FetchFlowCollectionStart(bool isSequenceToken) { // The indicators '[' and '{' may start a simple key. SaveSimpleKey(); // Increase the flow level. IncreaseFlowLevel(); // A simple key may follow the indicators '[' and '{'. simpleKeyAllowed = true; // Consume the token. var start = cursor.Mark(); Skip(); // Create the FLOW-SEQUENCE-START of FLOW-MAPPING-START token. Token token; if (isSequenceToken) { token = new FlowSequenceStart(start, start); flowSequenceStartLine = token.Start.Line; } else { token = new FlowMappingStart(start, start); } tokens.Enqueue(token); } /// /// Increase the flow level and resize the simple key list if needed. /// private void IncreaseFlowLevel() { // Reset the simple key on the next level. simpleKeys.Push(new SimpleKey()); // Increase the flow level. ++flowLevel; } /// /// Produce the FLOW-SEQUENCE-END or FLOW-MAPPING-END token. /// private void FetchFlowCollectionEnd(bool isSequenceToken) { // Reset any potential simple key on the current flow level. RemoveSimpleKey(); // Decrease the flow level. DecreaseFlowLevel(); // No simple keys after the indicators ']' and '}'. simpleKeyAllowed = false; // Consume the token. var start = cursor.Mark(); Skip(); Token? token, errorToken = null; if (isSequenceToken) { if (analyzer.Check('#')) { errorToken = new Error("While scanning a flow sequence end, found invalid comment after ']'.", start, start); } if (previous is StreamStart && flowSequenceStartLine != start.Line) { tokens.Enqueue(new Error("While scanning a flow sequence end, found mapping key spanning across multiple lines.", start, start)); } token = new FlowSequenceEnd(start, start); } else { token = new FlowMappingEnd(start, start); } tokens.Enqueue(token); if (errorToken != null) { tokens.Enqueue(errorToken); } } /// /// Decrease the flow level. /// private void DecreaseFlowLevel() { // flowLevel could be zero in case of malformed YAML. // Since this is handled elsewhere, just ignore it. if (flowLevel > 0) { --flowLevel; simpleKeys.Pop(); } } /// /// Produce the FLOW-ENTRY token. /// private void FetchFlowEntry() { // Reset any potential simple keys on the current flow level. RemoveSimpleKey(); // Simple keys are allowed after ','. simpleKeyAllowed = true; // Consume the token. var start = cursor.Mark(); Skip(); var end = cursor.Mark(); if (analyzer.Check('#')) { tokens.Enqueue(new Error("While scanning a flow entry, found invalid comment after comma.", start, end)); return; } // Create the FLOW-ENTRY token and append it to the queue. tokens.Enqueue(new FlowEntry(start, end)); } /// /// Produce the BLOCK-ENTRY token. /// private void FetchBlockEntry() { // Check if the scanner is in the block context. if (flowLevel == 0) { // Check if we are allowed to start a new entry. if (!simpleKeyAllowed) { if (previousAnchor != null) { if (previousAnchor.End.Line == cursor.Line) { throw new SemanticErrorException(previousAnchor.Start, previousAnchor.End, "Anchor before sequence entry on same line is not allowed."); } } var mark = cursor.Mark(); tokens.Enqueue(new Error("Block sequence entries are not allowed in this context.", mark, mark)); } // Add the BLOCK-SEQUENCE-START token if needed. RollIndent(cursor.LineOffset, -1, true, cursor.Mark()); } else { // It is an error for the '-' indicator to occur in the flow context, // but we let the Parser detect and report about it because the Parser // is able to point to the context. } // Reset any potential simple keys on the current flow level. RemoveSimpleKey(); // Simple keys are allowed after '-'. simpleKeyAllowed = true; // Consume the token. var start = cursor.Mark(); Skip(); // Create the BLOCK-ENTRY token and append it to the queue. tokens.Enqueue(new BlockEntry(start, cursor.Mark())); } /// /// Produce the KEY token. /// private void FetchKey() { // In the block context, additional checks are required. if (flowLevel == 0) { // Check if we are allowed to start a new key (not necessary simple). if (!simpleKeyAllowed) { var mark = cursor.Mark(); throw new SyntaxErrorException(mark, mark, "Mapping keys are not allowed in this context."); } // Add the BLOCK-MAPPING-START token if needed. RollIndent(cursor.LineOffset, -1, false, cursor.Mark()); } // Reset any potential simple keys on the current flow level. RemoveSimpleKey(); // Simple keys are allowed after '?' in the block context. simpleKeyAllowed = flowLevel == 0; // Consume the token. var start = cursor.Mark(); Skip(); // Create the KEY token and append it to the queue. tokens.Enqueue(new Key(start, cursor.Mark())); } /// /// Produce the VALUE token. /// private void FetchValue() { var simpleKey = simpleKeys.Peek(); // Have we find a simple key? if (simpleKey.IsPossible) { // Create the KEY token and insert it into the queue. tokens.Insert(simpleKey.TokenNumber - tokensParsed, new Key(simpleKey.Mark, simpleKey.Mark)); // In the block context, we may need to add the BLOCK-MAPPING-START token. RollIndent(simpleKey.LineOffset, simpleKey.TokenNumber, false, simpleKey.Mark); // Remove the simple key. simpleKey.MarkAsImpossible(); // A simple key cannot follow another simple key. simpleKeyAllowed = false; } else { // The ':' indicator follows a complex key. // Simple keys after ':' are allowed in the block context. var localSimpleKeyAllowed = flowLevel == 0; // In the block context, extra checks are required. if (localSimpleKeyAllowed) { // Check if we are allowed to start a complex value. if (!simpleKeyAllowed) { var mark = cursor.Mark(); tokens.Enqueue(new Error("Mapping values are not allowed in this context.", mark, mark)); return; } // Add the BLOCK-MAPPING-START token if needed. RollIndent(cursor.LineOffset, -1, false, cursor.Mark()); // Check if we are dealing with empty key. if (cursor.LineOffset == 0 && simpleKey.LineOffset == 0) { // Create the KEY token and insert it into the queue. tokens.Insert(tokens.Count, new Key(simpleKey.Mark, simpleKey.Mark)); // A simple key cannot follow another simple key. localSimpleKeyAllowed = false; } } simpleKeyAllowed = localSimpleKeyAllowed; } // Consume the token. var start = cursor.Mark(); Skip(); // Create the VALUE token and append it to the queue. tokens.Enqueue(new Value(start, cursor.Mark())); } /// /// Push the current indentation level to the stack and set the new level /// the current column is greater than the indentation level. In this case, /// append or insert the specified token into the token queue. /// private void RollIndent(int column, int number, bool isSequence, Mark position) { // In the flow context, do nothing. if (flowLevel > 0) { return; } if (indent < column) { // Push the current indentation level to the stack and set the new // indentation level. indents.Push(indent); indent = column; // Create a token and insert it into the queue. Token token; if (isSequence) { token = new BlockSequenceStart(position, position); } else { token = new BlockMappingStart(position, position); } if (number == -1) { tokens.Enqueue(token); } else { tokens.Insert(number - tokensParsed, token); } } } /// /// Produce the ALIAS or ANCHOR token. /// private void FetchAnchor(bool isAlias) { // An anchor or an alias could be a simple key. SaveSimpleKey(); // A simple key cannot follow an anchor or an alias. simpleKeyAllowed = false; // Create the ALIAS or ANCHOR token and append it to the queue. tokens.Enqueue(ScanAnchor(isAlias)); } private Token ScanAnchor(bool isAlias) { // Eat the indicator character. var start = cursor.Mark(); Skip(); var isAliasKey = false; if (isAlias) { var key = simpleKeys.Peek(); isAliasKey = key.IsRequired && key.IsPossible; } // Consume the value. // YAML 1.2 - section 6.9.2."Node Anchors" specifies disallowed characters // in the anchor name as follows: // '[', ']', '{', '}' and ',' // ref: https://yaml.org/spec/1.2/spec.html#id2785586 var value = new StringBuilder(); while (!analyzer.IsWhiteBreakOrZero()) { // Anchor: read all allowed characters // Alias: read all allowed characters except colon (':'); read colon when token is: // * not used in key OR // * used in key and colon is not last character if (!analyzer.Check("[]{},") && !(isAliasKey && analyzer.Check(':') && analyzer.IsWhiteBreakOrZero(1))) { value.Append(ReadCurrentCharacter()); } else { break; } } // Check if length of the anchor is greater than 0 and it is followed by // a whitespace character or one of the indicators: // '?', ':', ',', ']', '}', '%', '@', '`'. if (value.Length == 0 || !(analyzer.IsWhiteBreakOrZero() || analyzer.Check("?:,]}%@`"))) { throw new SyntaxErrorException(start, cursor.Mark(), "While scanning an anchor or alias, found value containing disallowed: []{},"); } // Create a token. var name = new AnchorName(value.ToString()); if (isAlias) { return new AnchorAlias(name, start, cursor.Mark()); } else { return previousAnchor = new Anchor(name, start, cursor.Mark()); } } /// /// Produce the TAG token. /// private void FetchTag() { // A tag could be a simple key. SaveSimpleKey(); // A simple key cannot follow a tag. simpleKeyAllowed = false; // Create the TAG token and append it to the queue. tokens.Enqueue(ScanTag()); } /// /// Scan a TAG token. /// Token ScanTag() { var start = cursor.Mark(); // Check if the tag is in the canonical form. string handle; string suffix; if (analyzer.Check('<', 1)) { // Set the handle to '' handle = string.Empty; // Eat '!<' Skip(); Skip(); // Consume the tag value. suffix = ScanTagUri(null, start); // Check for '>' and eat it. if (!analyzer.Check('>')) { throw new SyntaxErrorException(start, cursor.Mark(), "While scanning a tag, did not find the expected '>'."); } Skip(); } else { // The tag has either the '!suffix' or the '!handle!suffix' form. // First, try to scan a handle. var firstPart = ScanTagHandle(false, start); // Check if it is, indeed, handle. if (firstPart.Length > 1 && firstPart[0] == '!' && firstPart[firstPart.Length - 1] == '!') { handle = firstPart; // Scan the suffix now. suffix = ScanTagUri(null, start); } else { // It wasn't a handle after all. Scan the rest of the tag. suffix = ScanTagUri(firstPart, start); // Set the handle to '!'. handle = "!"; // A special case: the '!' tag. Set the handle to '' and the // suffix to '!'. if (suffix.Length == 0) { suffix = handle; handle = string.Empty; } } } // Check the character which ends the tag. if (!analyzer.IsWhiteBreakOrZero() && !analyzer.Check(',')) { throw new SyntaxErrorException(start, cursor.Mark(), "While scanning a tag, did not find expected whitespace, comma or line break."); } // Create a token. return new Tag(handle, suffix, start, cursor.Mark()); } /// /// Produce the SCALAR(...,literal) or SCALAR(...,folded) tokens. /// private void FetchBlockScalar(bool isLiteral) { // Remove any potential simple keys. RemoveSimpleKey(); // A simple key may follow a block scalar. simpleKeyAllowed = true; // Create the SCALAR token and append it to the queue. tokens.Enqueue(ScanBlockScalar(isLiteral)); } /// /// Scan a block scalar. /// Token ScanBlockScalar(bool isLiteral) { var value = new StringBuilder(); var leadingBreak = new StringBuilder(); var trailingBreaks = new StringBuilder(); var chomping = 0; var increment = 0; var currentIndent = 0; var leadingBlank = false; bool? isFirstLine = null; // Eat the indicator '|' or '>'. var start = cursor.Mark(); Skip(); // Check for a chomping indicator. if (analyzer.Check("+-")) { // Set the chomping method and eat the indicator. chomping = analyzer.Check('+') ? +1 : -1; Skip(); // Check for an indentation indicator. if (analyzer.IsDigit()) { // Check that the indentation is greater than 0. if (analyzer.Check('0')) { throw new SyntaxErrorException(start, cursor.Mark(), "While scanning a block scalar, found an indentation indicator equal to 0."); } // Get the indentation level and eat the indicator. increment = analyzer.AsDigit(); Skip(); } } // Do the same as above, but in the opposite order. else if (analyzer.IsDigit()) { if (analyzer.Check('0')) { throw new SyntaxErrorException(start, cursor.Mark(), "While scanning a block scalar, found an indentation indicator equal to 0."); } increment = analyzer.AsDigit(); Skip(); if (analyzer.Check("+-")) { chomping = analyzer.Check('+') ? +1 : -1; Skip(); } } // Check if there is a comment without whitespace after block scalar indicator (yaml-test-suite: X4QW). if (analyzer.Check('#')) { throw new SyntaxErrorException(start, cursor.Mark(), "While scanning a block scalar, found a comment without whtespace after '>' indicator."); } // Eat whitespaces and comments to the end of the line. while (analyzer.IsWhite()) { Skip(); } ProcessComment(); // Check if we are at the end of the line. if (!analyzer.IsBreakOrZero()) { throw new SyntaxErrorException(start, cursor.Mark(), "While scanning a block scalar, did not find expected comment or line break."); } // Eat a line break. if (analyzer.IsBreak()) { SkipLine(); if (!isFirstLine.HasValue) { isFirstLine = true; } else if (isFirstLine == true) { isFirstLine = false; } } var end = cursor.Mark(); // Set the indentation level if it was specified. if (increment != 0) { currentIndent = indent >= 0 ? indent + increment : increment; } // Scan the leading line breaks and determine the indentation level if needed. currentIndent = ScanBlockScalarBreaks(currentIndent, trailingBreaks, isLiteral, ref end, ref isFirstLine); isFirstLine = false; // Scan the block scalar content. while (cursor.LineOffset == currentIndent && !analyzer.IsZero() && !IsDocumentEnd()) { // We are at the beginning of a non-empty line. // Is it a trailing whitespace? var trailingBlank = analyzer.IsWhite(); // Check if we need to fold the leading line break. if (!isLiteral && StartsWith(leadingBreak, '\n') && !leadingBlank && !trailingBlank) { // Do we need to join the lines by space? if (trailingBreaks.Length == 0) { value.Append(' '); } leadingBreak.Length = 0; } else { value.Append(leadingBreak.ToString()); leadingBreak.Length = 0; } // Append the remaining line breaks. value.Append(trailingBreaks.ToString()); trailingBreaks.Length = 0; // Is it a leading whitespace? leadingBlank = analyzer.IsWhite(); // Consume the current line. while (!analyzer.IsBreakOrZero()) { value.Append(ReadCurrentCharacter()); } // Consume the line break. var lineBreak = ReadLine(); if (lineBreak != '\0') { leadingBreak.Append(lineBreak); } // Eat the following indentation spaces and line breaks. currentIndent = ScanBlockScalarBreaks(currentIndent, trailingBreaks, isLiteral, ref end, ref isFirstLine); } // Chomp the tail. if (chomping != -1) { value.Append(leadingBreak); } if (chomping == 1) { value.Append(trailingBreaks); } // Create a token. var style = isLiteral ? ScalarStyle.Literal : ScalarStyle.Folded; return new Scalar(value.ToString(), style, start, end); } /// /// Scan indentation spaces and line breaks for a block scalar. Determine the /// indentation level if needed. /// private int ScanBlockScalarBreaks(int currentIndent, StringBuilder breaks, bool isLiteral, ref Mark end, ref bool? isFirstLine) { var maxIndent = 0; var indentOfFirstLine = -1; end = cursor.Mark(); // Eat the indentation spaces and line breaks. while (true) { // Eat the indentation spaces. while ((currentIndent == 0 || cursor.LineOffset < currentIndent) && analyzer.IsSpace()) { Skip(); } if (cursor.LineOffset > maxIndent) { maxIndent = cursor.LineOffset; } // Have we find a non-empty line? if (!analyzer.IsBreak()) { if (isLiteral && isFirstLine == true) { var localIndent = cursor.LineOffset; var i = 0; while (!analyzer.IsBreak(i) && analyzer.IsSpace(i)) { ++i; ++localIndent; } if (analyzer.IsBreak(i) && localIndent > cursor.LineOffset) { isFirstLine = false; indentOfFirstLine = localIndent; } } break; } if (isFirstLine == true) { isFirstLine = false; indentOfFirstLine = cursor.LineOffset; } // Consume the line break. breaks.Append(ReadLine()); end = cursor.Mark(); } // Check if first line after literal is all spaces and count of spaces is more than "1 + currentIndent". if (isLiteral && indentOfFirstLine > 1 && currentIndent < indentOfFirstLine - 1) { // W9L4 throw new SemanticErrorException(end, cursor.Mark(), "While scanning a literal block scalar, found extra spaces in first line."); } if (!isLiteral && maxIndent > cursor.LineOffset && indentOfFirstLine > -1) { // S98Z throw new SemanticErrorException(end, cursor.Mark(), "While scanning a literal block scalar, found more spaces in lines above first content line."); } // Determine the indentation level if needed. if (currentIndent == 0 && (cursor.LineOffset > 0 || indent > -1)) { currentIndent = Math.Max(maxIndent, Math.Max(indent + 1, 1)); } return currentIndent; } /// /// Produce the SCALAR(...,single-quoted) or SCALAR(...,double-quoted) tokens. /// private void FetchFlowScalar(bool isSingleQuoted) { // A plain scalar could be a simple key. SaveSimpleKey(); // A simple key cannot follow a flow scalar. simpleKeyAllowed = false; // Create the SCALAR token and append it to the queue. tokens.Enqueue(ScanFlowScalar(isSingleQuoted)); // Check if there is a comment subsequently after double-quoted scalar without space. if (!isSingleQuoted && analyzer.Check('#')) { var start = cursor.Mark(); tokens.Enqueue(new Error("While scanning a flow sequence end, found invalid comment after double-quoted scalar.", start, start)); } } /// /// Scan a quoted scalar. /// private Token ScanFlowScalar(bool isSingleQuoted) { // Eat the left quote. var start = cursor.Mark(); Skip(); // Consume the content of the quoted scalar. var value = new StringBuilder(); var whitespaces = new StringBuilder(); var leadingBreak = new StringBuilder(); var trailingBreaks = new StringBuilder(); var hasLeadingBlanks = false; while (true) { // Check that there are no document indicators at the beginning of the line. if (IsDocumentIndicator()) { throw new SyntaxErrorException(start, cursor.Mark(), "While scanning a quoted scalar, found unexpected document indicator."); } // Check for EOF. if (analyzer.IsZero()) { throw new SyntaxErrorException(start, cursor.Mark(), "While scanning a quoted scalar, found unexpected end of stream."); } if (hasLeadingBlanks && !isSingleQuoted && indent >= cursor.LineOffset) { throw new SyntaxErrorException(start, cursor.Mark(), "While scanning a multi-line double-quoted scalar, found wrong indentation."); } hasLeadingBlanks = false; // Consume non-blank characters. while (!analyzer.IsWhiteBreakOrZero()) { // Check for an escaped single quote. if (isSingleQuoted && analyzer.Check('\'', 0) && analyzer.Check('\'', 1)) { value.Append('\''); Skip(); Skip(); } // Check for the right quote. else if (analyzer.Check(isSingleQuoted ? '\'' : '"')) { break; } // Check for an escaped line break. else if (!isSingleQuoted && analyzer.Check('\\') && analyzer.IsBreak(1)) { Skip(); SkipLine(); hasLeadingBlanks = true; break; } // Check for an escape sequence. else if (!isSingleQuoted && analyzer.Check('\\')) { var codeLength = 0; // Check the escape character. var escapeCharacter = analyzer.Peek(1); switch (escapeCharacter) { case 'x': codeLength = 2; break; case 'u': codeLength = 4; break; case 'U': codeLength = 8; break; default: char unescapedCharacter; if (SimpleEscapeCodes.TryGetValue(escapeCharacter, out unescapedCharacter)) { value.Append(unescapedCharacter); } else { throw new SyntaxErrorException(start, cursor.Mark(), "While scanning a quoted scalar, found unknown escape character."); } break; } Skip(); Skip(); // Consume an arbitrary escape code. if (codeLength > 0) { var character = 0; // Scan the character value. for (var k = 0; k < codeLength; ++k) { if (!analyzer.IsHex(k)) { throw new SyntaxErrorException(start, cursor.Mark(), "While scanning a quoted scalar, did not find expected hexadecimal number."); } character = ((character << 4) + analyzer.AsHex(k)); } // Check the value and write the character. if ((character >= 0xD800 && character <= 0xDFFF) || character > 0x10FFFF) { throw new SyntaxErrorException(start, cursor.Mark(), "While scanning a quoted scalar, found invalid Unicode character escape code."); } value.Append(char.ConvertFromUtf32(character)); // Advance the pointer. for (var k = 0; k < codeLength; ++k) { Skip(); } } } else { // It is a non-escaped non-blank character. value.Append(ReadCurrentCharacter()); } } // Check if we are at the end of the scalar. if (analyzer.Check(isSingleQuoted ? '\'' : '"')) { break; } // Consume blank characters. while (analyzer.IsWhite() || analyzer.IsBreak()) { if (analyzer.IsWhite()) { // Consume a space or a tab character. if (!hasLeadingBlanks) { whitespaces.Append(ReadCurrentCharacter()); } else { Skip(); } } else { // Check if it is a first line break. if (!hasLeadingBlanks) { whitespaces.Length = 0; leadingBreak.Append(ReadLine()); hasLeadingBlanks = true; } else { trailingBreaks.Append(ReadLine()); } } } // Join the whitespaces or fold line breaks. if (hasLeadingBlanks) { // Do we need to fold line breaks? if (StartsWith(leadingBreak, '\n')) { if (trailingBreaks.Length == 0) { value.Append(' '); } else { value.Append(trailingBreaks.ToString()); } } else { value.Append(leadingBreak.ToString()); value.Append(trailingBreaks.ToString()); } leadingBreak.Length = 0; trailingBreaks.Length = 0; } else { value.Append(whitespaces.ToString()); whitespaces.Length = 0; } } // Eat the right quote. Skip(); return new Scalar(value.ToString(), isSingleQuoted ? ScalarStyle.SingleQuoted : ScalarStyle.DoubleQuoted, start, cursor.Mark()); } /// /// Produce the SCALAR(...,plain) token. /// private void FetchPlainScalar() { // A plain scalar could be a simple key. SaveSimpleKey(); // A simple key cannot follow a flow scalar. simpleKeyAllowed = false; // Create the SCALAR token and append it to the queue. var isMultiline = false; var scalar = ScanPlainScalar(ref isMultiline); if (isMultiline && analyzer.Check(':') && flowLevel == 0 && indent < cursor.LineOffset) { tokens.Enqueue(new Error("While scanning a multiline plain scalar, found invalid mapping.", cursor.Mark(), cursor.Mark())); } tokens.Enqueue(scalar); } /// /// Scan a plain scalar. /// private Scalar ScanPlainScalar(ref bool isMultiline) { var value = new StringBuilder(); var whitespaces = new StringBuilder(); var leadingBreak = new StringBuilder(); var trailingBreaks = new StringBuilder(); var hasLeadingBlanks = false; var currentIndent = indent + 1; var start = cursor.Mark(); var end = start; var key = simpleKeys.Peek(); // Consume the content of the plain scalar. while (true) { // Check for a document indicator. if (IsDocumentIndicator()) { break; } // Check for a comment. if (analyzer.Check('#')) { if (indent < 0 && flowLevel == 0) { plainScalarFollowedByComment = true; } break; } var isAliasValue = analyzer.Check('*') && !(key.IsPossible && key.IsRequired); // Consume non-blank characters. while (!analyzer.IsWhiteBreakOrZero()) { // Check for indicators that may end a plain scalar. if (analyzer.Check(':') && !isAliasValue && (analyzer.IsWhiteBreakOrZero(1) || (flowLevel > 0 && analyzer.Check(',', 1))) || (flowLevel > 0 && analyzer.Check(",?[]{}"))) { if (flowLevel == 0 && !key.IsPossible) { tokens.Enqueue(new Error("While scanning a plain scalar value, found invalid mapping.", cursor.Mark(), cursor.Mark())); } break; } // Check if we need to join whitespaces and breaks. if (hasLeadingBlanks || whitespaces.Length > 0) { if (hasLeadingBlanks) { // Do we need to fold line breaks? if (StartsWith(leadingBreak, '\n')) { if (trailingBreaks.Length == 0) { value.Append(' '); } else { value.Append(trailingBreaks); } } else { value.Append(leadingBreak); value.Append(trailingBreaks); } leadingBreak.Length = 0; trailingBreaks.Length = 0; hasLeadingBlanks = false; } else { value.Append(whitespaces); whitespaces.Length = 0; } } if (flowLevel > 0 && cursor.LineOffset < currentIndent) { throw new Exception(); } // Copy the character. value.Append(ReadCurrentCharacter()); end = cursor.Mark(); } // Is it the end? if (!(analyzer.IsWhite() || analyzer.IsBreak())) { break; } // Consume blank characters. while (analyzer.IsWhite() || analyzer.IsBreak()) { if (analyzer.IsWhite()) { // Check for tab character that abuse indentation. if (hasLeadingBlanks && cursor.LineOffset < currentIndent && analyzer.IsTab()) { throw new SyntaxErrorException(start, cursor.Mark(), "While scanning a plain scalar, found a tab character that violate indentation."); } // Consume a space or a tab character. if (!hasLeadingBlanks) { whitespaces.Append(ReadCurrentCharacter()); } else { Skip(); } } else { isMultiline = true; // Check if it is a first line break. if (!hasLeadingBlanks) { whitespaces.Length = 0; leadingBreak.Append(ReadLine()); hasLeadingBlanks = true; } else { trailingBreaks.Append(ReadLine()); } } } // Check indentation level. if (flowLevel == 0 && cursor.LineOffset < currentIndent) { break; } } // Note that we change the 'simple_key_allowed' flag. if (hasLeadingBlanks) { simpleKeyAllowed = true; } // Create a token. return new Scalar(value.ToString(), ScalarStyle.Plain, start, end); } /// /// Remove a potential simple key at the current flow level. /// private void RemoveSimpleKey() { var key = simpleKeys.Peek(); if (key.IsPossible && key.IsRequired) { // If the key is required, it is an error. throw new SyntaxErrorException(key.Mark, key.Mark, "While scanning a simple key, could not find expected ':'."); } // Remove the key from the stack. key.MarkAsImpossible(); } /// /// Scan the directive name. /// /// Scope: /// %YAML 1.1 # a comment \n /// ^^^^ /// %TAG !yaml! tag:yaml.org,2002: \n /// ^^^ /// private string ScanDirectiveName(Mark start) { var name = new StringBuilder(); // Consume the directive name. while (analyzer.IsAlphaNumericDashOrUnderscore()) { name.Append(ReadCurrentCharacter()); } // Check if the name is empty. if (name.Length == 0) { throw new SyntaxErrorException(start, cursor.Mark(), "While scanning a directive, could not find expected directive name."); } // Check for an blank character after the name. if (!analyzer.IsWhiteBreakOrZero()) { throw new SyntaxErrorException(start, cursor.Mark(), "While scanning a directive, found unexpected non-alphabetical character."); } return name.ToString(); } private void SkipWhitespaces() { // Eat whitespaces. while (analyzer.IsWhite()) { Skip(); } } /// /// Scan the value of VERSION-DIRECTIVE. /// /// Scope: /// %YAML 1.1 # a comment \n /// ^^^^^^ /// private Token ScanVersionDirectiveValue(Mark start) { SkipWhitespaces(); // Consume the major version number. var major = ScanVersionDirectiveNumber(start); // Eat '.'. if (!analyzer.Check('.')) { throw new SyntaxErrorException(start, cursor.Mark(), "While scanning a %YAML directive, did not find expected digit or '.' character."); } Skip(); // Consume the minor version number. var minor = ScanVersionDirectiveNumber(start); return new VersionDirective(new Version(major, minor), start, start); } /// /// Scan the value of a TAG-DIRECTIVE token. /// /// Scope: /// %TAG !yaml! tag:yaml.org,2002: \n /// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ /// private Token ScanTagDirectiveValue(Mark start) { SkipWhitespaces(); // Scan a handle. var handle = ScanTagHandle(true, start); // Expect a whitespace. if (!analyzer.IsWhite()) { throw new SyntaxErrorException(start, cursor.Mark(), "While scanning a %TAG directive, did not find expected whitespace."); } SkipWhitespaces(); // Scan a prefix. var prefix = ScanTagUri(null, start); // Expect a whitespace or line break. if (!analyzer.IsWhiteBreakOrZero()) { throw new SyntaxErrorException(start, cursor.Mark(), "While scanning a %TAG directive, did not find expected whitespace or line break."); } return new TagDirective(handle, prefix, start, start); } /// /// Scan a tag. /// private string ScanTagUri(string? head, Mark start) { var tag = new StringBuilder(); if (head != null && head.Length > 1) { tag.Append(head.Substring(1)); } // Scan the tag. // The set of characters that may appear in URI is as follows: // '0'-'9', 'A'-'Z', 'a'-'z', '_', '-', ';', '/', '?', ':', '@', '&', // '=', '+', '$', ',', '.', '!', '~', '*', '\'', '(', ')', '[', ']', // '%'. while (analyzer.IsAlphaNumericDashOrUnderscore() || analyzer.Check(";/?:@&=+$.!~*'()[]%") || (analyzer.Check(',') && !analyzer.IsBreak(1))) { // Check if it is a URI-escape sequence. if (analyzer.Check('%')) { tag.Append(ScanUriEscapes(start)); } else if (analyzer.Check('+')) { tag.Append(' '); Skip(); } else { tag.Append(ReadCurrentCharacter()); } } // Check if the tag is non-empty. if (tag.Length == 0) { return string.Empty; } return tag.ToString(); } private static readonly byte[] EmptyBytes = new byte[0]; /// /// Decode an URI-escape sequence corresponding to a single UTF-8 character. /// private string ScanUriEscapes(Mark start) { // Decode the required number of characters. var charBytes = EmptyBytes; var nextInsertionIndex = 0; var width = 0; do { // Check for a URI-escaped octet. if (!(analyzer.Check('%') && analyzer.IsHex(1) && analyzer.IsHex(2))) { throw new SyntaxErrorException(start, cursor.Mark(), "While scanning a tag, did not find URI escaped octet."); } // Get the octet. var octet = (analyzer.AsHex(1) << 4) + analyzer.AsHex(2); // If it is the leading octet, determine the length of the UTF-8 sequence. if (width == 0) { width = (octet & 0x80) == 0x00 ? 1 : (octet & 0xE0) == 0xC0 ? 2 : (octet & 0xF0) == 0xE0 ? 3 : (octet & 0xF8) == 0xF0 ? 4 : 0; if (width == 0) { throw new SyntaxErrorException(start, cursor.Mark(), "While scanning a tag, found an incorrect leading UTF-8 octet."); } charBytes = new byte[width]; } else { // Check if the trailing octet is correct. if ((octet & 0xC0) != 0x80) { throw new SyntaxErrorException(start, cursor.Mark(), "While scanning a tag, found an incorrect trailing UTF-8 octet."); } } // Copy the octet and move the pointers. charBytes[nextInsertionIndex++] = (byte)octet; Skip(); Skip(); Skip(); } while (--width > 0); var result = Encoding.UTF8.GetString(charBytes, 0, nextInsertionIndex); if (result.Length == 0 || result.Length > 2) { throw new SyntaxErrorException(start, cursor.Mark(), "While scanning a tag, found an incorrect UTF-8 sequence."); } return result; } /// /// Scan a tag handle. /// private string ScanTagHandle(bool isDirective, Mark start) { // Check the initial '!' character. if (!analyzer.Check('!')) { throw new SyntaxErrorException(start, cursor.Mark(), "While scanning a tag, did not find expected '!'."); } // Copy the '!' character. var tagHandle = new StringBuilder(); tagHandle.Append(ReadCurrentCharacter()); // Copy all subsequent alphabetical and numerical characters. while (analyzer.IsAlphaNumericDashOrUnderscore()) { tagHandle.Append(ReadCurrentCharacter()); } // Check if the trailing character is '!' and copy it. if (analyzer.Check('!')) { tagHandle.Append(ReadCurrentCharacter()); } else { // It's either the '!' tag or not really a tag handle. If it's a %TAG // directive, it's an error. If it's a tag token, it must be a part of // URI. if (isDirective && (tagHandle.Length != 1 || tagHandle[0] != '!')) { throw new SyntaxErrorException(start, cursor.Mark(), "While scanning a tag directive, did not find expected '!'."); } } return tagHandle.ToString(); } /// /// Scan the version number of VERSION-DIRECTIVE. /// /// Scope: /// %YAML 1.1 # a comment \n /// ^ /// %YAML 1.1 # a comment \n /// ^ /// private int ScanVersionDirectiveNumber(Mark start) { var value = 0; var length = 0; // Repeat while the next character is digit. while (analyzer.IsDigit()) { // Check if the number is too long. if (++length > MaxVersionNumberLength) { throw new SyntaxErrorException(start, cursor.Mark(), "While scanning a %YAML directive, found extremely long version number."); } value = value * 10 + analyzer.AsDigit(); Skip(); } // Check if the number was present. if (length == 0) { throw new SyntaxErrorException(start, cursor.Mark(), "While scanning a %YAML directive, did not find expected version number."); } return value; } /// /// Check if a simple key may start at the current position and add it if /// needed. /// private void SaveSimpleKey() { // A simple key is required at the current position if the scanner is in // the block context and the current column coincides with the indentation // level. var isRequired = (flowLevel == 0 && indent == cursor.LineOffset); // A simple key is required only when it is the first token in the current // line. Therefore it is always allowed. But we add a check anyway. Debug.Assert(simpleKeyAllowed || !isRequired, "Can't require a simple key and disallow it at the same time."); // Impossible. // If the current position may start a simple key, save it. if (simpleKeyAllowed) { var key = new SimpleKey(isRequired, tokensParsed + tokens.Count, cursor); RemoveSimpleKey(); simpleKeys.Pop(); simpleKeys.Push(key); } } } }