Code View

fson / source / fson-1.1.0.0 / libs / internal / sdk / fson / FsonParser.hh
// SPDX-License-Identifier: MIT
#pragma once

#include <memory>
#include <optional>
#include <string>
#include <vector>

#include "cparse/Parser.hh"

#include "Comment.hh"
#include "Document.hh"

namespace fedem
{
  namespace fson
  {
    class Array;
    class Include;
    class Object;
    class Value;

    // ─────────────────────────────────────────────────────────────────────
    // FsonParser
    //
    // Recursive-descent FSON parser built on fedem::parser::Parser,
    // implementing the grammar in docs/notation (00005-fson_file et al).
    //
    // Error strategy: collect-and-continue. Errors are recorded in the
    // inherited error collector (errors()/hasErrors()) and parsing resumes
    // at the next ',' / '}' / ']' synchronisation point. A document is
    // produced even for faulty input; callers MUST check hasErrors()
    // before trusting or re-writing it.
    //
    // Semantics implemented per project decisions:
    //   • duplicate ACTIVE member names are an error (ParseError::Kind::
    //     Faulty); the duplicate member is still stored structurally so no
    //     source information is lost
    //   • \uXXXX escapes are decoded to UTF-8 (surrogate pairs combined;
    //     invalid sequences are reported and replaced with U+FFFD)
    //   • "--" disables a member (quoted or bare key alike)
    //
    // JSON5-superset grammar (v0.9.0.0, decision A.2a and the JSON5
    // acceptance list): the document root may be any value, not just an
    // object; strings may be single- or double-quoted, with line
    // continuations and \xNN/\v/\0 escapes (any other character after
    // '\' is itself, a deliberate simplification vs. strict JSON5); a
    // trailing comma is allowed in object and array bodies; numbers admit
    // a leading '+', "0x"/"0X" hex literals, a leading or trailing '.',
    // and the literal tokens Infinity/-Infinity/NaN; and whitespace
    // additionally recognises NBSP, BOM, U+2028/2029, and the other
    // Unicode Zs space separators. Nested block comments remain supported
    // (the one deliberate exception to the JSON5-superset claim — JSON5
    // itself forbids nesting).
    //
    // Comment attachment: leading comments before a member/element,
    // trailing end-of-line comment on the same line as the value end,
    // dangling comments before '}' / ']', document header/footer.
    // ─────────────────────────────────────────────────────────────────────
    class FsonParser final : public parser::Parser
    {
      public:
        FsonParser( );
        ~FsonParser( ) override;

        std::string getGrammarName( ) const noexcept override;

        // Parses the given file. Returns nullptr only when the file could
        // not be opened; otherwise returns the (possibly partial) document.
        // Check hasErrors() afterwards.
        std::unique_ptr< Document > parseFile( std::string const& filename );

      protected:
        bool start( ) override;
        bool skipComments( ) override;
        bool skipWhiteSpaces( ) override;

      private:
        struct PendingComment
        {
          Comment            comment;
          unsigned long int  line;
        };

        std::unique_ptr< Document >    document;
        std::vector< PendingComment >  pending;

        // ── trivia management ───────────────────────────────────────────
        CommentList takeAllPending( );
        std::optional< Comment > takePendingOnLine( unsigned long int line );
        void restorePending( CommentList comments );

        // ── grammar rules ───────────────────────────────────────────────
        bool parseObjectBody( Object& object );
        bool parseArrayBody ( Array&  array  );
        bool parseValue     ( std::unique_ptr< Value >& result );
        bool parseString    ( std::string& result );

        // Parses a `|`-block multi-line string (00040-string.rrd option 3).
        // Precondition: the next character is '|'. Decodes into result.
        bool parseMultilineString( std::string& result );

        // Continuation lookahead for `|`-blocks: skips spaces/tabs and any
        // /* */ block comments on the current physical line only.
        void skipInlineBlanksAndBlockComments( );
        bool parseName      ( std::string& result );
        bool parseNumber    ( std::unique_ptr< Value >& result );
        bool parseLiteralTail( std::string const& literal );

        // Returns the appended member, or nullptr when the member could
        // not be formed; memberLine receives the line of the value end.
        Member* parseMember( Object& object, unsigned long int& memberLine );

        // Parses a "%include" directive (the caller has confirmed a '%'
        // lookahead). Returns the appended Include, or nullptr on error;
        // entryLine receives the line of the directive end for trailing-
        // comment attachment. Consumes any leading "--" disabled prefix.
        Include* parseInclude( Object& object, unsigned long int& entryLine );

        // Non-consuming lookahead: true when the next entry is a "%include"
        // directive, i.e. a '%' optionally preceded by a "--" disabled prefix
        // (and interleaving trivia). Restores the cursor before returning.
        bool looksLikeInclude( );

        // ── string helpers ──────────────────────────────────────────────
        bool readHexQuad( unsigned long int& codeUnit );
        void decodeUnicodeEscape( std::string& result );
        bool readHexByte( unsigned long int& byteValue );
        void decodeHexByteEscape( std::string& result );
        static void appendUtf8( std::string& result, unsigned long int codePoint );

        // Panic-mode recovery: consumes input until the next ',' / '}' /
        // ']' at the current nesting depth (or EOF), skipping strings and
        // comments. The synchronisation character itself is not consumed.
        void recover( );
    };
  }  // end namespace fson
}  // end namespace fedem