1703 Zeilen
63 KiB
Diff
1703 Zeilen
63 KiB
Diff
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
|
|
From: Jake Potrebic <jake.m.potrebic@gmail.com>
|
|
Date: Sat, 1 Jan 2022 21:24:50 -0800
|
|
Subject: [PATCH] Fix saving configs with more long comments
|
|
|
|
This is a bug with snakeyaml
|
|
PR: https://bitbucket.org/snakeyaml/snakeyaml/pull-requests/3
|
|
Issue: https://bitbucket.org/snakeyaml/snakeyaml/issues/518/comments-could-cause-queue-full
|
|
|
|
Added the entire Emitter class from snakeyaml because
|
|
dev-imports doesn't work with non-mojang-added dependencies
|
|
|
|
Replacement for upstream: https://hub.spigotmc.org/stash/projects/SPIGOT/repos/bukkit/commits/a7505b3cd0498baca152777767f0e4ddebbe4d1a
|
|
|
|
diff --git a/src/main/java/org/yaml/snakeyaml/emitter/Emitter.java b/src/main/java/org/yaml/snakeyaml/emitter/Emitter.java
|
|
new file mode 100644
|
|
index 0000000000000000000000000000000000000000..ca32c00775f3d98abea39bbfeb0268bb9efbc12b
|
|
--- /dev/null
|
|
+++ b/src/main/java/org/yaml/snakeyaml/emitter/Emitter.java
|
|
@@ -0,0 +1,1682 @@
|
|
+/**
|
|
+ * 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 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;
|
|
+
|
|
+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.concurrent.ArrayBlockingQueue;
|
|
+import java.util.regex.Matcher;
|
|
+import java.util.regex.Pattern;
|
|
+
|
|
+/**
|
|
+ * <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 {
|
|
+ public static final int MIN_INDENT = 1;
|
|
+ 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();
|
|
+ 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 final static 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;
|
|
+
|
|
+
|
|
+ public Emitter(Writer stream, DumperOptions opts) {
|
|
+ // 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); // Paper - allow more than 100 events (or comments)
|
|
+ // this.events = new ArrayBlockingQueue<>(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(); // FIXME why without check ???
|
|
+ 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 final static 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;
|
|
+ }
|
|
+
|
|
+ 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;
|
|
+
|
|
+ // 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) {
|
|
+ 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 if (!this.allowUnicode || !StreamReader.isPrintable(ch)) {
|
|
+ // 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 (ch >= '\uD800' && ch <= '\uDBFF') {
|
|
+ if (end + 1 < text.length()) {
|
|
+ Character ch2 = text.charAt(++end);
|
|
+ String s = "000" + Long.toHexString(Character.toCodePoint(ch, ch2));
|
|
+ data = "\\U" + s.substring(s.length() - 8);
|
|
+ } else {
|
|
+ String s = "000" + Integer.toString(ch, 16);
|
|
+ data = "\\u" + s.substring(s.length() - 4);
|
|
+ }
|
|
+ } else {
|
|
+ String s = "000" + Integer.toString(ch, 16);
|
|
+ data = "\\u" + s.substring(s.length() - 4);
|
|
+ }
|
|
+ } else {
|
|
+ data = String.valueOf(ch);
|
|
+ }
|
|
+ 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++;
|
|
+ }
|
|
+ }
|
|
+}
|