01a13871de
Patch documentation to come Issues with the old system that are fixed now: - World generation does not scale with cpu cores effectively. - Relies on the main thread for scheduling and maintaining chunk state, dropping chunk load/generate rates at lower tps. - Unreliable prioritisation of chunk gen/load calls that block the main thread. - Shutdown logic is utterly unreliable, as it has to wait for all chunks to unload - is it guaranteed that the chunk system is in a state on shutdown that it can reliably do this? Watchdog shutdown also typically failed due to thread checks, which is now resolved. - Saving of data is not unified (i.e can save chunk data without saving entity data, poses problems for desync if shutdown is really abnormal. - Entities are not loaded with chunks. This caused quite a bit of headache for Chunk#getEntities API, but now the new chunk system loads entities with chunks so that they are ready whenever the chunk loads in. Effectively brings the behavior back to 1.16 era, but still storing entities in their own separate regionfiles. The above list is not complete. The patch documentation will complete it. New chunk system hard relies on starlight and dataconverter, and most importantly the new concurrent utilities in ConcurrentUtil. Some of the old async chunk i/o interface (i.e the old file io thread reroutes _some_ calls to the new file io thread) is kept for plugin compat reasons. It will be removed in the next major version of minecraft. The old legacy chunk system patches have been moved to the removed folder in case we need them again.
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++;
|
|
+ }
|
|
+ }
|
|
+}
|