// Copyright (c) Tunnel Vision Laboratories, LLC. All Rights Reserved. // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. namespace LightJson.Serialization { using System.IO; using ErrorType = JsonParseException.ErrorType; /// /// Represents a text scanner that reads one character at a time. /// internal sealed class TextScanner { private TextReader reader; private TextPosition position; /// /// Initializes a new instance of the class. /// /// The TextReader to read the text. public TextScanner(TextReader reader) { this.reader = reader; } /// /// Gets the position of the scanner within the text. /// /// The position of the scanner within the text. public TextPosition Position { get { return this.position; } } /// /// Reads the next character in the stream without changing the current position. /// /// The next character in the stream. public char Peek() => (char)this.Peek(throwAtEndOfFile: true); /// /// Reads the next character in the stream without changing the current position. /// /// to throw an exception if the end of the file is /// reached; otherwise, . /// The next character in the stream, or -1 if the end of the file is reached with /// set to . public int Peek(bool throwAtEndOfFile) { var next = this.reader.Peek(); if (next == -1 && throwAtEndOfFile) { throw new JsonParseException( ErrorType.IncompleteMessage, this.position); } else { return next; } } /// /// Reads the next character in the stream, advancing the text position. /// /// The next character in the stream. public char Read() { var next = this.reader.Read(); if (next == -1) { throw new JsonParseException( ErrorType.IncompleteMessage, this.position); } else { if (next == '\n') { this.position.Line += 1; this.position.Column = 0; } else { this.position.Column += 1; } return (char)next; } } /// /// Advances the scanner to next non-whitespace character. /// public void SkipWhitespace() { while (true) { char next = this.Peek(); if (char.IsWhiteSpace(next)) { this.Read(); continue; } else if (next == '/') { this.SkipComment(); continue; } else { break; } } } /// /// Verifies that the given character matches the next character in the stream. /// If the characters do not match, an exception will be thrown. /// /// The expected character. public void Assert(char next) { var errorPosition = this.position; if (this.Read() != next) { throw new JsonParseException( string.Format("Parser expected '{0}'", next), ErrorType.InvalidOrUnexpectedCharacter, errorPosition); } } /// /// Verifies that the given string matches the next characters in the stream. /// If the strings do not match, an exception will be thrown. /// /// The expected string. public void Assert(string next) { for (var i = 0; i < next.Length; i += 1) { this.Assert(next[i]); } } private void SkipComment() { // First character is the first slash this.Read(); switch (this.Peek()) { case '/': this.SkipLineComment(); return; case '*': this.SkipBlockComment(); return; default: throw new JsonParseException( string.Format("Parser expected '{0}'", this.Peek()), ErrorType.InvalidOrUnexpectedCharacter, this.position); } } private void SkipLineComment() { // First character is the second '/' of the opening '//' this.Read(); while (true) { switch (this.reader.Peek()) { case '\n': // Reached the end of the line this.Read(); return; case -1: // Reached the end of the file return; default: this.Read(); continue; } } } private void SkipBlockComment() { // First character is the '*' of the opening '/*' this.Read(); bool foundStar = false; while (true) { switch (this.reader.Peek()) { case '*': this.Read(); foundStar = true; continue; case '/': this.Read(); if (foundStar) { return; } else { foundStar = false; continue; } case -1: // Reached the end of the file return; default: this.Read(); foundStar = false; continue; } } } } }