Emitter.java

/**
 * Copyright (c) 2008, SnakeYAML
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
 * in compliance with the License. You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software distributed under the License
 * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
 * or implied. See the License for the specific language governing permissions and limitations under
 * the License.
 */
package org.yaml.snakeyaml.emitter;

import java.io.IOException;
import java.io.Writer;
import java.util.ArrayDeque;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.Set;
import java.util.TreeSet;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.yaml.snakeyaml.DumperOptions;
import org.yaml.snakeyaml.DumperOptions.ScalarStyle;
import org.yaml.snakeyaml.DumperOptions.Version;
import org.yaml.snakeyaml.comments.CommentEventsCollector;
import org.yaml.snakeyaml.comments.CommentLine;
import org.yaml.snakeyaml.comments.CommentType;
import org.yaml.snakeyaml.error.YAMLException;
import org.yaml.snakeyaml.events.AliasEvent;
import org.yaml.snakeyaml.events.CollectionEndEvent;
import org.yaml.snakeyaml.events.CollectionStartEvent;
import org.yaml.snakeyaml.events.CommentEvent;
import org.yaml.snakeyaml.events.DocumentEndEvent;
import org.yaml.snakeyaml.events.DocumentStartEvent;
import org.yaml.snakeyaml.events.Event;
import org.yaml.snakeyaml.events.Event.ID;
import org.yaml.snakeyaml.events.MappingEndEvent;
import org.yaml.snakeyaml.events.MappingStartEvent;
import org.yaml.snakeyaml.events.NodeEvent;
import org.yaml.snakeyaml.events.ScalarEvent;
import org.yaml.snakeyaml.events.SequenceEndEvent;
import org.yaml.snakeyaml.events.SequenceStartEvent;
import org.yaml.snakeyaml.events.StreamEndEvent;
import org.yaml.snakeyaml.events.StreamStartEvent;
import org.yaml.snakeyaml.nodes.Tag;
import org.yaml.snakeyaml.reader.StreamReader;
import org.yaml.snakeyaml.scanner.Constant;
import org.yaml.snakeyaml.util.ArrayStack;

/**
 * <pre>
 * Emitter expects events obeying the following grammar:
 * stream ::= STREAM-START document* STREAM-END
 * document ::= DOCUMENT-START node DOCUMENT-END
 * node ::= SCALAR | sequence | mapping
 * sequence ::= SEQUENCE-START node* SEQUENCE-END
 * mapping ::= MAPPING-START (node node)* MAPPING-END
 * </pre>
 */
public final class Emitter implements Emitable {

  /**
   * indent cannot be zero spaces
   */
  public static final int MIN_INDENT = 1;
  /**
   * indent should not be more than 10 spaces
   */
  public static final int MAX_INDENT = 10;
  private static final char[] SPACE = {' '};

  private static final Pattern SPACES_PATTERN = Pattern.compile("\\s");
  private static final Set<Character> INVALID_ANCHOR = new HashSet<Character>();

  static {
    INVALID_ANCHOR.add('[');
    INVALID_ANCHOR.add(']');
    INVALID_ANCHOR.add('{');
    INVALID_ANCHOR.add('}');
    INVALID_ANCHOR.add(',');
    INVALID_ANCHOR.add('*');
    INVALID_ANCHOR.add('&');
  }

  private static final Map<Character, String> ESCAPE_REPLACEMENTS =
      new HashMap<Character, String>();

  static {
    ESCAPE_REPLACEMENTS.put('\0', "0");
    ESCAPE_REPLACEMENTS.put('\u0007', "a");
    ESCAPE_REPLACEMENTS.put('\u0008', "b");
    ESCAPE_REPLACEMENTS.put('\u0009', "t");
    ESCAPE_REPLACEMENTS.put('\n', "n");
    ESCAPE_REPLACEMENTS.put('\u000B', "v");
    ESCAPE_REPLACEMENTS.put('\u000C', "f");
    ESCAPE_REPLACEMENTS.put('\r', "r");
    ESCAPE_REPLACEMENTS.put('\u001B', "e");
    ESCAPE_REPLACEMENTS.put('"', "\"");
    ESCAPE_REPLACEMENTS.put('\\', "\\");
    ESCAPE_REPLACEMENTS.put('\u0085', "N");
    ESCAPE_REPLACEMENTS.put('\u00A0', "_");
    ESCAPE_REPLACEMENTS.put('\u2028', "L");
    ESCAPE_REPLACEMENTS.put('\u2029', "P");
  }

  private static final Map<String, String> DEFAULT_TAG_PREFIXES =
      new LinkedHashMap<String, String>();

  static {
    DEFAULT_TAG_PREFIXES.put("!", "!");
    DEFAULT_TAG_PREFIXES.put(Tag.PREFIX, "!!");
  }

  // The stream should have the methods `write` and possibly `flush`.
  private final Writer stream;

  // Encoding is defined by Writer (cannot be overridden by STREAM-START.)
  // private Charset encoding;

  // Emitter is a state machine with a stack of states to handle nested
  // structures.
  private final ArrayStack<EmitterState> states;
  private EmitterState state;

  // Current event and the event queue.
  private final Queue<Event> events;
  private Event event;

  // The current indentation level and the stack of previous indents.
  private final ArrayStack<Integer> indents;
  private Integer indent;

  // Flow level.
  private int flowLevel;

  // Contexts.
  private boolean rootContext;
  private boolean mappingContext;
  private boolean simpleKeyContext;

  //
  // Characteristics of the last emitted character:
  // - current position.
  // - is it a whitespace?
  // - is it an indention character
  // (indentation space, '-', '?', or ':')?
  // private int line; this variable is not used
  private int column;
  private boolean whitespace;
  private boolean indention;
  private boolean openEnded;

  // Formatting details.
  private final Boolean canonical;
  // pretty print flow by adding extra line breaks
  private final Boolean prettyFlow;

  private final boolean allowUnicode;
  private int bestIndent;
  private final int indicatorIndent;
  private final boolean indentWithIndicator;
  private int bestWidth;
  private final char[] bestLineBreak;
  private final boolean splitLines;
  private final int maxSimpleKeyLength;
  private final boolean emitComments;

  // Tag prefixes.
  private Map<String, String> tagPrefixes;

  // Prepared anchor and tag.
  private String preparedAnchor;
  private String preparedTag;

  // Scalar analysis and style.
  private ScalarAnalysis analysis;
  private DumperOptions.ScalarStyle style;

  // Comment processing
  private final CommentEventsCollector blockCommentsCollector;
  private final CommentEventsCollector inlineCommentsCollector;


  /**
   * Create
   *
   * @param stream - output to write to
   * @param opts - options
   */
  public Emitter(Writer stream, DumperOptions opts) {
    if (stream == null) {
      throw new NullPointerException("Writer must be provided.");
    }
    if (opts == null) {
      throw new NullPointerException("DumperOptions must be provided.");
    }
    // The stream should have the methods `write` and possibly `flush`.
    this.stream = stream;
    // Emitter is a state machine with a stack of states to handle nested structures.
    this.states = new ArrayStack<EmitterState>(100);
    this.state = new ExpectStreamStart();
    // Current event and the event queue.
    this.events = new ArrayDeque<>(100);
    this.event = null;
    // The current indentation level and the stack of previous indents.
    this.indents = new ArrayStack<Integer>(10);
    this.indent = null;
    // Flow level.
    this.flowLevel = 0;
    // Contexts.
    mappingContext = false;
    simpleKeyContext = false;

    //
    // Characteristics of the last emitted character:
    // - current position.
    // - is it a whitespace?
    // - is it an indention character
    // (indentation space, '-', '?', or ':')?
    column = 0;
    whitespace = true;
    indention = true;

    // Whether the document requires an explicit document indicator
    openEnded = false;

    // Formatting details.
    this.canonical = opts.isCanonical();
    this.prettyFlow = opts.isPrettyFlow();
    this.allowUnicode = opts.isAllowUnicode();
    this.bestIndent = 2;
    if ((opts.getIndent() > MIN_INDENT) && (opts.getIndent() < MAX_INDENT)) {
      this.bestIndent = opts.getIndent();
    }
    this.indicatorIndent = opts.getIndicatorIndent();
    this.indentWithIndicator = opts.getIndentWithIndicator();
    this.bestWidth = 80;
    if (opts.getWidth() > this.bestIndent * 2) {
      this.bestWidth = opts.getWidth();
    }
    this.bestLineBreak = opts.getLineBreak().getString().toCharArray();
    this.splitLines = opts.getSplitLines();
    this.maxSimpleKeyLength = opts.getMaxSimpleKeyLength();
    this.emitComments = opts.isProcessComments();

    // Tag prefixes.
    this.tagPrefixes = new LinkedHashMap<String, String>();

    // Prepared anchor and tag.
    this.preparedAnchor = null;
    this.preparedTag = null;

    // Scalar analysis and style.
    this.analysis = null;
    this.style = null;

    // Comment processing
    this.blockCommentsCollector =
        new CommentEventsCollector(events, CommentType.BLANK_LINE, CommentType.BLOCK);
    this.inlineCommentsCollector = new CommentEventsCollector(events, CommentType.IN_LINE);
  }

  public void emit(Event event) throws IOException {
    this.events.add(event);
    while (!needMoreEvents()) {
      this.event = this.events.poll();
      this.state.expect();
      this.event = null;
    }
  }

  // In some cases, we wait for a few next events before emitting.

  private boolean needMoreEvents() {
    if (events.isEmpty()) {
      return true;
    }

    Iterator<Event> iter = events.iterator();
    Event event = iter.next(); // it cannot be empty here
    while (event instanceof CommentEvent) {
      if (!iter.hasNext()) {
        return true;
      }
      event = iter.next();
    }

    if (event instanceof DocumentStartEvent) {
      return needEvents(iter, 1);
    } else if (event instanceof SequenceStartEvent) {
      return needEvents(iter, 2);
    } else if (event instanceof MappingStartEvent) {
      return needEvents(iter, 3);
    } else if (event instanceof StreamStartEvent) {
      return needEvents(iter, 2);
    } else if (event instanceof StreamEndEvent) {
      return false;
    } else if (emitComments) {
      return needEvents(iter, 1);
    }
    return false;
  }

  private boolean needEvents(Iterator<Event> iter, int count) {
    int level = 0;
    int actualCount = 0;
    while (iter.hasNext()) {
      Event event = iter.next();
      if (event instanceof CommentEvent) {
        continue;
      }
      actualCount++;
      if (event instanceof DocumentStartEvent || event instanceof CollectionStartEvent) {
        level++;
      } else if (event instanceof DocumentEndEvent || event instanceof CollectionEndEvent) {
        level--;
      } else if (event instanceof StreamEndEvent) {
        level = -1;
      }
      if (level < 0) {
        return false;
      }
    }
    return actualCount < count;
  }

  private void increaseIndent(boolean flow, boolean indentless) {
    indents.push(indent);
    if (indent == null) {
      if (flow) {
        indent = bestIndent;
      } else {
        indent = 0;
      }
    } else if (!indentless) {
      this.indent += bestIndent;
    }
  }

  // States

  // Stream handlers.

  private class ExpectStreamStart implements EmitterState {

    public void expect() throws IOException {
      if (event instanceof StreamStartEvent) {
        writeStreamStart();
        state = new ExpectFirstDocumentStart();
      } else {
        throw new EmitterException("expected StreamStartEvent, but got " + event);
      }
    }
  }

  private class ExpectNothing implements EmitterState {

    public void expect() throws IOException {
      throw new EmitterException("expecting nothing, but got " + event);
    }
  }

  // Document handlers.

  private class ExpectFirstDocumentStart implements EmitterState {

    public void expect() throws IOException {
      new ExpectDocumentStart(true).expect();
    }
  }

  private class ExpectDocumentStart implements EmitterState {

    private final boolean first;

    public ExpectDocumentStart(boolean first) {
      this.first = first;
    }

    public void expect() throws IOException {
      if (event instanceof DocumentStartEvent) {
        DocumentStartEvent ev = (DocumentStartEvent) event;
        if ((ev.getVersion() != null || ev.getTags() != null) && openEnded) {
          writeIndicator("...", true, false, false);
          writeIndent();
        }
        if (ev.getVersion() != null) {
          String versionText = prepareVersion(ev.getVersion());
          writeVersionDirective(versionText);
        }
        tagPrefixes = new LinkedHashMap<String, String>(DEFAULT_TAG_PREFIXES);
        if (ev.getTags() != null) {
          Set<String> handles = new TreeSet<String>(ev.getTags().keySet());
          for (String handle : handles) {
            String prefix = ev.getTags().get(handle);
            tagPrefixes.put(prefix, handle);
            String handleText = prepareTagHandle(handle);
            String prefixText = prepareTagPrefix(prefix);
            writeTagDirective(handleText, prefixText);
          }
        }
        boolean implicit = first && !ev.getExplicit() && !canonical && ev.getVersion() == null
            && (ev.getTags() == null || ev.getTags().isEmpty()) && !checkEmptyDocument();
        if (!implicit) {
          writeIndent();
          writeIndicator("---", true, false, false);
          if (canonical) {
            writeIndent();
          }
        }
        state = new ExpectDocumentRoot();
      } else if (event instanceof StreamEndEvent) {
        writeStreamEnd();
        state = new ExpectNothing();
      } else if (event instanceof CommentEvent) {
        blockCommentsCollector.collectEvents(event);
        writeBlockComment();
        // state = state; remains unchanged
      } else {
        throw new EmitterException("expected DocumentStartEvent, but got " + event);
      }
    }
  }

  private class ExpectDocumentEnd implements EmitterState {

    public void expect() throws IOException {
      event = blockCommentsCollector.collectEventsAndPoll(event);
      writeBlockComment();
      if (event instanceof DocumentEndEvent) {
        writeIndent();
        if (((DocumentEndEvent) event).getExplicit()) {
          writeIndicator("...", true, false, false);
          writeIndent();
        }
        flushStream();
        state = new ExpectDocumentStart(false);
      } else {
        throw new EmitterException("expected DocumentEndEvent, but got " + event);
      }
    }
  }

  private class ExpectDocumentRoot implements EmitterState {

    public void expect() throws IOException {
      event = blockCommentsCollector.collectEventsAndPoll(event);
      if (!blockCommentsCollector.isEmpty()) {
        writeBlockComment();
        if (event instanceof DocumentEndEvent) {
          new ExpectDocumentEnd().expect();
          return;
        }
      }
      states.push(new ExpectDocumentEnd());
      expectNode(true, false, false);
    }
  }

  // Node handlers.

  private void expectNode(boolean root, boolean mapping, boolean simpleKey) throws IOException {
    rootContext = root;
    mappingContext = mapping;
    simpleKeyContext = simpleKey;
    if (event instanceof AliasEvent) {
      expectAlias();
    } else if (event instanceof ScalarEvent || event instanceof CollectionStartEvent) {
      processAnchor("&");
      processTag();
      if (event instanceof ScalarEvent) {
        expectScalar();
      } else if (event instanceof SequenceStartEvent) {
        if (flowLevel != 0 || canonical || ((SequenceStartEvent) event).isFlow()
            || checkEmptySequence()) {
          expectFlowSequence();
        } else {
          expectBlockSequence();
        }
      } else {// MappingStartEvent
        if (flowLevel != 0 || canonical || ((MappingStartEvent) event).isFlow()
            || checkEmptyMapping()) {
          expectFlowMapping();
        } else {
          expectBlockMapping();
        }
      }
    } else {
      throw new EmitterException("expected NodeEvent, but got " + event);
    }
  }

  private void expectAlias() throws IOException {
    if (!(event instanceof AliasEvent)) {
      throw new EmitterException("Alias must be provided");
    }
    processAnchor("*");
    state = states.pop();
  }

  private void expectScalar() throws IOException {
    increaseIndent(true, false);
    processScalar();
    indent = indents.pop();
    state = states.pop();
  }

  // Flow sequence handlers.

  private void expectFlowSequence() throws IOException {
    writeIndicator("[", true, true, false);
    flowLevel++;
    increaseIndent(true, false);
    if (prettyFlow) {
      writeIndent();
    }
    state = new ExpectFirstFlowSequenceItem();
  }

  private class ExpectFirstFlowSequenceItem implements EmitterState {

    public void expect() throws IOException {
      if (event instanceof SequenceEndEvent) {
        indent = indents.pop();
        flowLevel--;
        writeIndicator("]", false, false, false);
        inlineCommentsCollector.collectEvents();
        writeInlineComments();
        state = states.pop();
      } else if (event instanceof CommentEvent) {
        blockCommentsCollector.collectEvents(event);
        writeBlockComment();
      } else {
        if (canonical || (column > bestWidth && splitLines) || prettyFlow) {
          writeIndent();
        }
        states.push(new ExpectFlowSequenceItem());
        expectNode(false, false, false);
        event = inlineCommentsCollector.collectEvents(event);
        writeInlineComments();
      }
    }
  }

  private class ExpectFlowSequenceItem implements EmitterState {

    public void expect() throws IOException {
      if (event instanceof SequenceEndEvent) {
        indent = indents.pop();
        flowLevel--;
        if (canonical) {
          writeIndicator(",", false, false, false);
          writeIndent();
        } else if (prettyFlow) {
          writeIndent();
        }
        writeIndicator("]", false, false, false);
        inlineCommentsCollector.collectEvents();
        writeInlineComments();
        if (prettyFlow) {
          writeIndent();
        }
        state = states.pop();
      } else if (event instanceof CommentEvent) {
        event = blockCommentsCollector.collectEvents(event);
      } else {
        writeIndicator(",", false, false, false);
        writeBlockComment();
        if (canonical || (column > bestWidth && splitLines) || prettyFlow) {
          writeIndent();
        }
        states.push(new ExpectFlowSequenceItem());
        expectNode(false, false, false);
        event = inlineCommentsCollector.collectEvents(event);
        writeInlineComments();
      }
    }
  }

  // Flow mapping handlers.

  private void expectFlowMapping() throws IOException {
    writeIndicator("{", true, true, false);
    flowLevel++;
    increaseIndent(true, false);
    if (prettyFlow) {
      writeIndent();
    }
    state = new ExpectFirstFlowMappingKey();
  }

  private class ExpectFirstFlowMappingKey implements EmitterState {

    public void expect() throws IOException {
      event = blockCommentsCollector.collectEventsAndPoll(event);
      writeBlockComment();
      if (event instanceof MappingEndEvent) {
        indent = indents.pop();
        flowLevel--;
        writeIndicator("}", false, false, false);
        inlineCommentsCollector.collectEvents();
        writeInlineComments();
        state = states.pop();
      } else {
        if (canonical || (column > bestWidth && splitLines) || prettyFlow) {
          writeIndent();
        }
        if (!canonical && checkSimpleKey()) {
          states.push(new ExpectFlowMappingSimpleValue());
          expectNode(false, true, true);
        } else {
          writeIndicator("?", true, false, false);
          states.push(new ExpectFlowMappingValue());
          expectNode(false, true, false);
        }
      }
    }
  }

  private class ExpectFlowMappingKey implements EmitterState {

    public void expect() throws IOException {
      if (event instanceof MappingEndEvent) {
        indent = indents.pop();
        flowLevel--;
        if (canonical) {
          writeIndicator(",", false, false, false);
          writeIndent();
        }
        if (prettyFlow) {
          writeIndent();
        }
        writeIndicator("}", false, false, false);
        inlineCommentsCollector.collectEvents();
        writeInlineComments();
        state = states.pop();
      } else {
        writeIndicator(",", false, false, false);
        event = blockCommentsCollector.collectEventsAndPoll(event);
        writeBlockComment();
        if (canonical || (column > bestWidth && splitLines) || prettyFlow) {
          writeIndent();
        }
        if (!canonical && checkSimpleKey()) {
          states.push(new ExpectFlowMappingSimpleValue());
          expectNode(false, true, true);
        } else {
          writeIndicator("?", true, false, false);
          states.push(new ExpectFlowMappingValue());
          expectNode(false, true, false);
        }
      }
    }
  }

  private class ExpectFlowMappingSimpleValue implements EmitterState {

    public void expect() throws IOException {
      writeIndicator(":", false, false, false);
      event = inlineCommentsCollector.collectEventsAndPoll(event);
      writeInlineComments();
      states.push(new ExpectFlowMappingKey());
      expectNode(false, true, false);
      inlineCommentsCollector.collectEvents(event);
      writeInlineComments();
    }
  }

  private class ExpectFlowMappingValue implements EmitterState {

    public void expect() throws IOException {
      if (canonical || (column > bestWidth) || prettyFlow) {
        writeIndent();
      }
      writeIndicator(":", true, false, false);
      event = inlineCommentsCollector.collectEventsAndPoll(event);
      writeInlineComments();
      states.push(new ExpectFlowMappingKey());
      expectNode(false, true, false);
      inlineCommentsCollector.collectEvents(event);
      writeInlineComments();
    }
  }

  // Block sequence handlers.

  private void expectBlockSequence() throws IOException {
    boolean indentless = mappingContext && !indention;
    increaseIndent(false, indentless);
    state = new ExpectFirstBlockSequenceItem();
  }

  private class ExpectFirstBlockSequenceItem implements EmitterState {

    public void expect() throws IOException {
      new ExpectBlockSequenceItem(true).expect();
    }
  }

  private class ExpectBlockSequenceItem implements EmitterState {

    private final boolean first;

    public ExpectBlockSequenceItem(boolean first) {
      this.first = first;
    }

    public void expect() throws IOException {
      if (!this.first && event instanceof SequenceEndEvent) {
        indent = indents.pop();
        state = states.pop();
      } else if (event instanceof CommentEvent) {
        blockCommentsCollector.collectEvents(event);
      } else {
        writeIndent();
        if (!indentWithIndicator || this.first) {
          writeWhitespace(indicatorIndent);
        }
        writeIndicator("-", true, false, true);
        if (indentWithIndicator && this.first) {
          indent += indicatorIndent;
        }
        if (!blockCommentsCollector.isEmpty()) {
          increaseIndent(false, false);
          writeBlockComment();
          if (event instanceof ScalarEvent) {
            analysis = analyzeScalar(((ScalarEvent) event).getValue());
            if (!analysis.isEmpty()) {
              writeIndent();
            }
          }
          indent = indents.pop();
        }
        states.push(new ExpectBlockSequenceItem(false));
        expectNode(false, false, false);
        inlineCommentsCollector.collectEvents();
        writeInlineComments();
      }
    }
  }

  // Block mapping handlers.
  private void expectBlockMapping() throws IOException {
    increaseIndent(false, false);
    state = new ExpectFirstBlockMappingKey();
  }

  private class ExpectFirstBlockMappingKey implements EmitterState {

    public void expect() throws IOException {
      new ExpectBlockMappingKey(true).expect();
    }
  }

  private class ExpectBlockMappingKey implements EmitterState {

    private final boolean first;

    public ExpectBlockMappingKey(boolean first) {
      this.first = first;
    }

    public void expect() throws IOException {
      event = blockCommentsCollector.collectEventsAndPoll(event);
      writeBlockComment();
      if (!this.first && event instanceof MappingEndEvent) {
        indent = indents.pop();
        state = states.pop();
      } else {
        writeIndent();
        if (checkSimpleKey()) {
          states.push(new ExpectBlockMappingSimpleValue());
          expectNode(false, true, true);
        } else {
          writeIndicator("?", true, false, true);
          states.push(new ExpectBlockMappingValue());
          expectNode(false, true, false);
        }
      }
    }
  }

  private boolean isFoldedOrLiteral(Event event) {
    if (!event.is(ID.Scalar)) {
      return false;
    }
    ScalarEvent scalarEvent = (ScalarEvent) event;
    ScalarStyle style = scalarEvent.getScalarStyle();
    return style == ScalarStyle.FOLDED || style == ScalarStyle.LITERAL;
  }

  private class ExpectBlockMappingSimpleValue implements EmitterState {

    public void expect() throws IOException {
      writeIndicator(":", false, false, false);
      event = inlineCommentsCollector.collectEventsAndPoll(event);
      if (!isFoldedOrLiteral(event)) {
        if (writeInlineComments()) {
          increaseIndent(true, false);
          writeIndent();
          indent = indents.pop();
        }
      }
      event = blockCommentsCollector.collectEventsAndPoll(event);
      if (!blockCommentsCollector.isEmpty()) {
        increaseIndent(true, false);
        writeBlockComment();
        writeIndent();
        indent = indents.pop();
      }
      states.push(new ExpectBlockMappingKey(false));
      expectNode(false, true, false);
      inlineCommentsCollector.collectEvents();
      writeInlineComments();
    }
  }

  private class ExpectBlockMappingValue implements EmitterState {

    public void expect() throws IOException {
      writeIndent();
      writeIndicator(":", true, false, true);
      event = inlineCommentsCollector.collectEventsAndPoll(event);
      writeInlineComments();
      event = blockCommentsCollector.collectEventsAndPoll(event);
      writeBlockComment();
      states.push(new ExpectBlockMappingKey(false));
      expectNode(false, true, false);
      inlineCommentsCollector.collectEvents(event);
      writeInlineComments();
    }
  }

  // Checkers.

  private boolean checkEmptySequence() {
    return event instanceof SequenceStartEvent && !events.isEmpty()
        && events.peek() instanceof SequenceEndEvent;
  }

  private boolean checkEmptyMapping() {
    return event instanceof MappingStartEvent && !events.isEmpty()
        && events.peek() instanceof MappingEndEvent;
  }

  private boolean checkEmptyDocument() {
    if (!(event instanceof DocumentStartEvent) || events.isEmpty()) {
      return false;
    }
    Event event = events.peek();
    if (event instanceof ScalarEvent) {
      ScalarEvent e = (ScalarEvent) event;
      return e.getAnchor() == null && e.getTag() == null && e.getImplicit() != null
          && e.getValue().length() == 0;
    }
    return false;
  }

  private boolean checkSimpleKey() {
    int length = 0;
    if (event instanceof NodeEvent && ((NodeEvent) event).getAnchor() != null) {
      if (preparedAnchor == null) {
        preparedAnchor = prepareAnchor(((NodeEvent) event).getAnchor());
      }
      length += preparedAnchor.length();
    }
    String tag = null;
    if (event instanceof ScalarEvent) {
      tag = ((ScalarEvent) event).getTag();
    } else if (event instanceof CollectionStartEvent) {
      tag = ((CollectionStartEvent) event).getTag();
    }
    if (tag != null) {
      if (preparedTag == null) {
        preparedTag = prepareTag(tag);
      }
      length += preparedTag.length();
    }
    if (event instanceof ScalarEvent) {
      if (analysis == null) {
        analysis = analyzeScalar(((ScalarEvent) event).getValue());
      }
      length += analysis.getScalar().length();
    }
    return length < maxSimpleKeyLength && (event instanceof AliasEvent
        || (event instanceof ScalarEvent && !analysis.isEmpty() && !analysis.isMultiline())
        || checkEmptySequence() || checkEmptyMapping());
  }

  // Anchor, Tag, and Scalar processors.

  private void processAnchor(String indicator) throws IOException {
    NodeEvent ev = (NodeEvent) event;
    if (ev.getAnchor() == null) {
      preparedAnchor = null;
      return;
    }
    if (preparedAnchor == null) {
      preparedAnchor = prepareAnchor(ev.getAnchor());
    }
    writeIndicator(indicator + preparedAnchor, true, false, false);
    preparedAnchor = null;
  }

  private void processTag() throws IOException {
    String tag = null;
    if (event instanceof ScalarEvent) {
      ScalarEvent ev = (ScalarEvent) event;
      tag = ev.getTag();
      if (style == null) {
        style = chooseScalarStyle();
      }
      if ((!canonical || tag == null)
          && ((style == null && ev.getImplicit().canOmitTagInPlainScalar())
              || (style != null && ev.getImplicit().canOmitTagInNonPlainScalar()))) {
        preparedTag = null;
        return;
      }
      if (ev.getImplicit().canOmitTagInPlainScalar() && tag == null) {
        tag = "!";
        preparedTag = null;
      }
    } else {
      CollectionStartEvent ev = (CollectionStartEvent) event;
      tag = ev.getTag();
      if ((!canonical || tag == null) && ev.getImplicit()) {
        preparedTag = null;
        return;
      }
    }
    if (tag == null) {
      throw new EmitterException("tag is not specified");
    }
    if (preparedTag == null) {
      preparedTag = prepareTag(tag);
    }
    writeIndicator(preparedTag, true, false, false);
    preparedTag = null;
  }

  private DumperOptions.ScalarStyle chooseScalarStyle() {
    ScalarEvent ev = (ScalarEvent) event;
    if (analysis == null) {
      analysis = analyzeScalar(ev.getValue());
    }
    if (!ev.isPlain() && ev.getScalarStyle() == DumperOptions.ScalarStyle.DOUBLE_QUOTED
        || this.canonical) {
      return DumperOptions.ScalarStyle.DOUBLE_QUOTED;
    }
    if (ev.isPlain() && ev.getImplicit().canOmitTagInPlainScalar()) {
      if (!(simpleKeyContext && (analysis.isEmpty() || analysis.isMultiline()))
          && ((flowLevel != 0 && analysis.isAllowFlowPlain())
              || (flowLevel == 0 && analysis.isAllowBlockPlain()))) {
        return null;
      }
    }
    if (!ev.isPlain() && (ev.getScalarStyle() == DumperOptions.ScalarStyle.LITERAL
        || ev.getScalarStyle() == DumperOptions.ScalarStyle.FOLDED)) {
      if (flowLevel == 0 && !simpleKeyContext && analysis.isAllowBlock()) {
        return ev.getScalarStyle();
      }
    }
    if (ev.isPlain() || ev.getScalarStyle() == DumperOptions.ScalarStyle.SINGLE_QUOTED) {
      if (analysis.isAllowSingleQuoted() && !(simpleKeyContext && analysis.isMultiline())) {
        return DumperOptions.ScalarStyle.SINGLE_QUOTED;
      }
    }
    return DumperOptions.ScalarStyle.DOUBLE_QUOTED;
  }

  private void processScalar() throws IOException {
    ScalarEvent ev = (ScalarEvent) event;
    if (analysis == null) {
      analysis = analyzeScalar(ev.getValue());
    }
    if (style == null) {
      style = chooseScalarStyle();
    }
    boolean split = !simpleKeyContext && splitLines;
    if (style == null) {
      writePlain(analysis.getScalar(), split);
    } else {
      switch (style) {
        case DOUBLE_QUOTED:
          writeDoubleQuoted(analysis.getScalar(), split);
          break;
        case SINGLE_QUOTED:
          writeSingleQuoted(analysis.getScalar(), split);
          break;
        case FOLDED:
          writeFolded(analysis.getScalar(), split);
          break;
        case LITERAL:
          writeLiteral(analysis.getScalar());
          break;
        default:
          throw new YAMLException("Unexpected style: " + style);
      }
    }
    analysis = null;
    style = null;
  }

  // Analyzers.

  private String prepareVersion(Version version) {
    if (version.major() != 1) {
      throw new EmitterException("unsupported YAML version: " + version);
    }
    return version.getRepresentation();
  }

  private static final Pattern HANDLE_FORMAT = Pattern.compile("^![-_\\w]*!$");

  private String prepareTagHandle(String handle) {
    if (handle.length() == 0) {
      throw new EmitterException("tag handle must not be empty");
    } else if (handle.charAt(0) != '!' || handle.charAt(handle.length() - 1) != '!') {
      throw new EmitterException("tag handle must start and end with '!': " + handle);
    } else if (!"!".equals(handle) && !HANDLE_FORMAT.matcher(handle).matches()) {
      throw new EmitterException("invalid character in the tag handle: " + handle);
    }
    return handle;
  }

  private String prepareTagPrefix(String prefix) {
    if (prefix.length() == 0) {
      throw new EmitterException("tag prefix must not be empty");
    }
    StringBuilder chunks = new StringBuilder();
    int start = 0;
    int end = 0;
    if (prefix.charAt(0) == '!') {
      end = 1;
    }
    while (end < prefix.length()) {
      end++;
    }
    if (start < end) {
      chunks.append(prefix, start, end);
    }
    return chunks.toString();
  }

  private String prepareTag(String tag) {
    if (tag.length() == 0) {
      throw new EmitterException("tag must not be empty");
    }
    if ("!".equals(tag)) {
      return tag;
    }
    String handle = null;
    String suffix = tag;
    // shall the tag prefixes be sorted as in PyYAML?
    for (String prefix : tagPrefixes.keySet()) {
      if (tag.startsWith(prefix) && ("!".equals(prefix) || prefix.length() < tag.length())) {
        handle = prefix;
      }
    }
    if (handle != null) {
      suffix = tag.substring(handle.length());
      handle = tagPrefixes.get(handle);
    }

    int end = suffix.length();
    String suffixText = end > 0 ? suffix.substring(0, end) : "";

    if (handle != null) {
      return handle + suffixText;
    }
    return "!<" + suffixText + ">";
  }

  static String prepareAnchor(String anchor) {
    if (anchor.length() == 0) {
      throw new EmitterException("anchor must not be empty");
    }
    for (Character invalid : INVALID_ANCHOR) {
      if (anchor.indexOf(invalid) > -1) {
        throw new EmitterException("Invalid character '" + invalid + "' in the anchor: " + anchor);
      }
    }
    Matcher matcher = SPACES_PATTERN.matcher(anchor);
    if (matcher.find()) {
      throw new EmitterException("Anchor may not contain spaces: " + anchor);
    }
    return anchor;
  }

  // Equivalent to Pattern.compile("0[0-9_]+").matcher(scalar).matches().
  private static boolean hasLeadingZero(String scalar) {
    if (scalar.length() > 1 && scalar.charAt(0) == '0') {
      for (int i = 1; i < scalar.length(); i++) {
        char ch = scalar.charAt(i);
        boolean isDigitOrUnderscore = (ch >= '0' && ch <= '9') || ch == '_';
        if (!isDigitOrUnderscore) {
          return false;
        }
      }
      return true;
    }
    return false;
  }

  private ScalarAnalysis analyzeScalar(String scalar) {
    // Empty scalar is a special case.
    if (scalar.length() == 0) {
      return new ScalarAnalysis(scalar, true, false, false, true, true, false);
    }
    // Indicators and special characters.
    boolean blockIndicators = false;
    boolean flowIndicators = false;
    boolean lineBreaks = false;
    boolean specialCharacters = false;
    boolean leadingZeroNumber = hasLeadingZero(scalar);

    // Important whitespace combinations.
    boolean leadingSpace = false;
    boolean leadingBreak = false;
    boolean trailingSpace = false;
    boolean trailingBreak = false;
    boolean breakSpace = false;
    boolean spaceBreak = false;

    // Check document indicators.
    if (scalar.startsWith("---") || scalar.startsWith("...")) {
      blockIndicators = true;
      flowIndicators = true;
    }
    // First character or preceded by a whitespace.
    boolean preceededByWhitespace = true;
    boolean followedByWhitespace =
        scalar.length() == 1 || Constant.NULL_BL_T_LINEBR.has(scalar.codePointAt(1));
    // The previous character is a space.
    boolean previousSpace = false;

    // The previous character is a break.
    boolean previousBreak = false;

    int index = 0;

    while (index < scalar.length()) {
      int c = scalar.codePointAt(index);
      // Check for indicators.
      if (index == 0) {
        // Leading indicators are special characters.
        if ("#,[]{}&*!|>'\"%@`".indexOf(c) != -1) {
          flowIndicators = true;
          blockIndicators = true;
        }
        if (c == '?' || c == ':') {
          flowIndicators = true;
          if (followedByWhitespace) {
            blockIndicators = true;
          }
        }
        if (c == '-' && followedByWhitespace) {
          flowIndicators = true;
          blockIndicators = true;
        }
      } else {
        // Some indicators cannot appear within a scalar as well.
        if (",?[]{}".indexOf(c) != -1) {
          flowIndicators = true;
        }
        if (c == ':') {
          flowIndicators = true;
          if (followedByWhitespace) {
            blockIndicators = true;
          }
        }
        if (c == '#' && preceededByWhitespace) {
          flowIndicators = true;
          blockIndicators = true;
        }
      }
      // Check for line breaks, special, and unicode characters.
      boolean isLineBreak = Constant.LINEBR.has(c);
      if (isLineBreak) {
        lineBreaks = true;
      }
      if (!(c == '\n' || (0x20 <= c && c <= 0x7E))) {
        if (c == 0x85 || (c >= 0xA0 && c <= 0xD7FF) || (c >= 0xE000 && c <= 0xFFFD)
            || (c >= 0x10000 && c <= 0x10FFFF)) {
          // unicode is used
          if (!this.allowUnicode) {
            specialCharacters = true;
          }
        } else {
          specialCharacters = true;
        }
      }
      // Detect important whitespace combinations.
      if (c == ' ') {
        if (index == 0) {
          leadingSpace = true;
        }
        if (index == scalar.length() - 1) {
          trailingSpace = true;
        }
        if (previousBreak) {
          breakSpace = true;
        }
        previousSpace = true;
        previousBreak = false;
      } else if (isLineBreak) {
        if (index == 0) {
          leadingBreak = true;
        }
        if (index == scalar.length() - 1) {
          trailingBreak = true;
        }
        if (previousSpace) {
          spaceBreak = true;
        }
        previousSpace = false;
        previousBreak = true;
      } else {
        previousSpace = false;
        previousBreak = false;
      }

      // Prepare for the next character.
      index += Character.charCount(c);
      preceededByWhitespace = Constant.NULL_BL_T.has(c) || isLineBreak;
      followedByWhitespace = true;
      if (index + 1 < scalar.length()) {
        int nextIndex = index + Character.charCount(scalar.codePointAt(index));
        if (nextIndex < scalar.length()) {
          followedByWhitespace =
              (Constant.NULL_BL_T.has(scalar.codePointAt(nextIndex))) || isLineBreak;
        }
      }
    }
    // Let's decide what styles are allowed.
    boolean allowFlowPlain = true;
    boolean allowBlockPlain = true;
    boolean allowSingleQuoted = true;
    boolean allowBlock = true;
    // Leading and trailing whitespaces are bad for plain scalars.
    if (leadingSpace || leadingBreak || trailingSpace || trailingBreak || leadingZeroNumber) {
      allowFlowPlain = allowBlockPlain = false;
    }
    // We do not permit trailing spaces for block scalars.
    if (trailingSpace) {
      allowBlock = false;
    }
    // Spaces at the beginning of a new line are only acceptable for block
    // scalars.
    if (breakSpace) {
      allowFlowPlain = allowBlockPlain = allowSingleQuoted = false;
    }
    // Spaces followed by breaks, as well as special character are only
    // allowed for double-quoted scalars.
    if (spaceBreak || specialCharacters) {
      allowFlowPlain = allowBlockPlain = allowSingleQuoted = allowBlock = false;
    }
    // Although the plain scalar writer supports breaks, we never emit
    // multiline plain scalars in the flow context.
    if (lineBreaks) {
      allowFlowPlain = false;
    }
    // Flow indicators are forbidden for flow plain scalars.
    if (flowIndicators) {
      allowFlowPlain = false;
    }
    // Block indicators are forbidden for block plain scalars.
    if (blockIndicators) {
      allowBlockPlain = false;
    }

    return new ScalarAnalysis(scalar, false, lineBreaks, allowFlowPlain, allowBlockPlain,
        allowSingleQuoted, allowBlock);
  }

  // Writers.

  void flushStream() throws IOException {
    stream.flush();
  }

  void writeStreamStart() {
    // BOM is written by Writer.
  }

  void writeStreamEnd() throws IOException {
    flushStream();
  }

  void writeIndicator(String indicator, boolean needWhitespace, boolean whitespace,
      boolean indentation) throws IOException {
    if (!this.whitespace && needWhitespace) {
      this.column++;
      stream.write(SPACE);
    }
    this.whitespace = whitespace;
    this.indention = this.indention && indentation;
    this.column += indicator.length();
    openEnded = false;
    stream.write(indicator);
  }

  void writeIndent() throws IOException {
    int indent;
    if (this.indent != null) {
      indent = this.indent;
    } else {
      indent = 0;
    }

    if (!this.indention || this.column > indent || (this.column == indent && !this.whitespace)) {
      writeLineBreak(null);
    }

    writeWhitespace(indent - this.column);
  }

  private void writeWhitespace(int length) throws IOException {
    if (length <= 0) {
      return;
    }
    this.whitespace = true;
    char[] data = new char[length];
    for (int i = 0; i < data.length; i++) {
      data[i] = ' ';
    }
    this.column += length;
    stream.write(data);
  }

  private void writeLineBreak(String data) throws IOException {
    this.whitespace = true;
    this.indention = true;
    this.column = 0;
    if (data == null) {
      stream.write(this.bestLineBreak);
    } else {
      stream.write(data);
    }
  }

  void writeVersionDirective(String versionText) throws IOException {
    stream.write("%YAML ");
    stream.write(versionText);
    writeLineBreak(null);
  }

  void writeTagDirective(String handleText, String prefixText) throws IOException {
    // XXX: not sure 4 invocations better then StringBuilders created by str
    // + str
    stream.write("%TAG ");
    stream.write(handleText);
    stream.write(SPACE);
    stream.write(prefixText);
    writeLineBreak(null);
  }

  // Scalar streams.
  private void writeSingleQuoted(String text, boolean split) throws IOException {
    writeIndicator("'", true, false, false);
    boolean spaces = false;
    boolean breaks = false;
    int start = 0, end = 0;
    char ch;
    while (end <= text.length()) {
      ch = 0;
      if (end < text.length()) {
        ch = text.charAt(end);
      }
      if (spaces) {
        if (ch == 0 || ch != ' ') {
          if (start + 1 == end && this.column > this.bestWidth && split && start != 0
              && end != text.length()) {
            writeIndent();
          } else {
            int len = end - start;
            this.column += len;
            stream.write(text, start, len);
          }
          start = end;
        }
      } else if (breaks) {
        if (ch == 0 || Constant.LINEBR.hasNo(ch)) {
          if (text.charAt(start) == '\n') {
            writeLineBreak(null);
          }
          String data = text.substring(start, end);
          for (char br : data.toCharArray()) {
            if (br == '\n') {
              writeLineBreak(null);
            } else {
              writeLineBreak(String.valueOf(br));
            }
          }
          writeIndent();
          start = end;
        }
      } else {
        if (Constant.LINEBR.has(ch, "\0 '")) {
          if (start < end) {
            int len = end - start;
            this.column += len;
            stream.write(text, start, len);
            start = end;
          }
        }
      }
      if (ch == '\'') {
        this.column += 2;
        stream.write("''");
        start = end + 1;
      }
      if (ch != 0) {
        spaces = ch == ' ';
        breaks = Constant.LINEBR.has(ch);
      }
      end++;
    }
    writeIndicator("'", false, false, false);
  }

  private void writeDoubleQuoted(String text, boolean split) throws IOException {
    writeIndicator("\"", true, false, false);
    int start = 0;
    int end = 0;
    while (end <= text.length()) {
      Character ch = null;
      if (end < text.length()) {
        ch = text.charAt(end);
      }
      if (ch == null || "\"\\\u0085\u2028\u2029\uFEFF".indexOf(ch) != -1
          || !('\u0020' <= ch && ch <= '\u007E')) {
        if (start < end) {
          int len = end - start;
          this.column += len;
          stream.write(text, start, len);
          start = end;
        }
        if (ch != null) {
          String data;

          if (ESCAPE_REPLACEMENTS.containsKey(ch)) {
            data = "\\" + ESCAPE_REPLACEMENTS.get(ch);
          } else {
            int codePoint;

            if (Character.isHighSurrogate(ch) && end + 1 < text.length()) {
              char ch2 = text.charAt(end + 1);
              codePoint = Character.toCodePoint(ch, ch2);
            } else {
              codePoint = ch;
            }

            if (this.allowUnicode && StreamReader.isPrintable(codePoint)) {
              data = String.valueOf(Character.toChars(codePoint));

              if (Character.charCount(codePoint) == 2) {
                end++;
              }
            } else {
              // if !allowUnicode or the character is not printable,
              // we must encode it
              if (ch <= '\u00FF') {
                String s = "0" + Integer.toString(ch, 16);
                data = "\\x" + s.substring(s.length() - 2);
              } else if (Character.charCount(codePoint) == 2) {
                end++;
                String s = "000" + Long.toHexString(codePoint);
                data = "\\U" + s.substring(s.length() - 8);
              } else {
                String s = "000" + Integer.toString(ch, 16);
                data = "\\u" + s.substring(s.length() - 4);
              }
            }
          }

          this.column += data.length();
          stream.write(data);
          start = end + 1;
        }
      }
      if ((0 < end && end < (text.length() - 1)) && (ch == ' ' || start >= end)
          && (this.column + (end - start)) > this.bestWidth && split) {
        String data;
        if (start >= end) {
          data = "\\";
        } else {
          data = text.substring(start, end) + "\\";
        }
        if (start < end) {
          start = end;
        }
        this.column += data.length();
        stream.write(data);
        writeIndent();
        this.whitespace = false;
        this.indention = false;
        if (text.charAt(start) == ' ') {
          data = "\\";
          this.column += data.length();
          stream.write(data);
        }
      }
      end += 1;
    }
    writeIndicator("\"", false, false, false);
  }

  private boolean writeCommentLines(List<CommentLine> commentLines) throws IOException {
    boolean wroteComment = false;
    if (emitComments) {
      int indentColumns = 0;
      boolean firstComment = true;
      for (CommentLine commentLine : commentLines) {
        if (commentLine.getCommentType() != CommentType.BLANK_LINE) {
          if (firstComment) {
            firstComment = false;
            writeIndicator("#", commentLine.getCommentType() == CommentType.IN_LINE, false, false);
            indentColumns = this.column > 0 ? this.column - 1 : 0;
          } else {
            writeWhitespace(indentColumns);
            writeIndicator("#", false, false, false);
          }
          stream.write(commentLine.getValue());
          writeLineBreak(null);
        } else {
          writeLineBreak(null);
          writeIndent();
        }
        wroteComment = true;
      }
    }
    return wroteComment;
  }

  private void writeBlockComment() throws IOException {
    if (!blockCommentsCollector.isEmpty()) {
      writeIndent();
      writeCommentLines(blockCommentsCollector.consume());
    }
  }

  private boolean writeInlineComments() throws IOException {
    return writeCommentLines(inlineCommentsCollector.consume());
  }

  private String determineBlockHints(String text) {
    StringBuilder hints = new StringBuilder();
    if (Constant.LINEBR.has(text.charAt(0), " ")) {
      hints.append(bestIndent);
    }
    char ch1 = text.charAt(text.length() - 1);
    if (Constant.LINEBR.hasNo(ch1)) {
      hints.append("-");
    } else if (text.length() == 1 || Constant.LINEBR.has(text.charAt(text.length() - 2))) {
      hints.append("+");
    }
    return hints.toString();
  }

  void writeFolded(String text, boolean split) throws IOException {
    String hints = determineBlockHints(text);
    writeIndicator(">" + hints, true, false, false);
    if (hints.length() > 0 && (hints.charAt(hints.length() - 1) == '+')) {
      openEnded = true;
    }
    if (!writeInlineComments()) {
      writeLineBreak(null);
    }
    boolean leadingSpace = true;
    boolean spaces = false;
    boolean breaks = true;
    int start = 0, end = 0;
    while (end <= text.length()) {
      char ch = 0;
      if (end < text.length()) {
        ch = text.charAt(end);
      }
      if (breaks) {
        if (ch == 0 || Constant.LINEBR.hasNo(ch)) {
          if (!leadingSpace && ch != 0 && ch != ' ' && text.charAt(start) == '\n') {
            writeLineBreak(null);
          }
          leadingSpace = ch == ' ';
          String data = text.substring(start, end);
          for (char br : data.toCharArray()) {
            if (br == '\n') {
              writeLineBreak(null);
            } else {
              writeLineBreak(String.valueOf(br));
            }
          }
          if (ch != 0) {
            writeIndent();
          }
          start = end;
        }
      } else if (spaces) {
        if (ch != ' ') {
          if (start + 1 == end && this.column > this.bestWidth && split) {
            writeIndent();
          } else {
            int len = end - start;
            this.column += len;
            stream.write(text, start, len);
          }
          start = end;
        }
      } else {
        if (Constant.LINEBR.has(ch, "\0 ")) {
          int len = end - start;
          this.column += len;
          stream.write(text, start, len);
          if (ch == 0) {
            writeLineBreak(null);
          }
          start = end;
        }
      }
      if (ch != 0) {
        breaks = Constant.LINEBR.has(ch);
        spaces = ch == ' ';
      }
      end++;
    }
  }

  void writeLiteral(String text) throws IOException {
    String hints = determineBlockHints(text);
    writeIndicator("|" + hints, true, false, false);
    if (hints.length() > 0 && (hints.charAt(hints.length() - 1)) == '+') {
      openEnded = true;
    }
    if (!writeInlineComments()) {
      writeLineBreak(null);
    }
    boolean breaks = true;
    int start = 0, end = 0;
    while (end <= text.length()) {
      char ch = 0;
      if (end < text.length()) {
        ch = text.charAt(end);
      }
      if (breaks) {
        if (ch == 0 || Constant.LINEBR.hasNo(ch)) {
          String data = text.substring(start, end);
          for (char br : data.toCharArray()) {
            if (br == '\n') {
              writeLineBreak(null);
            } else {
              writeLineBreak(String.valueOf(br));
            }
          }
          if (ch != 0) {
            writeIndent();
          }
          start = end;
        }
      } else {
        if (ch == 0 || Constant.LINEBR.has(ch)) {
          stream.write(text, start, end - start);
          if (ch == 0) {
            writeLineBreak(null);
          }
          start = end;
        }
      }
      if (ch != 0) {
        breaks = Constant.LINEBR.has(ch);
      }
      end++;
    }
  }

  void writePlain(String text, boolean split) throws IOException {
    if (rootContext) {
      openEnded = true;
    }
    if (text.length() == 0) {
      return;
    }
    if (!this.whitespace) {
      this.column++;
      stream.write(SPACE);
    }
    this.whitespace = false;
    this.indention = false;
    boolean spaces = false;
    boolean breaks = false;
    int start = 0, end = 0;
    while (end <= text.length()) {
      char ch = 0;
      if (end < text.length()) {
        ch = text.charAt(end);
      }
      if (spaces) {
        if (ch != ' ') {
          if (start + 1 == end && this.column > this.bestWidth && split) {
            writeIndent();
            this.whitespace = false;
            this.indention = false;
          } else {
            int len = end - start;
            this.column += len;
            stream.write(text, start, len);
          }
          start = end;
        }
      } else if (breaks) {
        if (Constant.LINEBR.hasNo(ch)) {
          if (text.charAt(start) == '\n') {
            writeLineBreak(null);
          }
          String data = text.substring(start, end);
          for (char br : data.toCharArray()) {
            if (br == '\n') {
              writeLineBreak(null);
            } else {
              writeLineBreak(String.valueOf(br));
            }
          }
          writeIndent();
          this.whitespace = false;
          this.indention = false;
          start = end;
        }
      } else {
        if (Constant.LINEBR.has(ch, "\0 ")) {
          int len = end - start;
          this.column += len;
          stream.write(text, start, len);
          start = end;
        }
      }
      if (ch != 0) {
        spaces = ch == ' ';
        breaks = Constant.LINEBR.has(ch);
      }
      end++;
    }
  }
}