YAMLGenerator.java

package tools.jackson.dataformat.yaml;

import java.io.*;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.regex.Pattern;

import tools.jackson.core.*;

import tools.jackson.core.base.GeneratorBase;
import tools.jackson.core.io.IOContext;
import tools.jackson.core.io.NumberOutput;
import tools.jackson.core.json.DupDetector;
import tools.jackson.core.util.JacksonFeatureSet;
import tools.jackson.core.util.SimpleStreamWriteContext;

import tools.jackson.dataformat.yaml.util.StringQuotingChecker;

import org.snakeyaml.engine.v2.api.DumpSettings;
import org.snakeyaml.engine.v2.api.DumpSettingsBuilder;
import org.snakeyaml.engine.v2.comments.CommentType;
import org.snakeyaml.engine.v2.common.Anchor;
import org.snakeyaml.engine.v2.common.FlowStyle;
import org.snakeyaml.engine.v2.common.ScalarStyle;
import org.snakeyaml.engine.v2.common.SpecVersion;
import org.snakeyaml.engine.v2.emitter.Emitter;
import org.snakeyaml.engine.v2.events.*;
import org.snakeyaml.engine.v2.exceptions.YamlEngineException;
import org.snakeyaml.engine.v2.nodes.Tag;
import org.snakeyaml.engine.v2.resolver.JsonScalarResolver;

public class YAMLGenerator extends GeneratorBase
{
    /*
    /**********************************************************************
    /* Internal constants
    /**********************************************************************
     */

    protected final static long MIN_INT_AS_LONG = (long) Integer.MIN_VALUE;
    protected final static long MAX_INT_AS_LONG = (long) Integer.MAX_VALUE;

    protected final static Pattern PLAIN_NUMBER_P = Pattern.compile("[+-]?[0-9]*(\\.[0-9]*)?");

    // @since 3.2
    protected final static Pattern NEWLINE_SPLITTER_P = Pattern.compile("\r\n|\r|\n");

    protected final static String TAG_BINARY = Tag.BINARY.toString();

    // for property names, leave out quotes
    private final static ScalarStyle STYLE_UNQUOTED_NAME = ScalarStyle.PLAIN;

    // numbers, booleans, should use implicit
    private final static ScalarStyle STYLE_SCALAR = ScalarStyle.PLAIN;
    // Strings quoted for fun
    private final static ScalarStyle STYLE_QUOTED = ScalarStyle.DOUBLE_QUOTED;
    // Strings in literal (block) style
    private final static ScalarStyle STYLE_LITERAL = ScalarStyle.LITERAL;

    // Which flow style to use for Base64? Maybe basic quoted?
    // 29-Nov-2017, tatu: Actually SnakeYAML uses block style so:
    private final static ScalarStyle STYLE_BASE64 = STYLE_LITERAL;

    private final static ScalarStyle STYLE_PLAIN = ScalarStyle.PLAIN;

    /*
    /**********************************************************************
    /* Configuration
    /**********************************************************************
     */

    /**
     * Bit flag composed of bits that indicate which {@link YAMLWriteFeature}s
     * are enabled.
     */
    protected int _formatWriteFeatures;

    protected Writer _writer;

    /**
     * Caller-provided {@link OutputStream} that {@link #_writer} was constructed (by us)
     * to wrap, if any; {@code null} if {@link #_writer} was handed to us by the caller.
     *<p>
     * A {@link Writer} we construct must always be flushed and closed: that is what
     * hands its pending content over and returns its buffers to the recycler. It is
     * shielded from this stream (see {@link tools.jackson.core.io.GuardedOutputStream}), which is instead
     * flushed and closed directly, as per {@link StreamWriteFeature#FLUSH_PASSED_TO_STREAM}
     * and {@link StreamWriteFeature#AUTO_CLOSE_TARGET}.
     *
     * @since 3.3
     */
    protected final OutputStream _target;

    protected DumpSettings _outputOptions;

    protected final boolean _cfgMinimizeQuotes;

    protected final SpecVersion _docVersion;

    /*
    /**********************************************************************
    /* Output state
    /**********************************************************************
     */

    protected SimpleStreamWriteContext _streamWriteContext;

    protected Emitter _emitter;

    /**
     * YAML supports native Object identifiers, so databinder may indicate
     * need to output one.
     */
    protected String _objectId;

    /**
     * YAML supports native Type identifiers, so databinder may indicate
     * need to output one.
     */
    protected String _typeId;

    protected int _rootValueCount;

    protected final StringQuotingChecker _quotingChecker;

    /*
    /**********************************************************************
    /* Life-cycle
    /**********************************************************************
     */

    public YAMLGenerator(ObjectWriteContext writeContext, IOContext ioCtxt,
            int streamWriteFeatures, int yamlFeatures,
            StringQuotingChecker quotingChecker,
            Writer out, SpecVersion version,
            DumpSettings dumpOptions)
    {
        this(writeContext, ioCtxt, streamWriteFeatures, yamlFeatures, quotingChecker,
                out, null, version, dumpOptions);
    }

    /**
     * @param out Writer to write to: either provided by the caller, or constructed
     *    by Jackson to wrap {@code target}
     * @param target Caller-provided stream that {@code out} wraps, if {@code out} was
     *    constructed by Jackson (and hence must always be flushed and closed);
     *    {@code null} if {@code out} was provided by the caller
     *
     * @since 3.3
     */
    public YAMLGenerator(ObjectWriteContext writeContext, IOContext ioCtxt,
            int streamWriteFeatures, int yamlFeatures,
            StringQuotingChecker quotingChecker,
            Writer out, OutputStream target, SpecVersion version,
            DumpSettings dumpOptions)
    {
        super(writeContext, ioCtxt, streamWriteFeatures);
        _target = target;
        final DupDetector dups = StreamWriteFeature.STRICT_DUPLICATE_DETECTION.enabledIn(streamWriteFeatures)
                ? DupDetector.rootDetector(this) : null;
        _streamWriteContext = SimpleStreamWriteContext.createRootContext(dups);

        _formatWriteFeatures = yamlFeatures;
        _cfgMinimizeQuotes = YAMLWriteFeature.MINIMIZE_QUOTES.enabledIn(_formatWriteFeatures);
        _quotingChecker = quotingChecker;
        _writer = out;
        _docVersion = version;

        if (dumpOptions == null) {
            dumpOptions = buildDumperOptions(streamWriteFeatures, yamlFeatures, version);
        }
        _outputOptions = dumpOptions;

        _emitter = new Emitter(_outputOptions, new WriterWrapper(_writer));
        // should we start output now, or try to defer?
        _emit(new StreamStartEvent());
        _emitStartDocument();
    }

    protected DumpSettings buildDumperOptions(int streamWriteFeatures, int yamlFeatures,
            SpecVersion version)
    {
        DumpSettingsBuilder opt = DumpSettings.builder();
        // would we want canonical?
        if (YAMLWriteFeature.CANONICAL_OUTPUT.enabledIn(_formatWriteFeatures)) {
            opt.setCanonical(true);
        } else {
            opt.setCanonical(false);
            // if not, MUST specify flow styles
            opt.setDefaultFlowStyle(FlowStyle.BLOCK);
        }
        // split-lines for text blocks?
        opt.setSplitLines(YAMLWriteFeature.SPLIT_LINES.enabledIn(_formatWriteFeatures));
        // array indentation?
        if (YAMLWriteFeature.INDENT_ARRAYS.enabledIn(_formatWriteFeatures)) {
            // But, wrt [dataformats-text#34]: need to set both to diff values to work around bug
            // (otherwise indentation level is "invisible". Note that this should NOT be necessary
            // but is needed up to at least SnakeYAML 1.18.
            // Also looks like all kinds of values do work, except for both being 2... weird.
            opt.setIndicatorIndent(1);
            opt.setIndent(2);
        }
        // [dataformats-text#175]: further configurability that overrides prev setting
        if (YAMLWriteFeature.INDENT_ARRAYS_WITH_INDICATOR.enabledIn(_formatWriteFeatures)) {
            opt.setIndicatorIndent(2);
            opt.setIndentWithIndicator(true);
        }
        if (YAMLWriteFeature.ALLOW_LONG_KEYS.enabledIn(_formatWriteFeatures)) {
            opt.setMaxSimpleKeyLength(1024);
        }

        // 03-Oct-2020, tatu: Specify spec version; however, does not seem to make
        //   any difference?
        opt.setYamlDirective(Optional.ofNullable(version));
        // Enable comment output support (needed for writeComment())
        opt.setDumpComments(true);
        return opt.build();
    }

    /*
    /**********************************************************************
    /* Versioned
    /**********************************************************************
     */

    @Override
    public Version version() {
        return PackageVersion.VERSION;
    }

    /*
    /**********************************************************************
    /* Overrides: capability introspection
    /**********************************************************************
     */
    
    @Override // @since 3.2
    public boolean canWriteComments() {
        // 06-Apr-2026, tatu: as per [dataformats-text#36]:
        return true;
    }

    @Override
    public boolean canWriteObjectId() {
        // yes, YAML does support Native Type Ids!
        // 10-Sep-2014, tatu: Except as per [#23] might not want to...
        return YAMLWriteFeature.USE_NATIVE_OBJECT_ID.enabledIn(_formatWriteFeatures);
    }

    @Override
    public boolean canWriteTypeId() {
        // yes, YAML does support Native Type Ids!
        // 10-Sep-2014, tatu: Except as per [#22] might not want to...
        return YAMLWriteFeature.USE_NATIVE_TYPE_ID.enabledIn(_formatWriteFeatures);
    }

    @Override
    public boolean has(StreamWriteCapability capability) {
        return DEFAULT_TEXTUAL_WRITE_CAPABILITIES.isEnabled(capability);
    }

    @Override
    public JacksonFeatureSet<StreamWriteCapability> streamWriteCapabilities() {
        return DEFAULT_TEXTUAL_WRITE_CAPABILITIES;
    }

    /*
    /**********************************************************************
    /* Overridden output state handling methods
    /**********************************************************************
     */

    @Override
    public final TokenStreamContext streamWriteContext() { return _streamWriteContext; }

    @Override
    public final Object currentValue() {
        return _streamWriteContext.currentValue();
    }

    @Override
    public final void assignCurrentValue(Object v) {
        _streamWriteContext.assignCurrentValue(v);
    }

    /*
    /**********************************************************************
    /* Overridden methods, configuration
    /**********************************************************************
     */

    @Override
    public Object streamWriteOutputTarget() {
        // [dataformats-text#749]: Writer we constructed ourselves is an implementation
        //   detail, shielded from caller's stream: so expose the stream instead
        return (_target != null) ? _target : _writer;
    }

    /**
     * SnakeYAML does not expose buffered content amount, so we can only return
     * <code>-1</code> from here
     */
    @Override
    public int streamWriteOutputBuffered() {
        return -1;
    }

    /*
    /**********************************************************************
    /* Overrides: config access
    /**********************************************************************
     */

    @Override
    public PrettyPrinter getPrettyPrinter() {
        return null;
    }

    /*
    /**********************************************************************
    /* Extended API, configuration
    /**********************************************************************
     */

    public final boolean isEnabled(YAMLWriteFeature f) {
        return (_formatWriteFeatures & f.getMask()) != 0;
    }

    /*
    /**********************************************************************
    /* Overridden methods; writing property names
    /**********************************************************************
     */

    // And then methods overridden to make final, streamline some aspects...

    @Override
    public JsonGenerator writeName(String name) throws JacksonException
    {
        if (!_streamWriteContext.writeName(name)) {
            _reportError("Cannot write a property name, expecting a value");
        }
        _writeFieldName(name);
        return this;
    }

    @Override
    public JsonGenerator writeName(SerializableString name)
        throws JacksonException
    {
        // Object is a value, need to verify it's allowed
        if (!_streamWriteContext.writeName(name.getValue())) {
            _reportError("Cannotwrite a property name, expecting a value");
        }
        _writeFieldName(name.getValue());
        return this;
    }

    @Override
    public JsonGenerator writePropertyId(long id) throws JacksonException {
        // 24-Jul-2019, tatu: Should not force construction of a String here...
        String idStr = Long.toString(id);
        if (!_streamWriteContext.writeName(idStr)) {
            _reportError("Cannot write a property id, expecting a value");
        }
        // to avoid quoting
//        _writeFieldName(idStr);
        _writeScalar(idStr, "int", STYLE_SCALAR);
        return this;
    }

    private final void _writeFieldName(String name) throws JacksonException
    {
        _writeScalar(name, "string",
                _quotingChecker.needToQuoteName(name) ? STYLE_QUOTED : STYLE_UNQUOTED_NAME);
    }

    /*
    /**********************************************************************
    /* Public API: low-level I/O
    /**********************************************************************
     */

    @Override
    public final void flush()
    {
        // [dataformats-text#749]: nothing to flush once closed -- and target may
        //   have been closed along with us
        if (isClosed()) {
            return;
        }
        try {
            if (_target != null) {
                // [dataformats-text#749]: a Writer we constructed ourselves is just a
                //   buffer, so must be flushed regardless (caller's stream is shielded)
                _writer.flush();
            }
            if (isEnabled(StreamWriteFeature.FLUSH_PASSED_TO_STREAM)) {
                final Flushable target = (_target != null) ? _target : _writer;
                target.flush();
            }
        } catch (IOException e) {
            throw _wrapIOFailure(e);
        }
    }

    @Override
    public void close()
    {
        if (!isClosed()) {
            // 11-Dec-2019, tatu: Should perhaps check if content is to be auto-closed...
            //   but need END_DOCUMENT regardless
            try {
                _emitEndDocument();
                _emit(new StreamEndEvent());
            } catch (Throwable t) {
                // [dataformats-text#752]: must close regardless, but without masking
                //   the original failure
                try {
                    super.close();
                } catch (Throwable t2) {
                    t.addSuppressed(t2);
                }
                throw t;
            }
            super.close();
        }
    }

    @Override
    protected void _closeInput() throws IOException
    {
        if (_writer == null) {
            return;
        }
        final boolean closeTarget = _ioContext.isResourceManaged()
                || isEnabled(StreamWriteFeature.AUTO_CLOSE_TARGET);
        if (_target == null) {
            // 25-Nov-2008, tatus: As per [JACKSON-16] we are not to call close()
            //   on the underlying Writer, unless we "own" it, or auto-closing
            //   feature is enabled.
            if (closeTarget) {
                _writer.close();
            } else if (isEnabled(StreamWriteFeature.FLUSH_PASSED_TO_STREAM)) {
                // If we can't close it, we should at least flush
                _writer.flush();
            }
            return;
        }
        // [dataformats-text#749]: a Writer we constructed ourselves must be closed
        //   regardless: that hands its buffered content over to the caller's
        //   OutputStream and recycles its buffer. Caller's stream is shielded from
        //   that, and closed (or flushed) as per features enabled now --
        //   [dataformats-text#752] even if closing our Writer fails: try-with-resources
        //   ensures that, and reports the first failure (with any later one added
        //   as suppressed)
        final Closeable target = closeTarget ? _target
                : (isEnabled(StreamWriteFeature.FLUSH_PASSED_TO_STREAM) ? _target::flush : null);
        try (target) {
            _writer.close();
        }
    }

    /*
    /**********************************************************************
    /* Public API: structural output
    /**********************************************************************
     */

    @Override
    public JsonGenerator writeStartArray() throws JacksonException
    {
        _verifyValueWrite("start an array");
        _streamWriteContext = _streamWriteContext.createChildArrayContext(null);
        _streamWriteConstraints.validateNestingDepth(_streamWriteContext.getNestingDepth());
        FlowStyle style = _outputOptions.getDefaultFlowStyle();
        String yamlTag = _typeId;
        boolean implicit = (yamlTag == null);
        Optional<Anchor> anchor = Optional.ofNullable(_objectId).map(Anchor::new);
        _objectId = null;
        _emit(new SequenceStartEvent(anchor, Optional.ofNullable(yamlTag),
                implicit,  style));
        return this;
    }

    @Override
    public JsonGenerator writeStartArray(Object currValue) throws JacksonException {
        writeStartArray();
        assignCurrentValue(currValue);
        return this;
    }

    @Override
    public JsonGenerator writeEndArray() throws JacksonException
    {
        if (!_streamWriteContext.inArray()) {
            _reportError("Current context not Array but "+_streamWriteContext.typeDesc());
        }
        // just to make sure we don't "leak" type ids
        _typeId = null;
        _streamWriteContext = _streamWriteContext.getParent();
        _emit(new SequenceEndEvent());
        return this;
    }

    @Override
    public JsonGenerator writeStartObject() throws JacksonException
    {
        _verifyValueWrite("start an object");
        _streamWriteContext = _streamWriteContext.createChildObjectContext(null);
        _streamWriteConstraints.validateNestingDepth(_streamWriteContext.getNestingDepth());
        FlowStyle style = _outputOptions.getDefaultFlowStyle();
        String yamlTag = _typeId;
        boolean implicit = (yamlTag == null);
        Optional<Anchor> anchor = Optional.ofNullable(_objectId).map(Anchor::new);
        _objectId = null;
        _emit(new MappingStartEvent(anchor, Optional.ofNullable(yamlTag), implicit,  style));
        return this;
    }

    @Override
    public JsonGenerator writeStartObject(Object currValue) throws JacksonException {
        writeStartObject();
        assignCurrentValue(currValue);
        return this;
    }

    @Override
    public JsonGenerator writeEndObject() throws JacksonException
    {
        if (!_streamWriteContext.inObject()) {
            _reportError("Current context not Object but "+_streamWriteContext.typeDesc());
        }
        // just to make sure we don't "leak" type ids
        _typeId = null;
        _streamWriteContext = _streamWriteContext.getParent();
        _emit(new MappingEndEvent());
        return this;
    }

    /*
    /**********************************************************************
    /* Output method implementations, textual
    /**********************************************************************
     */

    @Override
    public JsonGenerator writeString(String text) throws JacksonException
    {
        if (text == null) {
            return writeNull();
        }
        _verifyValueWrite("write String value");

        // [dataformats-text#50]: Empty String always quoted
        if (text.isEmpty()) {
            _writeScalar(text, "string", STYLE_QUOTED);
            return this;
        }

        ScalarStyle style;
        if (_cfgMinimizeQuotes) {
            if (text.indexOf('\n') >= 0) {
                style = STYLE_LITERAL;
            // If one of reserved values ("true", "null"), or, number, preserve quoting:
            } else if (_quotingChecker.needToQuoteValue(text)
                || (YAMLWriteFeature.ALWAYS_QUOTE_NUMBERS_AS_STRINGS.enabledIn(_formatWriteFeatures)
                        && _looksLikeYAMLNumber(text))
                ) {
                style = STYLE_QUOTED;
            } else {
                style = STYLE_PLAIN;
            }
        } else {
            if (YAMLWriteFeature.LITERAL_BLOCK_STYLE.enabledIn(_formatWriteFeatures)
                    && text.indexOf('\n') >= 0) {
                style = STYLE_LITERAL;
            } else {
                style = STYLE_QUOTED;
            }
        }
        _writeScalar(text, "string", style);
        return this;
    }

    @Override
    public JsonGenerator writeString(char[] text, int offset, int len) throws JacksonException
    {
        return writeString(new String(text, offset, len));
    }

    /**
     * Checks whether given String value would be re-read as a YAML number if emitted
     * unquoted; used by {@link YAMLWriteFeature#ALWAYS_QUOTE_NUMBERS_AS_STRINGS}.
     * Combines the historical {@link #PLAIN_NUMBER_P} check (retained for backwards
     * compatibility) with the implicit resolver patterns
     * ({@link JsonScalarResolver#INT}, {@link JsonScalarResolver#FLOAT}) that
     * {@link YAMLParser} uses to resolve plain scalars back to numbers, so that
     * exponent ({@code 1e5}) and non-finite ({@code .inf}, {@code .nan}) forms, which
     * {@link #PLAIN_NUMBER_P} does not match, are quoted too and thus round-trip.
     * See [dataformats-text#701].
     *
     * @since 3.1.6
     */
    protected boolean _looksLikeYAMLNumber(String text) {
        // 23-Jul-2026, tatu: Regexps are relatively costly so avoid them for the
        //    common case of "regular" text: all forms matched below have to start
        //    with one of following characters
        if (text.isEmpty()) {
            return false;
        }
        switch (text.charAt(0)) {
        case '+': case '-': case '.':
        case '0': case '1': case '2': case '3': case '4':
        case '5': case '6': case '7': case '8': case '9':
            break;
        default:
            return false;
        }
        // NOTE: unlike YAML 1.1 (used by 2.x), the YAML 1.2 JSON schema resolves
        //   hex ("0x1F"), binary ("0b101"), underscore ("12_34") and 60-base ("1:30")
        //   forms as Strings already -- so, unlike 2.x, no exclusions needed here
        return PLAIN_NUMBER_P.matcher(text).matches()
                || JsonScalarResolver.INT.matcher(text).matches()
                || JsonScalarResolver.FLOAT.matcher(text).matches();
    }

    @Override
    public JsonGenerator writeString(SerializableString sstr)
        throws JacksonException
    {
        return writeString(sstr.toString());
    }

    @Override
    public JsonGenerator writeRawUTF8String(byte[] text, int offset, int len)
        throws JacksonException
    {
        return _reportUnsupportedOperation();
    }

    @Override
    public JsonGenerator writeUTF8String(byte[] text, int offset, int len)
        throws JacksonException
    {
        return writeString(new String(text, offset, len, StandardCharsets.UTF_8));
    }

    /*
    /**********************************************************************
    /* Output method implementations, unprocessed ("raw")
    /**********************************************************************
     */

    @Override
    public JsonGenerator writeRaw(String text) throws JacksonException {
        return _reportUnsupportedOperation();
    }

    @Override
    public JsonGenerator writeRaw(String text, int offset, int len) throws JacksonException {
        return _reportUnsupportedOperation();
    }

    @Override
    public JsonGenerator writeRaw(char[] text, int offset, int len) throws JacksonException {
        return _reportUnsupportedOperation();
    }

    @Override
    public JsonGenerator writeRaw(char c) throws JacksonException {
        return _reportUnsupportedOperation();
    }

    @Override
    public JsonGenerator writeRawValue(String text) throws JacksonException {
        return _reportUnsupportedOperation();
    }

    @Override
    public JsonGenerator writeRawValue(String text, int offset, int len) throws JacksonException {
        return _reportUnsupportedOperation();
    }

    @Override
    public JsonGenerator writeRawValue(char[] text, int offset, int len) throws JacksonException {
        return _reportUnsupportedOperation();
    }

    /*
    /**********************************************************************
    /* Output method implementations, base64-encoded binary
    /**********************************************************************
     */

    @Override
    public JsonGenerator writeBinary(Base64Variant b64variant, byte[] data, int offset, int len) throws JacksonException
    {
        if (data == null) {
            return writeNull();
        }
        _verifyValueWrite("write Binary value");
        if (offset > 0 || (offset+len) != data.length) {
            data = Arrays.copyOfRange(data, offset, offset+len);
        }
        _writeScalarBinary(b64variant, data);
        return this;
    }

    /*
    /**********************************************************************
    /* Output method implementations, scalars
    /**********************************************************************
     */

    @Override
    public JsonGenerator writeBoolean(boolean state) throws JacksonException
    {
        _verifyValueWrite("write boolean value");
        _writeScalar(state ? "true" : "false", "bool", STYLE_SCALAR);
        return this;
    }

    @Override
    public JsonGenerator writeNumber(short v) throws JacksonException {
        return writeNumber((int) v);
    }

    @Override
    public JsonGenerator writeNumber(int v) throws JacksonException
    {
        _verifyValueWrite("write number");
        _writeScalar(Integer.toString(v), "int", STYLE_SCALAR);
        return this;
    }

    @Override
    public JsonGenerator writeNumber(long l) throws JacksonException
    {
        // First: maybe 32 bits is enough?
        if (l <= MAX_INT_AS_LONG && l >= MIN_INT_AS_LONG) {
            return writeNumber((int) l);
        }
        _verifyValueWrite("write number");
        _writeScalar(Long.toString(l), "long", STYLE_SCALAR);
        return this;
    }

    @Override
    public JsonGenerator writeNumber(BigInteger v) throws JacksonException
    {
        if (v == null) {
            return writeNull();
        }
        _verifyValueWrite("write number");
        _writeScalar(v.toString(), "java.math.BigInteger", STYLE_SCALAR);
        return this;
    }

    @Override
    public JsonGenerator writeNumber(double d) throws JacksonException
    {
        _verifyValueWrite("write number");
        if (NumberOutput.notFinite(d) && isEnabled(YAMLWriteFeature.USE_YAML_NONFINITE_NOTATION)) {
            if (Double.isNaN(d)) {
                _writeScalar(".nan", "double", STYLE_PLAIN);
            } else if (d > 0.0) {
                _writeScalar(".inf", "double", STYLE_PLAIN);
            } else {
                _writeScalar("-.inf", "double", STYLE_PLAIN);
            }
            return this;
        }
        _writeScalar(NumberOutput.toString(d, isEnabled(StreamWriteFeature.USE_FAST_DOUBLE_WRITER)),
                "double", STYLE_SCALAR);
        return this;
    }

    @Override
    public JsonGenerator writeNumber(float f) throws JacksonException
    {
        _verifyValueWrite("write number");
        if (NumberOutput.notFinite(f) && isEnabled(YAMLWriteFeature.USE_YAML_NONFINITE_NOTATION)) {
            if (Float.isNaN(f)) {
                _writeScalar(".nan", "float", STYLE_PLAIN);
            } else if (f > 0.0f) {
                _writeScalar(".inf", "float", STYLE_PLAIN);
            } else {
                _writeScalar("-.inf", "float", STYLE_PLAIN);
            }
            return this;
        }
        _writeScalar(NumberOutput.toString(f, isEnabled(StreamWriteFeature.USE_FAST_DOUBLE_WRITER)),
                "float", STYLE_SCALAR);
        return this;
    }

    @Override
    public JsonGenerator writeNumber(BigDecimal dec) throws JacksonException
    {
        if (dec == null) {
            return writeNull();
        }
        _verifyValueWrite("write number");
        String str = isEnabled(StreamWriteFeature.WRITE_BIGDECIMAL_AS_PLAIN) ? dec.toPlainString() : dec.toString();
        _writeScalar(str, "java.math.BigDecimal", STYLE_SCALAR);
        return this;
    }

    @Override
    public JsonGenerator writeNumber(String encodedValue) throws JacksonException
    {
        if (encodedValue == null) {
            return writeNull();
        }
        _verifyValueWrite("write number");
        _writeScalar(encodedValue, "number", STYLE_SCALAR);
        return this;
    }

    @Override
    public JsonGenerator writeNull() throws JacksonException
    {
        _verifyValueWrite("write null value");
        // no real type for this, is there?
        _writeScalar("null", "object", STYLE_SCALAR);
        return this;
    }

    /*
    /**********************************************************************
    /* Public API, write methods, Native Ids
    /**********************************************************************
     */

    @Override
    public JsonGenerator writeTypeId(Object id)
        throws JacksonException
    {
        // should we verify there's no preceding type id?
        _typeId = String.valueOf(id);
        return this;
    }

    @Override
    public JsonGenerator writeObjectRef(Object id)
        throws JacksonException
    {
        _verifyValueWrite("write Object reference");
        // NOTE: `String.valueOf()` to retain legacy handling of `null` id as "null"
        // (`AliasEvent` itself does not allow missing Anchor)
        AliasEvent evt = new AliasEvent(Optional.of(new Anchor(String.valueOf(id))));
        _emit(evt);
        return this;
    }

    @Override
    public JsonGenerator writeObjectId(Object id)
        throws JacksonException
    {
        // should we verify there's no preceding id?
        _objectId = (id == null) ? null : String.valueOf(id);
        return this;
    }

    /*
    /**********************************************************************
    /* Output method implementations, other
    /**********************************************************************
     */

    /**
     * Method for writing a YAML comment.
     * If called right after {@link #writeName}, the comment is emitted as an
     * end-of-line (inline) comment on the property name line; otherwise it is
     * emitted as a block comment (full line starting with {@code #}).
     *<p>
     * If {@code text} is {@code null}, an empty (blank) line is emitted instead.
     *<p>
     * Multi-line comments are supported: if the text contains newlines,
     * each line will be emitted as a separate comment line.
     *<p>
     * Note: YAML comments are presentation detail and are not part of the
     * serialization model per the YAML specification.
     *
     * @param text Comment text to write (without the leading {@code #});
     *    if {@code null}, writes an empty line
     *
     * @return This generator, to allow call chaining
     *
     * @throws JacksonException if there is a problem emitting the comment
     *
     * @since 3.2
     */
    @Override
    public YAMLGenerator writeComment(String text) throws JacksonException
    {
        if (text == null) {
            _emit(new CommentEvent(CommentType.BLANK_LINE, "",
                    Optional.empty(), Optional.empty()));
            return this;
        }
        // If we just wrote a property name (expecting value next),
        // emit as an inline (end-of-line) comment; otherwise as a block comment
        final CommentType type = (_streamWriteContext.inObject()
                && _streamWriteContext.hasCurrentName())
                ? CommentType.IN_LINE : CommentType.BLOCK;
        // Split multi-line comments into separate CommentEvents;
        // handle all standard linefeeds: "\r\n", "\r", "\n"
        String[] lines = NEWLINE_SPLITTER_P.split(text, -1);
        for (String line : lines) {
            _emit(new CommentEvent(type, line,
                    Optional.empty(), Optional.empty()));
        }
        return this;
    }

    /*
    /**********************************************************************
    /* Implementations for methods from base class
    /**********************************************************************
     */

    @Override
    protected final void _verifyValueWrite(String typeMsg) throws JacksonException
    {
        if (!_streamWriteContext.writeValue()) {
            _reportError("Cannot "+typeMsg+", expecting a property name");
        }
        if (_streamWriteContext.inRoot()) {
            // Start-doc emitted when creating generator, but otherwise need it; similarly,
            // need matching end-document to close earlier open one
            if (_streamWriteContext.getCurrentIndex() > 0) {
                _emitEndDocument();
                _emitStartDocument();
            }
        }

    }

    @Override
    protected void _releaseBuffers() {
        // nothing special to do...
    }

    /*
    /**********************************************************************
    /* Internal methods
    /**********************************************************************
     */

    // Implicit means that (type) tags won't be shown, right?
    private final static ImplicitTuple NO_TAGS = new ImplicitTuple(true, true);

    // ... and sometimes we specifically DO want explicit tag:
    private final static ImplicitTuple EXPLICIT_TAGS = new ImplicitTuple(false, false);

    protected void _writeScalar(String value, String type, ScalarStyle style) throws JacksonException
    {
        _emit(_scalarEvent(value, style));
    }

    private void _writeScalarBinary(Base64Variant b64variant,
            byte[] data) throws JacksonException
    {
        // 15-Dec-2017, tatu: as per [dataformats-text#62], can not use SnakeYAML's internal
        //    codec. Also: force use of linefeed variant if using default
        if (b64variant == Base64Variants.getDefaultVariant()) {
            b64variant = Base64Variants.MIME;
        }
        final String lf = _lf();
        String encoded = b64variant.encode(data, false, lf);
        _emit(new ScalarEvent(Optional.empty(), Optional.ofNullable(TAG_BINARY), EXPLICIT_TAGS, encoded, STYLE_BASE64));
    }

    protected ScalarEvent _scalarEvent(String value, ScalarStyle style)
    {
        String yamlTag = _typeId;
        if (yamlTag != null) {
            _typeId = null;
        }
        Optional<Anchor> anchor = Optional.ofNullable(_objectId).map(Anchor::new);
        _objectId = null;
        // 29-Nov-2017, tatu: Not 100% sure why we don't force explicit tags for
        //    type id, but trying to do so seems to double up tag output...
        return new ScalarEvent(anchor, Optional.ofNullable(yamlTag), NO_TAGS, value, style);
    }

    protected String _lf() {
        return _outputOptions.getBestLineBreak();
    }

    protected void _emitStartDocument() throws JacksonException
    {
        Map<String,String> noTags = Map.of();
        boolean startMarker = YAMLWriteFeature.WRITE_DOC_START_MARKER.enabledIn(_formatWriteFeatures);
        _emit(new DocumentStartEvent(startMarker, _outputOptions.getYamlDirective(),
                 // for 1.10 was: ((version == null) ? null : version.getArray()),
                noTags));
    }

    protected void _emitEndDocument() throws JacksonException {
        _emit(new DocumentEndEvent(false));
    }

    protected final void _emit(Event e) throws JacksonException {
        try {
            _emitter.emit(e);
        } catch (UncheckedIOException ex) {
            // throws `JacksonIOException`:
            throw _wrapIOFailure(ex.getCause());
        } catch (YamlEngineException ex) {
            // Wrap SnakeYAML exception as Jackson exception
            throw new JacksonYAMLWriteException(this, ex.getMessage(), ex);
        }
    }
}