org.snakeyaml.engine.v2.emitter.Emitter Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of snakeyaml-engine Show documentation
Show all versions of snakeyaml-engine Show documentation
Core YAML 1.2 parser and emitter for Java
The newest version!
/*
* Copyright (c) 2018, 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.snakeyaml.engine.v2.emitter;
import java.util.ArrayDeque;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Queue;
import java.util.Set;
import java.util.TreeSet;
import java.util.regex.Pattern;
import org.snakeyaml.engine.v2.api.DumpSettings;
import org.snakeyaml.engine.v2.api.StreamDataWriter;
import org.snakeyaml.engine.v2.comments.CommentEventsCollector;
import org.snakeyaml.engine.v2.comments.CommentLine;
import org.snakeyaml.engine.v2.comments.CommentType;
import org.snakeyaml.engine.v2.common.Anchor;
import org.snakeyaml.engine.v2.common.ArrayStack;
import org.snakeyaml.engine.v2.common.CharConstants;
import org.snakeyaml.engine.v2.common.ScalarStyle;
import org.snakeyaml.engine.v2.common.SpecVersion;
import org.snakeyaml.engine.v2.events.AliasEvent;
import org.snakeyaml.engine.v2.events.CollectionEndEvent;
import org.snakeyaml.engine.v2.events.CollectionStartEvent;
import org.snakeyaml.engine.v2.events.CommentEvent;
import org.snakeyaml.engine.v2.events.DocumentEndEvent;
import org.snakeyaml.engine.v2.events.DocumentStartEvent;
import org.snakeyaml.engine.v2.events.Event;
import org.snakeyaml.engine.v2.events.MappingStartEvent;
import org.snakeyaml.engine.v2.events.NodeEvent;
import org.snakeyaml.engine.v2.events.ScalarEvent;
import org.snakeyaml.engine.v2.events.SequenceStartEvent;
import org.snakeyaml.engine.v2.events.StreamEndEvent;
import org.snakeyaml.engine.v2.events.StreamStartEvent;
import org.snakeyaml.engine.v2.exceptions.EmitterException;
import org.snakeyaml.engine.v2.exceptions.YamlEngineException;
import org.snakeyaml.engine.v2.nodes.Tag;
import org.snakeyaml.engine.v2.scanner.StreamReader;
/**
*
* 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
*
*/
public final class Emitter implements Emitable {
private static final Map ESCAPE_REPLACEMENTS = new HashMap<>();
/**
* indent cannot be zero spaces
*/
public static final int MIN_INDENT = 1;
/**
* indent should not be more than 10 spaces
*/
public static final int MAX_INDENT = 10;
private static final String SPACE = " ";
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', "_");
}
private static final Map DEFAULT_TAG_PREFIXES = new LinkedHashMap<>();
static {
DEFAULT_TAG_PREFIXES.put("!", "!");
DEFAULT_TAG_PREFIXES.put(Tag.PREFIX, "!!");
}
private final StreamDataWriter stream;
// Emitter is a state machine with a stack of states to handle nested structures.
private final ArrayStack states;
private EmitterState state; // current state
// Current event and the event queue.
private final Queue events;
private Event event;
// The current indentation level and the stack of previous indents.
private final ArrayStack indents;
private Integer indent; // can be null to choose the best
// 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 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 multiLineFlow;
private final boolean allowUnicode;
private int bestIndent;
private final int indicatorIndent;
private final boolean indentWithIndicator;
private int bestWidth;
private final String bestLineBreak;
private final boolean splitLines;
private final int maxSimpleKeyLength;
private final boolean emitComments;
// Tag prefixes.
private Map tagPrefixes;
// Prepared anchor and tag.
private Optional preparedAnchor;
private String preparedTag;
// Scalar analysis and style.
private ScalarAnalysis analysis;
private ScalarStyle scalarStyle;
// Comment processing
private final CommentEventsCollector blockCommentsCollector;
private final CommentEventsCollector inlineCommentsCollector;
/**
* Create
*
* @param opts - configuration options
* @param stream - output stream
*/
public Emitter(DumpSettings opts, StreamDataWriter stream) {
this.stream = stream;
// Emitter is a state machine with a stack of states to handle nested
// structures.
this.states = new ArrayStack<>(100);
this.state = new ExpectStreamStart();
// Current event and the event queue.
this.events = new ArrayDeque<>(100);
this.event = null;
// The current indentation level and the stack of previous indents.
this.indents = new ArrayStack<>(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.multiLineFlow = opts.isMultiLineFlow();
this.allowUnicode = opts.isUseUnicodeEncoding();
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.getBestLineBreak();
this.splitLines = opts.isSplitLines();
this.maxSimpleKeyLength = opts.getMaxSimpleKeyLength();
this.emitComments = opts.getDumpComments();
// Tag prefixes.
this.tagPrefixes = new LinkedHashMap<>();
// Prepared anchor and tag.
this.preparedAnchor = Optional.empty();
this.preparedTag = null;
// Scalar analysis and style.
this.analysis = null;
this.scalarStyle = 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) {
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 iter = events.iterator();
Event event = iter.next();
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) {
// To collect any comment events
return needEvents(iter, 1);
}
return false;
}
private boolean needEvents(Iterator 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 isFlow, boolean indentless) {
indents.push(indent);
if (indent == null) {
if (isFlow) {
indent = bestIndent;
} else {
indent = 0;
}
} else if (!indentless) {
this.indent += bestIndent;
}
}
// States
// Stream handlers.
private class ExpectStreamStart implements EmitterState {
public void expect() {
if (event.getEventId() == Event.ID.StreamStart) {
writeStreamStart();
state = new ExpectFirstDocumentStart();
} else {
throw new EmitterException("expected StreamStartEvent, but got " + event);
}
}
}
private class ExpectNothing implements EmitterState {
public void expect() {
throw new EmitterException("expecting nothing, but got " + event);
}
}
// Document handlers.
private class ExpectFirstDocumentStart implements EmitterState {
public void expect() {
new ExpectDocumentStart(true).expect();
}
}
private class ExpectDocumentStart implements EmitterState {
private final boolean first;
public ExpectDocumentStart(boolean first) {
this.first = first;
}
public void expect() {
if (event.getEventId() == Event.ID.DocumentStart) {
DocumentStartEvent ev = (DocumentStartEvent) event;
handleDocumentStartEvent(ev);
state = new ExpectDocumentRoot();
} else if (event.getEventId() == Event.ID.StreamEnd) {
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 void handleDocumentStartEvent(DocumentStartEvent ev) {
if ((ev.getSpecVersion().isPresent() || !ev.getTags().isEmpty()) && openEnded) {
writeIndicator("...", true, false, false);
writeIndent();
}
ev.getSpecVersion().ifPresent(version -> writeVersionDirective(prepareVersion(version)));
tagPrefixes = new LinkedHashMap<>(DEFAULT_TAG_PREFIXES);
if (!ev.getTags().isEmpty()) {
handleTagDirectives(ev.getTags());
}
boolean implicit = first && !ev.isExplicit() && !canonical && !ev.getSpecVersion().isPresent()
&& (ev.getTags().isEmpty()) && !checkEmptyDocument();
if (!implicit) {
writeIndent();
writeIndicator("---", true, false, false);
if (canonical) {
writeIndent();
}
}
}
private void handleTagDirectives(Map tags) {
Set handles = new TreeSet<>(tags.keySet());
for (String handle : handles) {
String prefix = tags.get(handle);
tagPrefixes.put(prefix, handle);
String handleText = prepareTagHandle(handle);
String prefixText = prepareTagPrefix(prefix);
writeTagDirective(handleText, prefixText);
}
}
private boolean checkEmptyDocument() {
if (event.getEventId() != Event.ID.DocumentStart || events.isEmpty()) {
return false;
}
Event nextEvent = events.peek();
if (nextEvent.getEventId() == Event.ID.Scalar) {
ScalarEvent e = (ScalarEvent) nextEvent;
return !e.getAnchor().isPresent() && !e.getTag().isPresent() && e.getImplicit() != null
&& e.getValue().isEmpty();
}
return false;
}
}
private class ExpectDocumentEnd implements EmitterState {
public void expect() {
event = blockCommentsCollector.collectEventsAndPoll(event);
writeBlockComment();
if (event.getEventId() == Event.ID.DocumentEnd) {
writeIndent();
if (((DocumentEndEvent) event).isExplicit()) {
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() {
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) {
rootContext = root;
mappingContext = mapping;
simpleKeyContext = simpleKey;
if (event.getEventId() == Event.ID.Alias) {
expectAlias();
} else if (event.getEventId() == Event.ID.Scalar || event.getEventId() == Event.ID.SequenceStart
|| event.getEventId() == Event.ID.MappingStart) {
processAnchor("&");
processTag();
handleNodeEvent(event.getEventId());
} else {
throw new EmitterException("expected NodeEvent, but got " + event.getEventId());
}
}
private void handleNodeEvent(Event.ID id) {
switch (id) {
case Scalar:
expectScalar();
break;
case SequenceStart:
if (flowLevel != 0 || canonical || ((SequenceStartEvent) event).isFlow()
|| checkEmptySequence()) {
expectFlowSequence();
} else {
expectBlockSequence();
}
break;
case MappingStart:
if (flowLevel != 0 || canonical || ((MappingStartEvent) event).isFlow()
|| checkEmptyMapping()) {
expectFlowMapping();
} else {
expectBlockMapping();
}
break;
default:
throw new IllegalStateException();
}
}
private void expectAlias() {
if (event instanceof AliasEvent) {
processAnchor("*");
state = states.pop();
} else {
throw new EmitterException("Expecting Alias.");
}
}
private void expectScalar() {
increaseIndent(true, false);
processScalar();
indent = indents.pop();
state = states.pop();
}
// Flow sequence handlers.
private void expectFlowSequence() {
writeIndicator("[", true, true, false);
flowLevel++;
increaseIndent(true, false);
if (multiLineFlow) {
writeIndent();
}
state = new ExpectFirstFlowSequenceItem();
}
private class ExpectFirstFlowSequenceItem implements EmitterState {
public void expect() {
if (event.getEventId() == Event.ID.SequenceEnd) {
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) || multiLineFlow) {
writeIndent();
}
states.push(new ExpectFlowSequenceItem());
expectNode(false, false, false);
event = inlineCommentsCollector.collectEvents(event);
writeInlineComments();
}
}
}
private class ExpectFlowSequenceItem implements EmitterState {
public void expect() {
if (event.getEventId() == Event.ID.SequenceEnd) {
indent = indents.pop();
flowLevel--;
if (canonical) {
writeIndicator(",", false, false, false);
writeIndent();
} else if (multiLineFlow) {
writeIndent();
}
writeIndicator("]", false, false, false);
inlineCommentsCollector.collectEvents();
writeInlineComments();
if (multiLineFlow) {
writeIndent();
}
state = states.pop();
} else if (event instanceof CommentEvent) {
event = blockCommentsCollector.collectEvents(event);
} else {
writeIndicator(",", false, false, false);
writeBlockComment();
if (canonical || (column > bestWidth && splitLines) || multiLineFlow) {
writeIndent();
}
states.push(new ExpectFlowSequenceItem());
expectNode(false, false, false);
event = inlineCommentsCollector.collectEvents(event);
writeInlineComments();
}
}
}
// Flow mapping handlers.
private void expectFlowMapping() {
writeIndicator("{", true, true, false);
flowLevel++;
increaseIndent(true, false);
if (multiLineFlow) {
writeIndent();
}
state = new ExpectFirstFlowMappingKey();
}
private class ExpectFirstFlowMappingKey implements EmitterState {
public void expect() {
event = blockCommentsCollector.collectEventsAndPoll(event);
writeBlockComment();
if (event.getEventId() == Event.ID.MappingEnd) {
indent = indents.pop();
flowLevel--;
writeIndicator("}", false, false, false);
inlineCommentsCollector.collectEvents();
writeInlineComments();
state = states.pop();
} else {
if (canonical || (column > bestWidth && splitLines) || multiLineFlow) {
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() {
if (event.getEventId() == Event.ID.MappingEnd) {
indent = indents.pop();
flowLevel--;
if (canonical) {
writeIndicator(",", false, false, false);
writeIndent();
}
if (multiLineFlow) {
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) || multiLineFlow) {
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() {
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() {
if (canonical || (column > bestWidth) || multiLineFlow) {
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() {
boolean indentless = mappingContext && !indention;
increaseIndent(false, indentless);
state = new ExpectFirstBlockSequenceItem();
}
private class ExpectFirstBlockSequenceItem implements EmitterState {
public void expect() {
new ExpectBlockSequenceItem(true).expect();
}
}
private class ExpectBlockSequenceItem implements EmitterState {
private final boolean first;
public ExpectBlockSequenceItem(boolean first) {
this.first = first;
}
public void expect() {
if (!this.first && event.getEventId() == Event.ID.SequenceEnd) {
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() {
increaseIndent(false, false);
state = new ExpectFirstBlockMappingKey();
}
private class ExpectFirstBlockMappingKey implements EmitterState {
public void expect() {
new ExpectBlockMappingKey(true).expect();
}
}
private class ExpectBlockMappingKey implements EmitterState {
private final boolean first;
public ExpectBlockMappingKey(boolean first) {
this.first = first;
}
public void expect() {
event = blockCommentsCollector.collectEventsAndPoll(event);
writeBlockComment();
if (!this.first && event.getEventId() == Event.ID.MappingEnd) {
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.getEventId() != Event.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() {
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() {
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.getEventId() == Event.ID.SequenceStart && !events.isEmpty()
&& events.peek().getEventId() == Event.ID.SequenceEnd;
}
private boolean checkEmptyMapping() {
return event.getEventId() == Event.ID.MappingStart && !events.isEmpty()
&& events.peek().getEventId() == Event.ID.MappingEnd;
}
private boolean checkSimpleKey() {
int length = 0;
if (event instanceof NodeEvent) {
Optional anchorOpt = ((NodeEvent) event).getAnchor();
if (anchorOpt.isPresent()) {
if (!preparedAnchor.isPresent()) {
preparedAnchor = anchorOpt;
}
length += anchorOpt.get().getValue().length();
}
}
Optional tag = Optional.empty();
if (event.getEventId() == Event.ID.Scalar) {
tag = ((ScalarEvent) event).getTag();
} else if (event instanceof CollectionStartEvent) {
tag = ((CollectionStartEvent) event).getTag();
}
if (tag.isPresent()) {
if (preparedTag == null) {
preparedTag = prepareTag(tag.get());
}
length += preparedTag.length();
}
if (event.getEventId() == Event.ID.Scalar) {
if (analysis == null) {
analysis = analyzeScalar(((ScalarEvent) event).getValue());
}
length += analysis.getScalar().length();
}
return length < maxSimpleKeyLength && (event.getEventId() == Event.ID.Alias
|| (event.getEventId() == Event.ID.Scalar && !analysis.isEmpty() && !analysis.isMultiline())
|| checkEmptySequence() || checkEmptyMapping());
}
// Anchor, Tag, and Scalar processors.
private void processAnchor(String indicator) {
NodeEvent ev = (NodeEvent) event;
Optional anchorOption = ev.getAnchor();
if (anchorOption.isPresent()) {
Anchor anchor = anchorOption.get();
if (!preparedAnchor.isPresent()) {
preparedAnchor = anchorOption;
}
writeIndicator(indicator + anchor, true, false, false);
}
preparedAnchor = Optional.empty();
}
/**
* Emit the tag for the current event
*/
private void processTag() {
Optional tag;
if (event.getEventId() == Event.ID.Scalar) {
ScalarEvent ev = (ScalarEvent) event;
tag = ev.getTag();
if (scalarStyle == null) {
scalarStyle = chooseScalarStyle(ev);
}
// check when no tag is required
if ((!canonical || !tag.isPresent()) && ((scalarStyle == ScalarStyle.PLAIN
&& ev.getImplicit().canOmitTagInPlainScalar())
|| (scalarStyle != ScalarStyle.PLAIN && ev.getImplicit().canOmitTagInNonPlainScalar()))) {
preparedTag = null;
return; // no tag required
}
if (ev.getImplicit().canOmitTagInPlainScalar() && !tag.isPresent()) {
tag = Optional.of("!");
preparedTag = null;
}
} else {
CollectionStartEvent ev = (CollectionStartEvent) event;
tag = ev.getTag();
if ((!canonical || !tag.isPresent()) && ev.isImplicit()) {
preparedTag = null;
return; // no tag required
}
}
if (!tag.isPresent()) {
throw new EmitterException("tag is not specified");
}
if (preparedTag == null) {
preparedTag = prepareTag(tag.get());
}
writeIndicator(preparedTag, true, false, false);
preparedTag = null;
}
/**
* Choose the scalar style based on the contents of the scalar and scalar style chosen by
* Representer
*
* @return ScalarStyle to apply for this scala event
*/
private ScalarStyle chooseScalarStyle(ScalarEvent ev) {
if (analysis == null) {
analysis = analyzeScalar(ev.getValue());
}
if (!ev.isPlain() && ev.isDQuoted() || this.canonical) {
return ScalarStyle.DOUBLE_QUOTED;
}
if (ev.isJson() && Optional.of(Tag.STR.getValue()).equals(ev.getTag())) {
// special case for strings which are always double-quoted in JSON
return ScalarStyle.DOUBLE_QUOTED;
}
if ((ev.isPlain() || ev.isJson()) && ev.getImplicit().canOmitTagInPlainScalar()) {
if (!(simpleKeyContext && (analysis.isEmpty() || analysis.isMultiline()))
&& ((flowLevel != 0 && analysis.isAllowFlowPlain())
|| (flowLevel == 0 && analysis.isAllowBlockPlain()))) {
return ScalarStyle.PLAIN;
}
}
if (ev.isLiteral() || ev.isFolded()) {
if (flowLevel == 0 && !simpleKeyContext && analysis.isAllowBlock()) {
return ev.getScalarStyle();
}
}
if (ev.isPlain() || ev.isSQuoted()) {
if (analysis.isAllowSingleQuoted() && !(simpleKeyContext && analysis.isMultiline())) {
return ScalarStyle.SINGLE_QUOTED;
}
}
return ScalarStyle.DOUBLE_QUOTED;
}
private void processScalar() {
ScalarEvent ev = (ScalarEvent) event;
if (analysis == null) {
analysis = analyzeScalar(ev.getValue());
}
boolean split = !simpleKeyContext && splitLines;
switch (scalarStyle) {
case PLAIN:
writePlain(analysis.getScalar(), split);
break;
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 YamlEngineException("Unexpected scalarStyle: " + scalarStyle);
}
// reset scalar style for another scalar
analysis = null;
scalarStyle = null;
}
// Analyzers.
private String prepareVersion(SpecVersion version) {
if (version.getMajor() != 1) {
throw new EmitterException("unsupported YAML version: " + version);
}
return version.getRepresentation();
}
private static final Pattern HANDLE_FORMAT = Pattern.compile("^![-_\\w]*!$");
private String prepareTagHandle(String handle) {
if (handle.isEmpty()) {
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.isEmpty()) {
throw new EmitterException("tag prefix must not be empty");
}
StringBuilder chunks = new StringBuilder();
int end = 0;
if (prefix.charAt(0) == '!') {
end = 1;
}
while (end < prefix.length()) {
end++;
}
chunks.append(prefix, 0, end);
return chunks.toString();
}
/**
* Detect whether the tag starts with a standard handle and add ! when it does not
*
* @param tag - raw (complete tag)
* @return formatted tag ready to emit
*/
private String prepareTag(String tag) {
if (tag.isEmpty()) {
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 starts with prefix and contains more than just prefix
if (tag.startsWith(prefix) && ("!".equals(prefix) || prefix.length() < tag.length())) {
handle = prefix;
}
}
if (handle != null) {
suffix = tag.substring(handle.length());
handle = tagPrefixes.get(handle);
}
if (handle != null) {
return handle + suffix;
} else {
return "!<" + suffix + ">";
}
}
private ScalarAnalysis analyzeScalar(String scalar) {
// Empty scalar is a special case.
if (scalar.isEmpty()) {
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 || CharConstants.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 = CharConstants.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 = CharConstants.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 =
(CharConstants.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() {
stream.flush();
}
void writeStreamStart() {
// BOM is written by Writer.
}
void writeStreamEnd() {
flushStream();
}
void writeIndicator(String indicator, boolean needWhitespace, boolean whitespace,
boolean indentation) {
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);
}
int writeIndent() {
final int indentToWrite;
if (this.indent != null) {
indentToWrite = this.indent;
} else {
indentToWrite = 0;
}
if (!this.indention || this.column > indentToWrite
|| (this.column == indentToWrite && !this.whitespace)) {
writeLineBreak(null);
}
int whitespaces = indentToWrite - this.column;
writeWhitespace(whitespaces);
return whitespaces;
}
private void writeWhitespace(int length) {
if (length <= 0) {
return;
}
this.whitespace = true;
for (int i = 0; i < length; i++) {
stream.write(" ");
}
this.column += length;
}
private void writeLineBreak(String data) {
this.whitespace = true;
this.indention = true;
this.column = 0;
if (data == null) {
stream.write(this.bestLineBreak);
} else {
stream.write(data);
}
}
void writeVersionDirective(String versionText) {
stream.write("%YAML ");
stream.write(versionText);
writeLineBreak(null);
}
void writeTagDirective(String handleText, String prefixText) {
// 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) {
writeIndicator("'", true, false, false);
boolean spaces = false;
boolean breaks = false;
int start = 0;
int end = 0;
char ch;
while (end <= text.length()) {
ch = 0;
if (end < text.length()) {
ch = text.charAt(end);
}
if (spaces) {
if (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 || CharConstants.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 (CharConstants.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 = CharConstants.LINEBR.has(ch);
}
end++;
}
writeIndicator("'", false, false, false);
}
private void writeDoubleQuoted(String text, boolean split) {
writeIndicator("\"", true, false, false);
int start = 0;
int end = 0;
while (end <= text.length()) {
Character ch = null;
if (end < text.length()) {
ch = text.charAt(end);
}
if (ch == null || "\"\\\u0085\u2028\u2029\uFEFF".indexOf(ch) != -1
|| !('\u0020' <= ch && ch <= '\u007E')) {
if (start < end) {
int len = end - start;
this.column += len;
stream.write(text, start, len);
start = end;
}
if (ch != null) {
String data;
if (ESCAPE_REPLACEMENTS.containsKey(ch)) {
data = "\\" + ESCAPE_REPLACEMENTS.get(ch);
} else {
int codePoint;
if (Character.isHighSurrogate(ch) && end + 1 < text.length()) {
char ch2 = text.charAt(end + 1);
codePoint = Character.toCodePoint(ch, ch2);
} else {
codePoint = ch;
}
if (this.allowUnicode && StreamReader.isPrintable(codePoint)) {
data = String.valueOf(Character.toChars(codePoint));
if (Character.charCount(codePoint) == 2) {
end++;
}
} else {
// if !allowUnicode or the character is not printable,
// we must encode it
if (ch <= '\u00FF') {
String s = "0" + Integer.toString(ch, 16);
data = "\\x" + s.substring(s.length() - 2);
} else if (Character.charCount(codePoint) == 2) {
end++;
String s = "000" + Long.toHexString(codePoint);
data = "\\U" + s.substring(s.length() - 8);
} else {
String s = "000" + Integer.toString(ch, 16);
data = "\\u" + s.substring(s.length() - 4);
}
}
}
this.column += data.length();
stream.write(data);
start = end + 1;
}
}
if ((0 < end && end < (text.length() - 1)) && (ch == ' ' || start >= end)
&& (this.column + (end - start)) > this.bestWidth && split) {
String data;
if (start >= end) {
data = "\\";
} else {
data = text.substring(start, end) + "\\";
}
if (start < end) {
start = end;
}
this.column += data.length();
stream.write(data);
writeIndent();
this.whitespace = false;
this.indention = false;
if (text.charAt(start) == ' ') {
data = "\\";
this.column += data.length();
stream.write(data);
}
}
end += 1;
}
writeIndicator("\"", false, false, false);
}
private boolean writeCommentLines(List commentLines) {
boolean wroteComment = false;
if (emitComments) {
int indentColumns = 0, prevColumns = 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 - prevColumns);
writeIndicator("#", false, false, false);
}
stream.write(commentLine.getValue());
writeLineBreak(null);
prevColumns = 0;
} else {
writeLineBreak(null);
prevColumns = writeIndent();
}
wroteComment = true;
}
}
return wroteComment;
}
private void writeBlockComment() {
if (!blockCommentsCollector.isEmpty()) {
writeIndent();
writeCommentLines(blockCommentsCollector.consume());
}
}
private boolean writeInlineComments() {
return writeCommentLines(inlineCommentsCollector.consume());
}
private String determineBlockHints(String text) {
StringBuilder hints = new StringBuilder();
if (CharConstants.LINEBR.has(text.charAt(0), " ")) {
hints.append(bestIndent);
}
char ch1 = text.charAt(text.length() - 1);
if (CharConstants.LINEBR.hasNo(ch1)) {
hints.append("-");
} else if (text.length() == 1 || CharConstants.LINEBR.has(text.charAt(text.length() - 2))) {
hints.append("+");
}
return hints.toString();
}
void writeFolded(String text, boolean split) {
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;
int end = 0;
while (end <= text.length()) {
char ch = 0;
if (end < text.length()) {
ch = text.charAt(end);
}
if (breaks) {
if (ch == 0 || CharConstants.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 (CharConstants.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 = CharConstants.LINEBR.has(ch);
spaces = ch == ' ';
}
end++;
}
}
void writeLiteral(String text) {
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;
int end = 0;
while (end <= text.length()) {
char ch = 0;
if (end < text.length()) {
ch = text.charAt(end);
}
if (breaks) {
if (ch == 0 || CharConstants.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 || CharConstants.LINEBR.has(ch)) {
stream.write(text, start, end - start);
if (ch == 0) {
writeLineBreak(null);
}
start = end;
}
}
if (ch != 0) {
breaks = CharConstants.LINEBR.has(ch);
}
end++;
}
}
void writePlain(String text, boolean split) {
if (rootContext) {
openEnded = true;
}
if (text.isEmpty()) {
return;
}
if (!this.whitespace) {
this.column++;
stream.write(SPACE);
}
this.whitespace = false;
this.indention = false;
boolean spaces = false;
boolean breaks = false;
int start = 0;
int 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 (CharConstants.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 (CharConstants.LINEBR.has(ch, "\0 ")) {
int len = end - start;
this.column += len;
stream.write(text, start, len);
start = end;
}
}
if (ch != 0) {
spaces = ch == ' ';
breaks = CharConstants.LINEBR.has(ch);
}
end++;
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy