diff options
Diffstat (limited to 'src/main/java/org/yaml/snakeyaml/emitter')
4 files changed, 1485 insertions, 0 deletions
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 00000000..36b62e92 --- /dev/null +++ b/src/main/java/org/yaml/snakeyaml/emitter/Emitter.java @@ -0,0 +1,1422 @@ +/* + * See LICENSE file in distribution for copyright and licensing information. + */ +package org.yaml.snakeyaml.emitter; + +import java.io.IOException; +import java.io.Writer; +import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.LinkedList; +import java.util.Map; +import java.util.Queue; +import java.util.Set; +import java.util.TreeSet; +import java.util.regex.Pattern; + +import org.yaml.snakeyaml.DumperOptions; +import org.yaml.snakeyaml.events.AliasEvent; +import org.yaml.snakeyaml.events.CollectionEndEvent; +import org.yaml.snakeyaml.events.CollectionStartEvent; +import org.yaml.snakeyaml.events.DocumentEndEvent; +import org.yaml.snakeyaml.events.DocumentStartEvent; +import org.yaml.snakeyaml.events.Event; +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; + +/** + * <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> + * + * @see <a href="http://pyyaml.org/wiki/PyYAML">PyYAML</a> for more information + */ +public final class Emitter { + private static final Map<Character, String> ESCAPE_REPLACEMENTS = new HashMap<Character, String>(); + public static final int MIN_INDENT = 1; + public static final int MAX_INDENT = 10; + + static { + ESCAPE_REPLACEMENTS.put(new Character('\0'), "0"); + ESCAPE_REPLACEMENTS.put(new Character('\u0007'), "a"); + ESCAPE_REPLACEMENTS.put(new Character('\u0008'), "b"); + ESCAPE_REPLACEMENTS.put(new Character('\u0009'), "t"); + ESCAPE_REPLACEMENTS.put(new Character('\n'), "n"); + ESCAPE_REPLACEMENTS.put(new Character('\u000B'), "v"); + ESCAPE_REPLACEMENTS.put(new Character('\u000C'), "f"); + ESCAPE_REPLACEMENTS.put(new Character('\r'), "r"); + ESCAPE_REPLACEMENTS.put(new Character('\u001B'), "e"); + ESCAPE_REPLACEMENTS.put(new Character('"'), "\""); + ESCAPE_REPLACEMENTS.put(new Character('\\'), "\\"); + ESCAPE_REPLACEMENTS.put(new Character('\u0085'), "N"); + ESCAPE_REPLACEMENTS.put(new Character('\u00A0'), "_"); + ESCAPE_REPLACEMENTS.put(new Character('\u2028'), "L"); + ESCAPE_REPLACEMENTS.put(new Character('\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:yaml.org,2002:", "!!"); + } + // The stream should have the methods `write` and possibly `flush`. + private final Writer stream; + + // Encoding is defined by Writer (cannot be overriden by STREAM-START.) + // private Charset encoding; + + // Emitter is a state machine with a stack of states to handle nested + // structures. + private final LinkedList<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 LinkedList<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; + private int column; + private boolean whitespace; + private boolean indention; + private boolean openEnded; + + // Formatting details. + private Boolean canonical; + private boolean allowUnicode; + private int bestIndent; + private int bestWidth; + private String bestLineBreak; + + // 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 char style = 0; + + 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 LinkedList<EmitterState>(); + this.state = new ExpectStreamStart(); + // Current event and the event queue. + this.events = new LinkedList<Event>(); + this.event = null; + // The current indentation level and the stack of previous indents. + this.indents = new LinkedList<Integer>(); + 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 ':')? + line = 0; + column = 0; + whitespace = true; + indention = true; + + // Whether the document requires an explicit document indicator + openEnded = false; + + // Formatting details. + this.canonical = opts.isCanonical(); + this.allowUnicode = opts.isAllowUnicode(); + this.bestIndent = 2; + if ((opts.getIndent() > MIN_INDENT) && (opts.getIndent() < MAX_INDENT)) { + this.bestIndent = opts.getIndent(); + } + this.bestWidth = 80; + if (opts.getWidth() > this.bestIndent * 2) { + this.bestWidth = opts.getWidth(); + } + this.bestLineBreak = opts.getLineBreak().getString(); + + // 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 = (char) 0; + } + + public void emit(Event event) throws IOException { + this.events.offer(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; + } + Event event = events.peek(); + if (event instanceof DocumentStartEvent) { + return needEvents(1); + } else if (event instanceof SequenceStartEvent) { + return needEvents(2); + } else if (event instanceof MappingStartEvent) { + return needEvents(3); + } else { + return false; + } + } + + private boolean needEvents(int count) { + int level = 0; + Iterator<Event> iter = events.iterator(); + iter.next(); + while (iter.hasNext()) { + Event event = iter.next(); + 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 events.size() < count + 1; + } + + private void increaseIndent(boolean flow, boolean indentless) { + indents.addFirst(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 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 && !checkEmptyDocument(); + if (!implicit) { + writeIndent(); + writeIndicator("---", true, false, false); + if (canonical) { + writeIndent(); + } + } + state = new ExpectDocumentRoot(); + } else if (event instanceof StreamEndEvent) { + // TODO fix 313 PyYAML changeset + // if (openEnded) { + // writeIndicator("...", true, false, false); + // writeIndent(); + // } + writeStreamEnd(); + state = new ExpectNothing(); + } else { + throw new EmitterException("expected DocumentStartEvent, but got " + event); + } + } + } + + private class ExpectDocumentEnd implements EmitterState { + public void expect() throws IOException { + 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 { + states.addFirst(new ExpectDocumentEnd()); + expectNode(true, false, false, false); + } + } + + // Node handlers. + + private void expectNode(boolean root, boolean sequence, 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).getFlowStyle() + || checkEmptySequence()) { + expectFlowSequence(); + } else { + expectBlockSequence(); + } + } else {// MappingStartEvent + if (flowLevel != 0 || canonical || ((MappingStartEvent) event).getFlowStyle() + || checkEmptyMapping()) { + expectFlowMapping(); + } else { + expectBlockMapping(); + } + } + } else { + throw new EmitterException("expected NodeEvent, but got " + event); + } + } + + private void expectAlias() throws IOException { + if (((NodeEvent) event).getAnchor() == null) { + throw new EmitterException("anchor is not specified for alias"); + } + processAnchor("*"); + state = states.removeFirst(); + } + + private void expectScalar() throws IOException { + increaseIndent(true, false); + processScalar(); + indent = indents.removeFirst(); + state = states.removeFirst(); + } + + // Flow sequence handlers. + + private void expectFlowSequence() throws IOException { + writeIndicator("[", true, true, false); + flowLevel++; + increaseIndent(true, false); + state = new ExpectFirstFlowSequenceItem(); + } + + private class ExpectFirstFlowSequenceItem implements EmitterState { + public void expect() throws IOException { + if (event instanceof SequenceEndEvent) { + indent = indents.removeFirst(); + flowLevel--; + writeIndicator("]", false, false, false); + state = states.removeFirst(); + } else { + if (canonical || column > bestWidth) { + writeIndent(); + } + states.addFirst(new ExpectFlowSequenceItem()); + expectNode(false, true, false, false); + } + } + } + + private class ExpectFlowSequenceItem implements EmitterState { + public void expect() throws IOException { + if (event instanceof SequenceEndEvent) { + indent = indents.removeFirst(); + flowLevel--; + if (canonical) { + writeIndicator(",", false, false, false); + writeIndent(); + } + writeIndicator("]", false, false, false); + state = states.removeFirst(); + } else { + writeIndicator(",", false, false, false); + if (canonical || column > bestWidth) { + writeIndent(); + } + states.addFirst(new ExpectFlowSequenceItem()); + expectNode(false, true, false, false); + } + } + } + + // Flow mapping handlers. + + private void expectFlowMapping() throws IOException { + writeIndicator("{", true, true, false); + flowLevel++; + increaseIndent(true, false); + state = new ExpectFirstFlowMappingKey(); + } + + private class ExpectFirstFlowMappingKey implements EmitterState { + public void expect() throws IOException { + if (event instanceof MappingEndEvent) { + indent = indents.removeFirst(); + flowLevel--; + writeIndicator("}", false, false, false); + state = states.removeFirst(); + } else { + if (canonical || column > bestWidth) { + writeIndent(); + } + if (!canonical && checkSimpleKey()) { + states.addFirst(new ExpectFlowMappingSimpleValue()); + expectNode(false, false, true, true); + } else { + writeIndicator("?", true, false, false); + states.addFirst(new ExpectFlowMappingValue()); + expectNode(false, false, true, false); + } + } + } + } + + private class ExpectFlowMappingKey implements EmitterState { + public void expect() throws IOException { + if (event instanceof MappingEndEvent) { + indent = indents.removeFirst(); + flowLevel--; + if (canonical) { + writeIndicator(",", false, false, false); + writeIndent(); + } + writeIndicator("}", false, false, false); + state = states.removeFirst(); + } else { + writeIndicator(",", false, false, false); + if (canonical || column > bestWidth) { + writeIndent(); + } + if (!canonical && checkSimpleKey()) { + states.addFirst(new ExpectFlowMappingSimpleValue()); + expectNode(false, false, true, true); + } else { + writeIndicator("?", true, false, false); + states.addFirst(new ExpectFlowMappingValue()); + expectNode(false, false, true, false); + } + } + } + } + + private class ExpectFlowMappingSimpleValue implements EmitterState { + public void expect() throws IOException { + writeIndicator(":", false, false, false); + states.addFirst(new ExpectFlowMappingKey()); + expectNode(false, false, true, false); + } + } + + private class ExpectFlowMappingValue implements EmitterState { + public void expect() throws IOException { + if (canonical || column > bestWidth) { + writeIndent(); + } + writeIndicator(":", true, false, false); + states.addFirst(new ExpectFlowMappingKey()); + expectNode(false, false, true, false); + } + } + + // 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 boolean first; + + public ExpectBlockSequenceItem(boolean first) { + this.first = first; + } + + public void expect() throws IOException { + if (!this.first && event instanceof SequenceEndEvent) { + indent = indents.removeFirst(); + state = states.removeFirst(); + } else { + writeIndent(); + writeIndicator("-", true, false, true); + states.addFirst(new ExpectBlockSequenceItem(false)); + expectNode(false, true, false, false); + } + } + } + + // 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 boolean first; + + public ExpectBlockMappingKey(boolean first) { + this.first = first; + } + + public void expect() throws IOException { + if (!this.first && event instanceof MappingEndEvent) { + indent = indents.removeFirst(); + state = states.removeFirst(); + } else { + writeIndent(); + if (checkSimpleKey()) { + states.addFirst(new ExpectBlockMappingSimpleValue()); + expectNode(false, false, true, true); + } else { + writeIndicator("?", true, false, true); + states.addFirst(new ExpectBlockMappingValue()); + expectNode(false, false, true, false); + } + } + } + } + + private class ExpectBlockMappingSimpleValue implements EmitterState { + public void expect() throws IOException { + writeIndicator(":", false, false, false); + states.addFirst(new ExpectBlockMappingKey(false)); + expectNode(false, false, true, false); + } + } + + private class ExpectBlockMappingValue implements EmitterState { + public void expect() throws IOException { + writeIndent(); + writeIndicator(":", true, false, true); + states.addFirst(new ExpectBlockMappingKey(false)); + expectNode(false, false, true, false); + } + } + + // 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() == ""); + } else { + 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.scalar.length(); + } + return (length < 128 && (event instanceof AliasEvent + || (event instanceof ScalarEvent && !analysis.empty && !analysis.multiline) + || 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()); + } + if (preparedAnchor != null && !"".equals(preparedAnchor)) { + 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 == 0) { + style = chooseScalarStyle(); + } + if (((!canonical || tag == null) && ((style == 0 && ev.getImplicit()[0]) || (style != 0 && ev + .getImplicit()[1])))) { + preparedTag = null; + return; + } + if (ev.getImplicit()[0] && 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); + } + if (preparedTag != null && !"".equals(preparedTag)) { + writeIndicator(preparedTag, true, false, false); + } + preparedTag = null; + } + + private char chooseScalarStyle() { + ScalarEvent ev = (ScalarEvent) event; + if (analysis == null) { + analysis = analyzeScalar(ev.getValue()); + } + if (ev.getStyle() != null && ev.getStyle() == '"' || this.canonical) { + return '"'; + } + if (ev.getStyle() == null && ev.getImplicit()[0]) { + if (!(simpleKeyContext && (analysis.empty || analysis.multiline)) + && ((flowLevel != 0 && analysis.allowFlowPlain) || (flowLevel == 0 && analysis.allowBlockPlain))) { + return 0; + } + } + if (ev.getStyle() != null && (ev.getStyle() == '|' || ev.getStyle() == '>')) { + if (flowLevel == 0 && !simpleKeyContext && analysis.allowBlock) { + return ev.getStyle(); + } + } + if (ev.getStyle() == null || ev.getStyle() == '\'') { + if (analysis.allowSingleQuoted && !(simpleKeyContext && analysis.multiline)) { + return '\''; + } + } + return '"'; + } + + private void processScalar() throws IOException { + ScalarEvent ev = (ScalarEvent) event; + if (analysis == null) { + analysis = analyzeScalar(ev.getValue()); + } + if (style == 0) { + style = chooseScalarStyle(); + } + boolean split = !simpleKeyContext; + if (style == '"') { + writeDoubleQuoted(analysis.scalar, split); + } else if (style == '\'') { + writeSingleQuoted(analysis.scalar, split); + } else if (style == '>') { + writeFolded(analysis.scalar); + } else if (style == '|') { + writeLiteral(analysis.scalar); + } else { + writePlain(analysis.scalar, split); + } + analysis = null; + style = 0; + } + + // Analyzers. + + private String prepareVersion(Integer[] version) { + Integer major = version[0]; + Integer minor = version[1]; + if (major != 1) { + throw new EmitterException("unsupported YAML version: " + version[0] + "." + version[1]); + } + return major.toString() + "." + minor.toString(); + } + + private final static Pattern HANDLE_FORMAT = Pattern.compile("^![-_\\w]*!$"); + + private String prepareTagHandle(String handle) { + if (handle == null || "".equals(handle)) { + 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 == null || "".equals(prefix)) { + throw new EmitterException("tag prefix must not be empty"); + } + StringBuffer chunks = new StringBuffer(); + int start = 0; + int end = 0; + if (prefix.charAt(0) == '!') { + end = 1; + } + while (end < prefix.length()) { + end++; + } + if (start < end) { + chunks.append(prefix.substring(start, end)); + } + return chunks.toString(); + } + + private String prepareTag(String tag) { + if (tag == null || "".equals(tag)) { + throw new EmitterException("tag must not be empty"); + } + if (tag.equals("!")) { + return tag; + } + String handle = null; + String suffix = tag; + for (String prefix : tagPrefixes.keySet()) { + if (tag.startsWith(prefix) && (prefix.equals("!") || prefix.length() < tag.length())) { + handle = tagPrefixes.get(prefix); + suffix = tag.substring(prefix.length()); + } + } + StringBuffer chunks = new StringBuffer(); + int start = 0; + int end = 0; + while (end < suffix.length()) { + end++; + } + if (start < end) { + chunks.append(suffix.substring(start, end)); + } + String suffixText = chunks.toString(); + if (handle != null) { + return handle + suffixText; + } else { + return "!<" + suffixText + ">"; + } + } + + private final static Pattern ANCHOR_FORMAT = Pattern.compile("^[-_\\w]*$"); + + static String prepareAnchor(String anchor) { + if (anchor == null || "".equals(anchor)) { + throw new EmitterException("anchor must not be empty"); + } + if (!ANCHOR_FORMAT.matcher(anchor).matches()) { + throw new EmitterException("invalid character in the anchor: " + anchor); + } + return anchor; + } + + private ScalarAnalysis analyzeScalar(String scalar) { + // Empty scalar is a special case. + if (scalar == null || "".equals(scalar)) { + return new ScalarAnalysis(scalar, true, false, false, true, 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 || "\0 \t\r\n\u0085\u2029\u2029" + .indexOf(scalar.charAt(1)) != -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()) { + char ch = scalar.charAt(index); + // Check for indicators. + if (index == 0) { + // Leading indicators are special characters. + if ("#,[]{}&*!|>\'\"%@`".indexOf(ch) != -1) { + flowIndicators = true; + blockIndicators = true; + } + if (ch == '?' || ch == ':') { + flowIndicators = true; + if (followedByWhitespace) { + blockIndicators = true; + } + } + if (ch == '-' && followedByWhitespace) { + flowIndicators = true; + blockIndicators = true; + } + } else { + // Some indicators cannot appear within a scalar as well. + if (",?[]{}".indexOf(ch) != -1) { + flowIndicators = true; + } + if (ch == ':') { + flowIndicators = true; + if (followedByWhitespace) { + blockIndicators = true; + } + } + if (ch == '#' && preceededByWhitespace) { + flowIndicators = true; + blockIndicators = true; + } + } + // Check for line breaks, special, and unicode characters. + if (ch == '\n' || ch == '\u0085' || ch == '\u2028' || ch == '\u2029') { + lineBreaks = true; + } + if (!(ch == '\n' || ('\u0020' <= ch && ch <= '\u007E'))) { + if ((ch == '\u0085' || ('\u00A0' <= ch && ch <= '\uD7FF') || ('\uE000' <= ch && ch <= '\uFFFD')) + && (ch != '\uFEFF')) { + // unicode is used + if (!this.allowUnicode) { + specialCharacters = true; + } + } else { + specialCharacters = true; + } + } + // Detect important whitespace combinations. + if (ch == ' ') { + if (index == 0) { + leadingSpace = true; + } + if (index == scalar.length() - 1) { + trailingSpace = true; + } + if (previousBreak) { + breakSpace = true; + } + previousSpace = true; + previousBreak = false; + } else if ("\n\u0085\u2028\u2029".indexOf(ch) != -1) { + 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++; + preceededByWhitespace = "\0 \t\r\n\u0085\u2028\u2029".indexOf(ch) != -1; + followedByWhitespace = (index + 1 >= scalar.length() || "\0 \t\r\n\u0085\u2028\u2029" + .indexOf(scalar.charAt(index + 1)) != -1); + } + // Let's decide what styles are allowed. + boolean allowFlowPlain = true; + boolean allowBlockPlain = true; + boolean allowSingleQuoted = true; + boolean allowDoubleQuoted = 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. + if (lineBreaks) { + allowFlowPlain = allowBlockPlain = 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, allowDoubleQuoted, 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 { + String data = null; + if (this.whitespace || !needWhitespace) { + data = indicator; + } else { + data = " " + indicator; + } + this.whitespace = whitespace; + this.indention = this.indention && indentation; + this.column += data.length(); + openEnded = false; + stream.write(data); + } + + 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); + } + + if (this.column < indent) { + this.whitespace = true; + StringBuffer data = new StringBuffer(); + for (int i = 0; i < indent - this.column; i++) { + data.append(" "); + } + this.column = indent; + stream.write(data.toString()); + } + } + + private void writeLineBreak(String data) throws IOException { + if (data == null) { + data = this.bestLineBreak; + } + this.whitespace = true; + this.indention = true; + this.line++; + this.column = 0; + stream.write(data); + } + + void writeVersionDirective(String versionText) throws IOException { + stream.write("%YAML " + versionText); + writeLineBreak(null); + } + + void writeTagDirective(String handleText, String prefixText) throws IOException { + stream.write("%TAG " + handleText + " " + 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 { + String data = text.substring(start, end); + this.column += data.length(); + stream.write(data); + } + start = end; + } + } else if (breaks) { + if (ch == 0 || "\n\u0085\u2028\u2029".indexOf(ch) == -1) { + 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 (ch == 0 || " \n\u0085\u2028\u2029".indexOf(ch) != -1 || ch == '\'') { + if (start < end) { + String data = text.substring(start, end); + this.column += data.length(); + stream.write(data); + start = end; + } + } + } + if (ch == '\'') { + String data = "''"; + this.column += 2; + stream.write(data); + start = end + 1; + } + if (ch != 0) { + spaces = ch == ' '; + breaks = "\n\u0085\u2028\u2029".indexOf(ch) != -1; + } + 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) { + String data = text.substring(start, end); + this.column += data.length(); + stream.write(data); + start = end; + } + if (ch != null) { + String data; + if (ESCAPE_REPLACEMENTS.containsKey(new Character(ch))) { + data = "\\" + ESCAPE_REPLACEMENTS.get(new Character(ch)); + } else if (ch <= '\u00FF') { + String s = "0" + Integer.toString(ch, 16); + data = "\\x" + s.substring(s.length() - 2); + } 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 String determineBlockHints(String text) { + StringBuffer hints = new StringBuffer(); + if (text != null && text.length() > 0) { + if (" \n\u0085\u2028\u2029".indexOf(text.charAt(0)) != -1) { + hints.append(bestIndent); + } + char ch1 = text.charAt(text.length() - 1); + if ("\n\u0085\u2028\u2029".indexOf(ch1) == -1) { + hints.append("-"); + } else if (text.length() == 1 + || ("\n\u0085\u2028\u2029".indexOf(text.charAt(text.length() - 2)) != -1)) { + hints.append("+"); + } + } + return hints.toString(); + } + + void writeFolded(String text) throws IOException { + String hints = determineBlockHints(text); + writeIndicator(">" + hints, true, false, false); + if (hints.length() > 0 && (hints.charAt(hints.length() - 1) == '+')) { + openEnded = true; + } + 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 || ("\n\0085\u2028\u2029".indexOf(ch) == -1)) { + 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) { + writeIndent(); + } else { + String data = text.substring(start, end); + this.column += data.length(); + stream.write(data); + } + start = end; + } + } else { + if (ch == 0 || " \n\0085\u2028\u2029".indexOf(ch) != -1) { + String data = text.substring(start, end); + stream.write(data); + if (ch == 0) { + writeLineBreak(null); + } + start = end; + } + } + if (ch != 0) { + breaks = ("\n\u0085\u2028\u2029".indexOf(ch) != -1); + 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; + } + 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 || "\n\u0085\u2028\u2029".indexOf(ch) == -1) { + 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 || "\n\u0085\u2028\u2029".indexOf(ch) != -1) { + String data = text.substring(start, end); + stream.write(data); + if (ch == 0) { + writeLineBreak(null); + } + start = end; + } + } + if (ch != 0) { + breaks = ("\n\u0085\u2028\u2029".indexOf(ch) != -1); + } + end++; + } + } + + void writePlain(String text, boolean split) throws IOException { + if (rootContext) { + openEnded = true; + } + if (text == null || "".equals(text)) { + return; + } + if (!this.whitespace) { + String data = " "; + this.column += data.length(); + stream.write(data); + } + 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 { + String data = text.substring(start, end); + this.column += data.length(); + stream.write(data); + } + start = end; + } + } else if (breaks) { + if ("\n\u0085\u2028\u2029".indexOf(ch) == -1) { + 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 (ch == 0 || "\n\u0085\u2028\u2029".indexOf(ch) != -1) { + String data = text.substring(start, end); + this.column += data.length(); + stream.write(data); + start = end; + } + } + if (ch != 0) { + spaces = (ch == ' '); + breaks = ("\n\u0085\u2028\u2029".indexOf(ch) != -1); + } + end++; + } + } +} diff --git a/src/main/java/org/yaml/snakeyaml/emitter/EmitterException.java b/src/main/java/org/yaml/snakeyaml/emitter/EmitterException.java new file mode 100644 index 00000000..b63e0c4e --- /dev/null +++ b/src/main/java/org/yaml/snakeyaml/emitter/EmitterException.java @@ -0,0 +1,17 @@ +/* + * See LICENSE file in distribution for copyright and licensing information. + */ +package org.yaml.snakeyaml.emitter; + +import org.yaml.snakeyaml.error.YAMLException; + +/** + * @see <a href="http://pyyaml.org/wiki/PyYAML">PyYAML</a> for more information + */ +public class EmitterException extends YAMLException { + private static final long serialVersionUID = -8280070025452995908L; + + public EmitterException(String msg) { + super(msg); + } +} diff --git a/src/main/java/org/yaml/snakeyaml/emitter/EmitterState.java b/src/main/java/org/yaml/snakeyaml/emitter/EmitterState.java new file mode 100755 index 00000000..e7667fb5 --- /dev/null +++ b/src/main/java/org/yaml/snakeyaml/emitter/EmitterState.java @@ -0,0 +1,15 @@ +/**
+ * See LICENSE file in distribution for copyright and licensing information.
+ */
+package org.yaml.snakeyaml.emitter;
+
+import java.io.IOException;
+
+/**
+ * Python's methods are first class object. Java needs a class.
+ *
+ * @see <a href="http://pyyaml.org/wiki/PyYAML">PyYAML</a> for more information
+ */
+interface EmitterState {
+ void expect() throws IOException;
+}
\ No newline at end of file diff --git a/src/main/java/org/yaml/snakeyaml/emitter/ScalarAnalysis.java b/src/main/java/org/yaml/snakeyaml/emitter/ScalarAnalysis.java new file mode 100644 index 00000000..61ea599b --- /dev/null +++ b/src/main/java/org/yaml/snakeyaml/emitter/ScalarAnalysis.java @@ -0,0 +1,31 @@ +/**
+ * See LICENSE file in distribution for copyright and licensing information.
+ */
+package org.yaml.snakeyaml.emitter;
+
+/**
+ * @see <a href="http://pyyaml.org/wiki/PyYAML">PyYAML</a> for more information
+ */
+final class ScalarAnalysis {
+ public String scalar;
+ public boolean empty;
+ public boolean multiline;
+ public boolean allowFlowPlain;
+ public boolean allowBlockPlain;
+ public boolean allowSingleQuoted;
+ public boolean allowDoubleQuoted;
+ public boolean allowBlock;
+
+ public ScalarAnalysis(String scalar, boolean empty, boolean multiline, boolean allowFlowPlain,
+ boolean allowBlockPlain, boolean allowSingleQuoted, boolean allowDoubleQuoted,
+ boolean allowBlock) {
+ this.scalar = scalar;
+ this.empty = empty;
+ this.multiline = multiline;
+ this.allowFlowPlain = allowFlowPlain;
+ this.allowBlockPlain = allowBlockPlain;
+ this.allowSingleQuoted = allowSingleQuoted;
+ this.allowDoubleQuoted = allowDoubleQuoted;
+ this.allowBlock = allowBlock;
+ }
+}
\ No newline at end of file |