// 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