Code View

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

#include <iosfwd>
#include <string>
#include <string_view>

#include "cparse/Indentation.hh"

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

namespace fedem
{
  namespace fson
  {
    class Array;
    class Include;
    class Key;
    class Number;
    class Object;
    class String;
    class Value;

    // ─────────────────────────────────────────────────────────────────────
    // FsonWriter
    //
    // Pretty-printing serializer for FSON documents with three dialects:
    //
    // Dialect::Fson (default) — output is normalized whitespace-wise but
    // loses NO information: comments (header, leading, trailing,
    // dangling, footer), member order, key form (quoted/bare), "--"
    // disabled members and number lexemes (including hex / Infinity /
    // -Infinity / NaN, v0.9.0.0) are all written back.
    //
    // Dialect::Json — fully JSON-compliant output: comments are omitted,
    // every key is quoted, hex number lexemes are rewritten as plain
    // decimal, and Infinity/-Infinity/NaN cannot be represented — writing
    // one throws std::domain_error. Disabled members follow
    // getDisabledMemberPolicy() (see below); since strict JSON cannot
    // carry comments, DisabledMemberPolicy::ConvertToComment behaves the
    // same as Drop in this dialect.
    //
    // Dialect::Json5 (v0.9.0.0) — JSON5-compliant output: unlike Json,
    // comments ARE preserved (header, leading, trailing, dangling,
    // footer), bare keys are written bare (same rule as Fson), and
    // numbers keep their original lexeme verbatim — hex stays hex,
    // Infinity/-Infinity/NaN are written as their literal tokens, since
    // JSON5 natively supports all of this. Disabled members also follow
    // getDisabledMemberPolicy(). Any multi-line block-comment text
    // written in this dialect — original or synthesized — is flattened
    // into one "//"-prefixed line per source line, since JSON5 forbids
    // nested block comments (the one place FSON and JSON5 diverge; see
    // the class-level superset note in docs/notation.md).
    //
    // DisabledMemberPolicy governs "--key: value" members in the Json and
    // Json5 dialects (the Fson dialect always shows them literally,
    // unaffected by this setting):
    //   • ConvertToComment (default) — Json5 only: synthesize a comment
    //     rendering "key: value" (split across multiple "//" lines for a
    //     container value); Json: emit nothing, since strict JSON cannot
    //     carry comments.
    //   • Keep — emit as an ordinary member without the "--" prefix (note:
    //     this may produce duplicate keys when a disabled member shadows
    //     an active one).
    //   • Drop — emit nothing.
    //
    // Format (project decisions, all dialects unless noted):
    //   • 2-space indentation per level (parser::Indentation default)
    //   • "key: value" with one space after ':'; one member/element per line
    //   • empty containers are compact: "{}" / "[]" — in Fson and Json5,
    //     dangling comments or a converted-to-comment member force the
    //     expanded form (they would otherwise be lost); in Json, a
    //     container with nothing to emit is always compact
    //   • disabled members are written adjacent in Fson: "--key: value"
    //   • strings escape only the mandatory characters ('"', '\\' and
    //     control characters); all other bytes pass through as raw UTF-8
    //     (valid in all three dialects, including raw U+2028/U+2029)
    //   • the file ends with a single newline
    //
    // In the Fson and Json5 dialects, prefer-bare-keys rewrites quoted
    // keys without quotes whenever the name satisfies the bare-name
    // grammar; Json always quotes every key.
    //
    // The document root may be any value (v0.9.0.0); write() dispatches
    // on its actual type rather than assuming an object.
    //
    // The writer is stateless between write() calls and may be reused.
    // ─────────────────────────────────────────────────────────────────────
    class FsonWriter final
    {
      public:
        enum class Dialect
        {
          Fson,
          Json,
          Json5
        };

        // Governs "--key: value" disabled members in the Json/Json5
        // dialects; see the class comment above.
        enum class DisabledMemberPolicy
        {
          ConvertToComment,
          Keep,
          Drop
        };

        FsonWriter( );

        // Output dialect; default Dialect::Fson.
        void setDialect( Dialect dialect ) noexcept;
        Dialect getDialect( ) const noexcept;

        // Json/Json5 dialects only; default ConvertToComment.
        void setDisabledMemberPolicy( DisabledMemberPolicy policy ) noexcept;
        DisabledMemberPolicy getDisabledMemberPolicy( ) const noexcept;

        // Deprecated compatibility shims over DisabledMemberPolicy:
        // setKeepDisabledMembers( true )  == setDisabledMemberPolicy( Keep )
        // setKeepDisabledMembers( false ) == setDisabledMemberPolicy( ConvertToComment )
        // getKeepDisabledMembers() reports policy == Keep.
        void setKeepDisabledMembers( bool keep ) noexcept;
        bool getKeepDisabledMembers( ) const noexcept;

        // Fson and Json5 dialects only: write quoted keys without quotes
        // whenever the name satisfies the bare-name grammar.
        void setPreferBareKeys( bool prefer ) noexcept;
        bool getPreferBareKeys( ) const noexcept;

        // Serializes the document into the stream.
        void write( Document const& document, std::ostream& stream );

        // Serializes the document into a string.
        std::string write( Document const& document );

        // Writes the document to a file. Returns false on I/O failure.
        bool writeFile( Document const& document, std::string const& filename );

        // Escapes string content per the FSON string grammar (mandatory
        // characters only); does not add the surrounding quotes. The
        // result is also valid JSON/JSON5 string content.
        static std::string encodeString( std::string const& text );

      private:
        parser::Indentation  indentor;
        Dialect               dialect;
        DisabledMemberPolicy  disabledPolicy;
        bool                  preferBare;

        // Whether a member is written as an ordinary "key: value" entry
        // (with its own comma); see the DisabledMemberPolicy comment.
        bool isOrdinaryMember( Member const& member ) const noexcept;

        // Whether a member is synthesized into a comment instead (Json5 +
        // ConvertToComment only — Json's ConvertToComment drops silently).
        bool isConvertedToComment( Member const& member ) const noexcept;

        // Whether an %include directive is written out (Fson dialect only;
        // disabled directives only under DisabledMemberPolicy::Keep).
        bool isEmittedInclude( Include const& include ) const noexcept;

        // Whether a String is written as a `|`-block (Fson dialect, Multiline
        // form, content ending in '\n'); otherwise it is quoted.
        bool canWriteMultiline( String const& string ) const noexcept;
        void writeMultilineString( String const& string, std::ostream& stream );
        static std::string encodeMultilineLine( std::string_view line );

        void writeValue  ( Value  const& value,  std::ostream& stream );
        void writeObject ( Object const& object, std::ostream& stream );
        void writeArray  ( Array  const& array,  std::ostream& stream );
        void writeKey    ( Key    const& key,    std::ostream& stream );
        void writeNumber ( Number const& number, std::ostream& stream );

        // Renders a disabled member's "key: value" into a synthesized
        // comment (one "//" line for a scalar, one per line for a
        // container), at the current indentation.
        void writeConvertedMember( Member const& member, std::ostream& stream );

        // Emits the bare comment ("//text" or "/*text*/"), no indentation,
        // no line break. In the Json5 dialect a MultiLine comment is
        // flattened into one "//"-prefixed line per source line instead
        // (JSON5 forbids nested block comments).
        void writeComment( Comment const& comment, std::ostream& stream );

        // Emits each comment on its own indented line. No-op in the Json
        // dialect (which cannot carry comments at all).
        void writeCommentLines( CommentList const& comments, std::ostream& stream );

        // Emits a member's same-line trailing comment, if any: inline for
        // Fson and for a SingleLine comment in Json5; for a MultiLine
        // comment in Json5 (which cannot fit inline), the line ends here
        // and the flattened comment follows as standalone indented lines.
        void writeTrailingComment( Comment const* comment, std::ostream& stream );
    };
  }  // end namespace fson
}  // end namespace fedem