private/runtime/Parser/Readers/SourceReader.cs

using System;
using System.Globalization;
using System.IO;

namespace Carbon.Json.Parser
{
    internal sealed class SourceReader : IDisposable
    {
        private readonly TextReader source;

        private char current;

        private readonly SourceLocation location = new SourceLocation();

        private bool isEof = false;
        
        public SourceReader(TextReader textReader)
        {
            this.source = textReader ?? throw new ArgumentNullException(nameof(textReader));
        }

        /// <summary>
        /// Advances to the next character
        /// </summary>
        public void Next()
        {
            // Advance to the new line when we see a new line '\n'.
            // A new line may be prefixed by a carriage return '\r'.

            if (current == '\n')
            {
                location.MarkNewLine();
            }

            int charCode = source.Read(); // -1 for end

            if (charCode >= 0)
            {
                current = (char)charCode;
            }
            else
            {
                // If we've already marked this as the EOF, throw an exception
                if (isEof)
                {
                    throw new EndOfStreamException("Cannot advance past end of stream.");
                }

                isEof = true;

                current = '\0';
            }

            location.Advance();
        }

        public void SkipWhitespace()
        {
            while (char.IsWhiteSpace(current))
            {
                Next();
            }
        }

        public char ReadEscapeCode()
        {
            Next();

            char escapedChar = current;

            Next(); // Consume escaped character

            switch (escapedChar)
            {
                // Special escape codes
                case '"': return '"'; // " (Quotation mark) U+0022
                case '/': return '/'; // / (Solidus) U+002F
                case '\\': return '\\'; // \ (Reverse solidus) U+005C

                // Control Characters
                case '0': return '\0'; // Nul (0) U+0000
                case 'a': return '\a'; // Alert (7)
                case 'b': return '\b'; // Backspace (8) U+0008
                case 'f': return '\f'; // Form feed (12) U+000C
                case 'n': return '\n'; // Line feed (10) U+000A
                case 'r': return '\r'; // Carriage return (13) U+000D
                case 't': return '\t'; // Horizontal tab (9) U+0009
                case 'v': return '\v'; // Vertical tab

                // Unicode escape sequence
                case 'u': return ReadUnicodeEscapeSequence(); // U+XXXX

                default: throw new Exception($"Unrecognized escape sequence '\\{escapedChar}'");
            }
        }

        private readonly char[] hexCode = new char[4];

        private char ReadUnicodeEscapeSequence()
        {
            hexCode[0] = current; Next();
            hexCode[1] = current; Next();
            hexCode[2] = current; Next();
            hexCode[3] = current; Next();

            return Convert.ToChar(int.Parse(
                s : new string(hexCode),
                style : NumberStyles.HexNumber,
                provider: NumberFormatInfo.InvariantInfo
            ));
        }

        public char Current => current;

        public bool IsEof => isEof;

        public char Peek() => (char)source.Peek();

        public SourceLocation Location => location;

        public void Dispose()
        {
            source.Dispose();
        }
    }
}