1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 package org.yaml.snakeyaml.emitter;
17
18 import java.io.IOException;
19 import java.io.Writer;
20 import java.util.HashMap;
21 import java.util.Iterator;
22 import java.util.LinkedHashMap;
23 import java.util.Map;
24 import java.util.Queue;
25 import java.util.Set;
26 import java.util.TreeSet;
27 import java.util.concurrent.ArrayBlockingQueue;
28 import java.util.regex.Pattern;
29
30 import org.yaml.snakeyaml.DumperOptions;
31 import org.yaml.snakeyaml.DumperOptions.ScalarStyle;
32 import org.yaml.snakeyaml.DumperOptions.Version;
33 import org.yaml.snakeyaml.error.YAMLException;
34 import org.yaml.snakeyaml.events.AliasEvent;
35 import org.yaml.snakeyaml.events.CollectionEndEvent;
36 import org.yaml.snakeyaml.events.CollectionStartEvent;
37 import org.yaml.snakeyaml.events.DocumentEndEvent;
38 import org.yaml.snakeyaml.events.DocumentStartEvent;
39 import org.yaml.snakeyaml.events.Event;
40 import org.yaml.snakeyaml.events.MappingEndEvent;
41 import org.yaml.snakeyaml.events.MappingStartEvent;
42 import org.yaml.snakeyaml.events.NodeEvent;
43 import org.yaml.snakeyaml.events.ScalarEvent;
44 import org.yaml.snakeyaml.events.SequenceEndEvent;
45 import org.yaml.snakeyaml.events.SequenceStartEvent;
46 import org.yaml.snakeyaml.events.StreamEndEvent;
47 import org.yaml.snakeyaml.events.StreamStartEvent;
48 import org.yaml.snakeyaml.nodes.Tag;
49 import org.yaml.snakeyaml.reader.StreamReader;
50 import org.yaml.snakeyaml.scanner.Constant;
51 import org.yaml.snakeyaml.util.ArrayStack;
52
53
54
55
56
57
58
59
60
61
62
63 public final class Emitter implements Emitable {
64 private static final Map<Character, String> ESCAPE_REPLACEMENTS = new HashMap<Character, String>();
65 public static final int MIN_INDENT = 1;
66 public static final int MAX_INDENT = 10;
67
68 private static final char[] SPACE = { ' ' };
69
70 static {
71 ESCAPE_REPLACEMENTS.put('\0', "0");
72 ESCAPE_REPLACEMENTS.put('\u0007', "a");
73 ESCAPE_REPLACEMENTS.put('\u0008', "b");
74 ESCAPE_REPLACEMENTS.put('\u0009', "t");
75 ESCAPE_REPLACEMENTS.put('\n', "n");
76 ESCAPE_REPLACEMENTS.put('\u000B', "v");
77 ESCAPE_REPLACEMENTS.put('\u000C', "f");
78 ESCAPE_REPLACEMENTS.put('\r', "r");
79 ESCAPE_REPLACEMENTS.put('\u001B', "e");
80 ESCAPE_REPLACEMENTS.put('"', "\"");
81 ESCAPE_REPLACEMENTS.put('\\', "\\");
82 ESCAPE_REPLACEMENTS.put('\u0085', "N");
83 ESCAPE_REPLACEMENTS.put('\u00A0', "_");
84 ESCAPE_REPLACEMENTS.put('\u2028', "L");
85 ESCAPE_REPLACEMENTS.put('\u2029', "P");
86 }
87
88 private final static Map<String, String> DEFAULT_TAG_PREFIXES = new LinkedHashMap<String, String>();
89 static {
90 DEFAULT_TAG_PREFIXES.put("!", "!");
91 DEFAULT_TAG_PREFIXES.put(Tag.PREFIX, "!!");
92 }
93
94 private final Writer stream;
95
96
97
98
99
100
101 private final ArrayStack<EmitterState> states;
102 private EmitterState state;
103
104
105 private final Queue<Event> events;
106 private Event event;
107
108
109 private final ArrayStack<Integer> indents;
110 private Integer indent;
111
112
113 private int flowLevel;
114
115
116 private boolean rootContext;
117 private boolean mappingContext;
118 private boolean simpleKeyContext;
119
120
121
122
123
124
125
126
127 private int column;
128 private boolean whitespace;
129 private boolean indention;
130 private boolean openEnded;
131
132
133 private Boolean canonical;
134
135 private Boolean prettyFlow;
136
137 private boolean allowUnicode;
138 private int bestIndent;
139 private int bestWidth;
140 private char[] bestLineBreak;
141
142
143 private Map<String, String> tagPrefixes;
144
145
146 private String preparedAnchor;
147 private String preparedTag;
148
149
150 private ScalarAnalysis analysis;
151 private Character style;
152 private DumperOptions options;
153
154 public Emitter(Writer stream, DumperOptions opts) {
155
156 this.stream = stream;
157
158
159 this.states = new ArrayStack<EmitterState>(100);
160 this.state = new ExpectStreamStart();
161
162 this.events = new ArrayBlockingQueue<Event>(100);
163 this.event = null;
164
165 this.indents = new ArrayStack<Integer>(10);
166 this.indent = null;
167
168 this.flowLevel = 0;
169
170 mappingContext = false;
171 simpleKeyContext = false;
172
173
174
175
176
177
178
179 column = 0;
180 whitespace = true;
181 indention = true;
182
183
184 openEnded = false;
185
186
187 this.canonical = opts.isCanonical();
188 this.prettyFlow = opts.isPrettyFlow();
189 this.allowUnicode = opts.isAllowUnicode();
190 this.bestIndent = 2;
191 if ((opts.getIndent() > MIN_INDENT) && (opts.getIndent() < MAX_INDENT)) {
192 this.bestIndent = opts.getIndent();
193 }
194 this.bestWidth = 80;
195 if (opts.getWidth() > this.bestIndent * 2) {
196 this.bestWidth = opts.getWidth();
197 }
198 this.bestLineBreak = opts.getLineBreak().getString().toCharArray();
199
200
201 this.tagPrefixes = new LinkedHashMap<String, String>();
202
203
204 this.preparedAnchor = null;
205 this.preparedTag = null;
206
207
208 this.analysis = null;
209 this.style = null;
210 this.options = opts;
211 }
212
213 public void emit(Event event) throws IOException {
214 this.events.add(event);
215 while (!needMoreEvents()) {
216 this.event = this.events.poll();
217 this.state.expect();
218 this.event = null;
219 }
220 }
221
222
223
224 private boolean needMoreEvents() {
225 if (events.isEmpty()) {
226 return true;
227 }
228 Event event = events.peek();
229 if (event instanceof DocumentStartEvent) {
230 return needEvents(1);
231 } else if (event instanceof SequenceStartEvent) {
232 return needEvents(2);
233 } else if (event instanceof MappingStartEvent) {
234 return needEvents(3);
235 } else {
236 return false;
237 }
238 }
239
240 private boolean needEvents(int count) {
241 int level = 0;
242 Iterator<Event> iter = events.iterator();
243 iter.next();
244 while (iter.hasNext()) {
245 Event event = iter.next();
246 if (event instanceof DocumentStartEvent || event instanceof CollectionStartEvent) {
247 level++;
248 } else if (event instanceof DocumentEndEvent || event instanceof CollectionEndEvent) {
249 level--;
250 } else if (event instanceof StreamEndEvent) {
251 level = -1;
252 }
253 if (level < 0) {
254 return false;
255 }
256 }
257 return events.size() < count + 1;
258 }
259
260 private void increaseIndent(boolean flow, boolean indentless) {
261 indents.push(indent);
262 if (indent == null) {
263 if (flow) {
264 indent = bestIndent;
265 } else {
266 indent = 0;
267 }
268 } else if (!indentless) {
269 this.indent += bestIndent;
270 }
271 }
272
273
274
275
276
277 private class ExpectStreamStart implements EmitterState {
278 public void expect() throws IOException {
279 if (event instanceof StreamStartEvent) {
280 writeStreamStart();
281 state = new ExpectFirstDocumentStart();
282 } else {
283 throw new EmitterException("expected StreamStartEvent, but got " + event);
284 }
285 }
286 }
287
288 private class ExpectNothing implements EmitterState {
289 public void expect() throws IOException {
290 throw new EmitterException("expecting nothing, but got " + event);
291 }
292 }
293
294
295
296 private class ExpectFirstDocumentStart implements EmitterState {
297 public void expect() throws IOException {
298 new ExpectDocumentStart(true).expect();
299 }
300 }
301
302 private class ExpectDocumentStart implements EmitterState {
303 private boolean first;
304
305 public ExpectDocumentStart(boolean first) {
306 this.first = first;
307 }
308
309 public void expect() throws IOException {
310 if (event instanceof DocumentStartEvent) {
311 DocumentStartEvent ev = (DocumentStartEvent) event;
312 if ((ev.getVersion() != null || ev.getTags() != null) && openEnded) {
313 writeIndicator("...", true, false, false);
314 writeIndent();
315 }
316 if (ev.getVersion() != null) {
317 String versionText = prepareVersion(ev.getVersion());
318 writeVersionDirective(versionText);
319 }
320 tagPrefixes = new LinkedHashMap<String, String>(DEFAULT_TAG_PREFIXES);
321 if (ev.getTags() != null) {
322 Set<String> handles = new TreeSet<String>(ev.getTags().keySet());
323 for (String handle : handles) {
324 String prefix = ev.getTags().get(handle);
325 tagPrefixes.put(prefix, handle);
326 String handleText = prepareTagHandle(handle);
327 String prefixText = prepareTagPrefix(prefix);
328 writeTagDirective(handleText, prefixText);
329 }
330 }
331 boolean implicit = first && !ev.getExplicit() && !canonical
332 && ev.getVersion() == null
333 && (ev.getTags() == null || ev.getTags().isEmpty())
334 && !checkEmptyDocument();
335 if (!implicit) {
336 writeIndent();
337 writeIndicator("---", true, false, false);
338 if (canonical) {
339 writeIndent();
340 }
341 }
342 state = new ExpectDocumentRoot();
343 } else if (event instanceof StreamEndEvent) {
344
345
346
347
348
349 writeStreamEnd();
350 state = new ExpectNothing();
351 } else {
352 throw new EmitterException("expected DocumentStartEvent, but got " + event);
353 }
354 }
355 }
356
357 private class ExpectDocumentEnd implements EmitterState {
358 public void expect() throws IOException {
359 if (event instanceof DocumentEndEvent) {
360 writeIndent();
361 if (((DocumentEndEvent) event).getExplicit()) {
362 writeIndicator("...", true, false, false);
363 writeIndent();
364 }
365 flushStream();
366 state = new ExpectDocumentStart(false);
367 } else {
368 throw new EmitterException("expected DocumentEndEvent, but got " + event);
369 }
370 }
371 }
372
373 private class ExpectDocumentRoot implements EmitterState {
374 public void expect() throws IOException {
375 states.push(new ExpectDocumentEnd());
376 expectNode(true, false, false);
377 }
378 }
379
380
381
382 private void expectNode(boolean root, boolean mapping, boolean simpleKey) throws IOException {
383 rootContext = root;
384 mappingContext = mapping;
385 simpleKeyContext = simpleKey;
386 if (event instanceof AliasEvent) {
387 expectAlias();
388 } else if (event instanceof ScalarEvent || event instanceof CollectionStartEvent) {
389 processAnchor("&");
390 processTag();
391 if (event instanceof ScalarEvent) {
392 expectScalar();
393 } else if (event instanceof SequenceStartEvent) {
394 if (flowLevel != 0 || canonical || ((SequenceStartEvent) event).getFlowStyle()
395 || checkEmptySequence()) {
396 expectFlowSequence();
397 } else {
398 expectBlockSequence();
399 }
400 } else {
401 if (flowLevel != 0 || canonical || ((MappingStartEvent) event).getFlowStyle()
402 || checkEmptyMapping()) {
403 expectFlowMapping();
404 } else {
405 expectBlockMapping();
406 }
407 }
408 } else {
409 throw new EmitterException("expected NodeEvent, but got " + event);
410 }
411 }
412
413 private void expectAlias() throws IOException {
414 if (((NodeEvent) event).getAnchor() == null) {
415 throw new EmitterException("anchor is not specified for alias");
416 }
417 processAnchor("*");
418 state = states.pop();
419 }
420
421 private void expectScalar() throws IOException {
422 increaseIndent(true, false);
423 processScalar();
424 indent = indents.pop();
425 state = states.pop();
426 }
427
428
429
430 private void expectFlowSequence() throws IOException {
431 writeIndicator("[", true, true, false);
432 flowLevel++;
433 increaseIndent(true, false);
434 if (prettyFlow) {
435 writeIndent();
436 }
437 state = new ExpectFirstFlowSequenceItem();
438 }
439
440 private class ExpectFirstFlowSequenceItem implements EmitterState {
441 public void expect() throws IOException {
442 if (event instanceof SequenceEndEvent) {
443 indent = indents.pop();
444 flowLevel--;
445 writeIndicator("]", false, false, false);
446 state = states.pop();
447 } else {
448 if (canonical || column > bestWidth || prettyFlow) {
449 writeIndent();
450 }
451 states.push(new ExpectFlowSequenceItem());
452 expectNode(false, false, false);
453 }
454 }
455 }
456
457 private class ExpectFlowSequenceItem implements EmitterState {
458 public void expect() throws IOException {
459 if (event instanceof SequenceEndEvent) {
460 indent = indents.pop();
461 flowLevel--;
462 if (canonical) {
463 writeIndicator(",", false, false, false);
464 writeIndent();
465 }
466 writeIndicator("]", false, false, false);
467 if (prettyFlow) {
468 writeIndent();
469 }
470 state = states.pop();
471 } else {
472 writeIndicator(",", false, false, false);
473 if (canonical || column > bestWidth || prettyFlow) {
474 writeIndent();
475 }
476 states.push(new ExpectFlowSequenceItem());
477 expectNode(false, false, false);
478 }
479 }
480 }
481
482
483
484 private void expectFlowMapping() throws IOException {
485 writeIndicator("{", true, true, false);
486 flowLevel++;
487 increaseIndent(true, false);
488 if (prettyFlow) {
489 writeIndent();
490 }
491 state = new ExpectFirstFlowMappingKey();
492 }
493
494 private class ExpectFirstFlowMappingKey implements EmitterState {
495 public void expect() throws IOException {
496 if (event instanceof MappingEndEvent) {
497 indent = indents.pop();
498 flowLevel--;
499 writeIndicator("}", false, false, false);
500 state = states.pop();
501 } else {
502 if (canonical || column > bestWidth || prettyFlow) {
503 writeIndent();
504 }
505 if (!canonical && checkSimpleKey()) {
506 states.push(new ExpectFlowMappingSimpleValue());
507 expectNode(false, true, true);
508 } else {
509 writeIndicator("?", true, false, false);
510 states.push(new ExpectFlowMappingValue());
511 expectNode(false, true, false);
512 }
513 }
514 }
515 }
516
517 private class ExpectFlowMappingKey implements EmitterState {
518 public void expect() throws IOException {
519 if (event instanceof MappingEndEvent) {
520 indent = indents.pop();
521 flowLevel--;
522 if (canonical) {
523 writeIndicator(",", false, false, false);
524 writeIndent();
525 }
526 if (prettyFlow) {
527 writeIndent();
528 }
529 writeIndicator("}", false, false, false);
530 state = states.pop();
531 } else {
532 writeIndicator(",", false, false, false);
533 if (canonical || column > bestWidth || prettyFlow) {
534 writeIndent();
535 }
536 if (!canonical && checkSimpleKey()) {
537 states.push(new ExpectFlowMappingSimpleValue());
538 expectNode(false, true, true);
539 } else {
540 writeIndicator("?", true, false, false);
541 states.push(new ExpectFlowMappingValue());
542 expectNode(false, true, false);
543 }
544 }
545 }
546 }
547
548 private class ExpectFlowMappingSimpleValue implements EmitterState {
549 public void expect() throws IOException {
550 writeIndicator(":", false, false, false);
551 states.push(new ExpectFlowMappingKey());
552 expectNode(false, true, false);
553 }
554 }
555
556 private class ExpectFlowMappingValue implements EmitterState {
557 public void expect() throws IOException {
558 if (canonical || column > bestWidth || prettyFlow) {
559 writeIndent();
560 }
561 writeIndicator(":", true, false, false);
562 states.push(new ExpectFlowMappingKey());
563 expectNode(false, true, false);
564 }
565 }
566
567
568
569 private void expectBlockSequence() throws IOException {
570 boolean indentless = (mappingContext && !indention);
571 increaseIndent(false, indentless);
572 state = new ExpectFirstBlockSequenceItem();
573 }
574
575 private class ExpectFirstBlockSequenceItem implements EmitterState {
576 public void expect() throws IOException {
577 new ExpectBlockSequenceItem(true).expect();
578 }
579 }
580
581 private class ExpectBlockSequenceItem implements EmitterState {
582 private boolean first;
583
584 public ExpectBlockSequenceItem(boolean first) {
585 this.first = first;
586 }
587
588 public void expect() throws IOException {
589 if (!this.first && event instanceof SequenceEndEvent) {
590 indent = indents.pop();
591 state = states.pop();
592 } else {
593 writeIndent();
594 writeIndicator("-", true, false, true);
595 states.push(new ExpectBlockSequenceItem(false));
596 expectNode(false, false, false);
597 }
598 }
599 }
600
601
602 private void expectBlockMapping() throws IOException {
603 increaseIndent(false, false);
604 state = new ExpectFirstBlockMappingKey();
605 }
606
607 private class ExpectFirstBlockMappingKey implements EmitterState {
608 public void expect() throws IOException {
609 new ExpectBlockMappingKey(true).expect();
610 }
611 }
612
613 private class ExpectBlockMappingKey implements EmitterState {
614 private boolean first;
615
616 public ExpectBlockMappingKey(boolean first) {
617 this.first = first;
618 }
619
620 public void expect() throws IOException {
621 if (!this.first && event instanceof MappingEndEvent) {
622 indent = indents.pop();
623 state = states.pop();
624 } else {
625 writeIndent();
626 if (checkSimpleKey()) {
627 states.push(new ExpectBlockMappingSimpleValue());
628 expectNode(false, true, true);
629 } else {
630 writeIndicator("?", true, false, true);
631 states.push(new ExpectBlockMappingValue());
632 expectNode(false, true, false);
633 }
634 }
635 }
636 }
637
638 private class ExpectBlockMappingSimpleValue implements EmitterState {
639 public void expect() throws IOException {
640 writeIndicator(":", false, false, false);
641 states.push(new ExpectBlockMappingKey(false));
642 expectNode(false, true, false);
643 }
644 }
645
646 private class ExpectBlockMappingValue implements EmitterState {
647 public void expect() throws IOException {
648 writeIndent();
649 writeIndicator(":", true, false, true);
650 states.push(new ExpectBlockMappingKey(false));
651 expectNode(false, true, false);
652 }
653 }
654
655
656
657 private boolean checkEmptySequence() {
658 return (event instanceof SequenceStartEvent && !events.isEmpty() && events.peek() instanceof SequenceEndEvent);
659 }
660
661 private boolean checkEmptyMapping() {
662 return (event instanceof MappingStartEvent && !events.isEmpty() && events.peek() instanceof MappingEndEvent);
663 }
664
665 private boolean checkEmptyDocument() {
666 if (!(event instanceof DocumentStartEvent) || events.isEmpty()) {
667 return false;
668 }
669 Event event = events.peek();
670 if (event instanceof ScalarEvent) {
671 ScalarEvent e = (ScalarEvent) event;
672 return (e.getAnchor() == null && e.getTag() == null && e.getImplicit() != null && e
673 .getValue().length() == 0);
674 }
675 return false;
676 }
677
678 private boolean checkSimpleKey() {
679 int length = 0;
680 if (event instanceof NodeEvent && ((NodeEvent) event).getAnchor() != null) {
681 if (preparedAnchor == null) {
682 preparedAnchor = prepareAnchor(((NodeEvent) event).getAnchor());
683 }
684 length += preparedAnchor.length();
685 }
686 String tag = null;
687 if (event instanceof ScalarEvent) {
688 tag = ((ScalarEvent) event).getTag();
689 } else if (event instanceof CollectionStartEvent) {
690 tag = ((CollectionStartEvent) event).getTag();
691 }
692 if (tag != null) {
693 if (preparedTag == null) {
694 preparedTag = prepareTag(tag);
695 }
696 length += preparedTag.length();
697 }
698 if (event instanceof ScalarEvent) {
699 if (analysis == null) {
700 analysis = analyzeScalar(((ScalarEvent) event).getValue());
701 }
702 length += analysis.scalar.length();
703 }
704 return (length < 128 && (event instanceof AliasEvent
705 || (event instanceof ScalarEvent && !analysis.empty && !analysis.multiline)
706 || checkEmptySequence() || checkEmptyMapping()));
707 }
708
709
710
711 private void processAnchor(String indicator) throws IOException {
712 NodeEvent ev = (NodeEvent) event;
713 if (ev.getAnchor() == null) {
714 preparedAnchor = null;
715 return;
716 }
717 if (preparedAnchor == null) {
718 preparedAnchor = prepareAnchor(ev.getAnchor());
719 }
720 writeIndicator(indicator + preparedAnchor, true, false, false);
721 preparedAnchor = null;
722 }
723
724 private void processTag() throws IOException {
725 String tag = null;
726 if (event instanceof ScalarEvent) {
727 ScalarEvent ev = (ScalarEvent) event;
728 tag = ev.getTag();
729 if (style == null) {
730 style = chooseScalarStyle();
731 }
732 if (((!canonical || tag == null) && ((style == null && ev.getImplicit()
733 .canOmitTagInPlainScalar()) || (style != null && ev.getImplicit()
734 .canOmitTagInNonPlainScalar())))) {
735 preparedTag = null;
736 return;
737 }
738 if (ev.getImplicit().canOmitTagInPlainScalar() && tag == null) {
739 tag = "!";
740 preparedTag = null;
741 }
742 } else {
743 CollectionStartEvent ev = (CollectionStartEvent) event;
744 tag = ev.getTag();
745 if ((!canonical || tag == null) && ev.getImplicit()) {
746 preparedTag = null;
747 return;
748 }
749 }
750 if (tag == null) {
751 throw new EmitterException("tag is not specified");
752 }
753 if (preparedTag == null) {
754 preparedTag = prepareTag(tag);
755 }
756 writeIndicator(preparedTag, true, false, false);
757 preparedTag = null;
758 }
759
760 private Character chooseScalarStyle() {
761 ScalarEvent ev = (ScalarEvent) event;
762 if (analysis == null) {
763 analysis = analyzeScalar(ev.getValue());
764 }
765 if (ev.getStyle() != null && ev.getStyle() == '"' || this.canonical) {
766 return '"';
767 }
768 if (ev.getStyle() == null && ev.getImplicit().canOmitTagInPlainScalar()) {
769 if (!(simpleKeyContext && (analysis.empty || analysis.multiline))
770 && ((flowLevel != 0 && analysis.allowFlowPlain) || (flowLevel == 0 && analysis.allowBlockPlain))) {
771 return null;
772 }
773 }
774 if (ev.getStyle() != null && (ev.getStyle() == '|' || ev.getStyle() == '>')) {
775 if (flowLevel == 0 && !simpleKeyContext && analysis.allowBlock) {
776 return ev.getStyle();
777 }
778 }
779 if (ev.getStyle() == null || ev.getStyle() == '\'') {
780 if (analysis.allowSingleQuoted && !(simpleKeyContext && analysis.multiline)) {
781 return '\'';
782 }
783 }
784 return '"';
785 }
786
787 @SuppressWarnings("deprecation")
788 private void processScalar() throws IOException {
789 ScalarEvent ev = (ScalarEvent) event;
790 if (analysis == null) {
791 analysis = analyzeScalar(ev.getValue());
792 }
793 if (style == null) {
794 style = chooseScalarStyle();
795 }
796
797 style = options.calculateScalarStyle(analysis, ScalarStyle.createStyle(style)).getChar();
798 boolean split = !simpleKeyContext;
799 if (style == null) {
800 writePlain(analysis.scalar, split);
801 } else {
802 switch (style) {
803 case '"':
804 writeDoubleQuoted(analysis.scalar, split);
805 break;
806 case '\'':
807 writeSingleQuoted(analysis.scalar, split);
808 break;
809 case '>':
810 writeFolded(analysis.scalar);
811 break;
812 case '|':
813 writeLiteral(analysis.scalar);
814 break;
815 default:
816 throw new YAMLException("Unexpected style: " + style);
817 }
818 }
819 analysis = null;
820 style = null;
821 }
822
823
824
825 private String prepareVersion(Version version) {
826 if (version.getArray()[0] != 1) {
827 throw new EmitterException("unsupported YAML version: " + version);
828 }
829 return version.getRepresentation();
830 }
831
832 private final static Pattern HANDLE_FORMAT = Pattern.compile("^![-_\\w]*!$");
833
834 private String prepareTagHandle(String handle) {
835 if (handle.length() == 0) {
836 throw new EmitterException("tag handle must not be empty");
837 } else if (handle.charAt(0) != '!' || handle.charAt(handle.length() - 1) != '!') {
838 throw new EmitterException("tag handle must start and end with '!': " + handle);
839 } else if (!"!".equals(handle) && !HANDLE_FORMAT.matcher(handle).matches()) {
840 throw new EmitterException("invalid character in the tag handle: " + handle);
841 }
842 return handle;
843 }
844
845 private String prepareTagPrefix(String prefix) {
846 if (prefix.length() == 0) {
847 throw new EmitterException("tag prefix must not be empty");
848 }
849 StringBuilder chunks = new StringBuilder();
850 int start = 0;
851 int end = 0;
852 if (prefix.charAt(0) == '!') {
853 end = 1;
854 }
855 while (end < prefix.length()) {
856 end++;
857 }
858 if (start < end) {
859 chunks.append(prefix.substring(start, end));
860 }
861 return chunks.toString();
862 }
863
864 private String prepareTag(String tag) {
865 if (tag.length() == 0) {
866 throw new EmitterException("tag must not be empty");
867 }
868 if ("!".equals(tag)) {
869 return tag;
870 }
871 String handle = null;
872 String suffix = tag;
873
874 for (String prefix : tagPrefixes.keySet()) {
875 if (tag.startsWith(prefix) && ("!".equals(prefix) || prefix.length() < tag.length())) {
876 handle = prefix;
877 }
878 }
879 if (handle != null) {
880 suffix = tag.substring(handle.length());
881 handle = tagPrefixes.get(handle);
882 }
883
884 int end = suffix.length();
885 String suffixText = end > 0 ? suffix.substring(0, end) : "";
886
887 if (handle != null) {
888 return handle + suffixText;
889 }
890 return "!<" + suffixText + ">";
891 }
892
893 private final static Pattern ANCHOR_FORMAT = Pattern.compile("^[-_\\w]*$");
894
895 static String prepareAnchor(String anchor) {
896 if (anchor.length() == 0) {
897 throw new EmitterException("anchor must not be empty");
898 }
899 if (!ANCHOR_FORMAT.matcher(anchor).matches()) {
900 throw new EmitterException("invalid character in the anchor: " + anchor);
901 }
902 return anchor;
903 }
904
905 private ScalarAnalysis analyzeScalar(String scalar) {
906
907 if (scalar.length() == 0) {
908 return new ScalarAnalysis(scalar, true, false, false, true, true, false);
909 }
910
911 boolean blockIndicators = false;
912 boolean flowIndicators = false;
913 boolean lineBreaks = false;
914 boolean specialCharacters = false;
915
916
917 boolean leadingSpace = false;
918 boolean leadingBreak = false;
919 boolean trailingSpace = false;
920 boolean trailingBreak = false;
921 boolean breakSpace = false;
922 boolean spaceBreak = false;
923
924
925 if (scalar.startsWith("---") || scalar.startsWith("...")) {
926 blockIndicators = true;
927 flowIndicators = true;
928 }
929
930 boolean preceededByWhitespace = true;
931 boolean followedByWhitespace = (scalar.length() == 1 || Constant.NULL_BL_T_LINEBR
932 .has(scalar.charAt(1)));
933
934 boolean previousSpace = false;
935
936
937 boolean previousBreak = false;
938
939 int index = 0;
940
941 while (index < scalar.length()) {
942 char ch = scalar.charAt(index);
943
944 if (index == 0) {
945
946 if ("#,[]{}&*!|>\'\"%@`".indexOf(ch) != -1) {
947 flowIndicators = true;
948 blockIndicators = true;
949 }
950 if (ch == '?' || ch == ':') {
951 flowIndicators = true;
952 if (followedByWhitespace) {
953 blockIndicators = true;
954 }
955 }
956 if (ch == '-' && followedByWhitespace) {
957 flowIndicators = true;
958 blockIndicators = true;
959 }
960 } else {
961
962 if (",?[]{}".indexOf(ch) != -1) {
963 flowIndicators = true;
964 }
965 if (ch == ':') {
966 flowIndicators = true;
967 if (followedByWhitespace) {
968 blockIndicators = true;
969 }
970 }
971 if (ch == '#' && preceededByWhitespace) {
972 flowIndicators = true;
973 blockIndicators = true;
974 }
975 }
976
977 boolean isLineBreak = Constant.LINEBR.has(ch);
978 if (isLineBreak) {
979 lineBreaks = true;
980 }
981 if (!(ch == '\n' || ('\u0020' <= ch && ch <= '\u007E'))) {
982 if ((ch == '\u0085' || ('\u00A0' <= ch && ch <= '\uD7FF') || ('\uE000' <= ch && ch <= '\uFFFD'))
983 && (ch != '\uFEFF')) {
984
985 if (!this.allowUnicode) {
986 specialCharacters = true;
987 }
988 } else {
989 specialCharacters = true;
990 }
991 }
992
993 if (ch == ' ') {
994 if (index == 0) {
995 leadingSpace = true;
996 }
997 if (index == scalar.length() - 1) {
998 trailingSpace = true;
999 }
1000 if (previousBreak) {
1001 breakSpace = true;
1002 }
1003 previousSpace = true;
1004 previousBreak = false;
1005 } else if (isLineBreak) {
1006 if (index == 0) {
1007 leadingBreak = true;
1008 }
1009 if (index == scalar.length() - 1) {
1010 trailingBreak = true;
1011 }
1012 if (previousSpace) {
1013 spaceBreak = true;
1014 }
1015 previousSpace = false;
1016 previousBreak = true;
1017 } else {
1018 previousSpace = false;
1019 previousBreak = false;
1020 }
1021
1022
1023 index++;
1024 preceededByWhitespace = Constant.NULL_BL_T.has(ch) || isLineBreak;
1025 followedByWhitespace = (index + 1 >= scalar.length()
1026 || (Constant.NULL_BL_T.has(scalar.charAt(index + 1))) || isLineBreak);
1027 }
1028
1029 boolean allowFlowPlain = true;
1030 boolean allowBlockPlain = true;
1031 boolean allowSingleQuoted = true;
1032 boolean allowBlock = true;
1033
1034 if (leadingSpace || leadingBreak || trailingSpace || trailingBreak) {
1035 allowFlowPlain = allowBlockPlain = false;
1036 }
1037
1038 if (trailingSpace) {
1039 allowBlock = false;
1040 }
1041
1042
1043 if (breakSpace) {
1044 allowFlowPlain = allowBlockPlain = allowSingleQuoted = false;
1045 }
1046
1047
1048 if (spaceBreak || specialCharacters) {
1049 allowFlowPlain = allowBlockPlain = allowSingleQuoted = allowBlock = false;
1050 }
1051
1052
1053 if (lineBreaks) {
1054 allowFlowPlain = false;
1055 }
1056
1057 if (flowIndicators) {
1058 allowFlowPlain = false;
1059 }
1060
1061 if (blockIndicators) {
1062 allowBlockPlain = false;
1063 }
1064
1065 return new ScalarAnalysis(scalar, false, lineBreaks, allowFlowPlain, allowBlockPlain,
1066 allowSingleQuoted, allowBlock);
1067 }
1068
1069
1070
1071 void flushStream() throws IOException {
1072 stream.flush();
1073 }
1074
1075 void writeStreamStart() {
1076
1077 }
1078
1079 void writeStreamEnd() throws IOException {
1080 flushStream();
1081 }
1082
1083 void writeIndicator(String indicator, boolean needWhitespace, boolean whitespace,
1084 boolean indentation) throws IOException {
1085 if (!this.whitespace && needWhitespace) {
1086 this.column++;
1087 stream.write(SPACE);
1088 }
1089 this.whitespace = whitespace;
1090 this.indention = this.indention && indentation;
1091 this.column += indicator.length();
1092 openEnded = false;
1093 stream.write(indicator);
1094 }
1095
1096 void writeIndent() throws IOException {
1097 int indent;
1098 if (this.indent != null) {
1099 indent = this.indent;
1100 } else {
1101 indent = 0;
1102 }
1103
1104 if (!this.indention || this.column > indent || (this.column == indent && !this.whitespace)) {
1105 writeLineBreak(null);
1106 }
1107
1108 if (this.column < indent) {
1109 this.whitespace = true;
1110 char[] data = new char[indent - this.column];
1111 for (int i = 0; i < data.length; i++) {
1112 data[i] = ' ';
1113 }
1114 this.column = indent;
1115 stream.write(data);
1116 }
1117 }
1118
1119 private void writeLineBreak(String data) throws IOException {
1120 this.whitespace = true;
1121 this.indention = true;
1122 this.column = 0;
1123 if (data == null) {
1124 stream.write(this.bestLineBreak);
1125 } else {
1126 stream.write(data);
1127 }
1128 }
1129
1130 void writeVersionDirective(String versionText) throws IOException {
1131 stream.write("%YAML ");
1132 stream.write(versionText);
1133 writeLineBreak(null);
1134 }
1135
1136 void writeTagDirective(String handleText, String prefixText) throws IOException {
1137
1138
1139 stream.write("%TAG ");
1140 stream.write(handleText);
1141 stream.write(SPACE);
1142 stream.write(prefixText);
1143 writeLineBreak(null);
1144 }
1145
1146
1147 private void writeSingleQuoted(String text, boolean split) throws IOException {
1148 writeIndicator("'", true, false, false);
1149 boolean spaces = false;
1150 boolean breaks = false;
1151 int start = 0, end = 0;
1152 char ch;
1153 while (end <= text.length()) {
1154 ch = 0;
1155 if (end < text.length()) {
1156 ch = text.charAt(end);
1157 }
1158 if (spaces) {
1159 if (ch == 0 || ch != ' ') {
1160 if (start + 1 == end && this.column > this.bestWidth && split && start != 0
1161 && end != text.length()) {
1162 writeIndent();
1163 } else {
1164 int len = end - start;
1165 this.column += len;
1166 stream.write(text, start, len);
1167 }
1168 start = end;
1169 }
1170 } else if (breaks) {
1171 if (ch == 0 || Constant.LINEBR.hasNo(ch)) {
1172 if (text.charAt(start) == '\n') {
1173 writeLineBreak(null);
1174 }
1175 String data = text.substring(start, end);
1176 for (char br : data.toCharArray()) {
1177 if (br == '\n') {
1178 writeLineBreak(null);
1179 } else {
1180 writeLineBreak(String.valueOf(br));
1181 }
1182 }
1183 writeIndent();
1184 start = end;
1185 }
1186 } else {
1187 if (Constant.LINEBR.has(ch, "\0 \'")) {
1188 if (start < end) {
1189 int len = end - start;
1190 this.column += len;
1191 stream.write(text, start, len);
1192 start = end;
1193 }
1194 }
1195 }
1196 if (ch == '\'') {
1197 this.column += 2;
1198 stream.write("''");
1199 start = end + 1;
1200 }
1201 if (ch != 0) {
1202 spaces = ch == ' ';
1203 breaks = Constant.LINEBR.has(ch);
1204 }
1205 end++;
1206 }
1207 writeIndicator("'", false, false, false);
1208 }
1209
1210 private void writeDoubleQuoted(String text, boolean split) throws IOException {
1211 writeIndicator("\"", true, false, false);
1212 int start = 0;
1213 int end = 0;
1214 while (end <= text.length()) {
1215 Character ch = null;
1216 if (end < text.length()) {
1217 ch = text.charAt(end);
1218 }
1219 if (ch == null || "\"\\\u0085\u2028\u2029\uFEFF".indexOf(ch) != -1
1220 || !('\u0020' <= ch && ch <= '\u007E')) {
1221 if (start < end) {
1222 int len = end - start;
1223 this.column += len;
1224 stream.write(text, start, len);
1225 start = end;
1226 }
1227 if (ch != null) {
1228 String data;
1229 if (ESCAPE_REPLACEMENTS.containsKey(ch)) {
1230 data = "\\" + ESCAPE_REPLACEMENTS.get(ch);
1231 } else if (!this.allowUnicode || !StreamReader.isPrintable(ch)) {
1232
1233
1234 if (ch <= '\u00FF') {
1235 String s = "0" + Integer.toString(ch, 16);
1236 data = "\\x" + s.substring(s.length() - 2);
1237 } else if (ch >= '\uD800' && ch <= '\uDBFF') {
1238 if (end + 1 < text.length()) {
1239 Character ch2 = text.charAt(++end);
1240 String s = "000" + Long.toHexString(Character.toCodePoint(ch, ch2));
1241 data = "\\U" + s.substring(s.length() - 8);
1242 } else {
1243 String s = "000" + Integer.toString(ch, 16);
1244 data = "\\u" + s.substring(s.length() - 4);
1245 }
1246 } else {
1247 String s = "000" + Integer.toString(ch, 16);
1248 data = "\\u" + s.substring(s.length() - 4);
1249 }
1250 } else {
1251 data = String.valueOf(ch);
1252 }
1253 this.column += data.length();
1254 stream.write(data);
1255 start = end + 1;
1256 }
1257 }
1258 if ((0 < end && end < (text.length() - 1)) && (ch == ' ' || start >= end)
1259 && (this.column + (end - start)) > this.bestWidth && split) {
1260 String data;
1261 if (start >= end) {
1262 data = "\\";
1263 } else {
1264 data = text.substring(start, end) + "\\";
1265 }
1266 if (start < end) {
1267 start = end;
1268 }
1269 this.column += data.length();
1270 stream.write(data);
1271 writeIndent();
1272 this.whitespace = false;
1273 this.indention = false;
1274 if (text.charAt(start) == ' ') {
1275 data = "\\";
1276 this.column += data.length();
1277 stream.write(data);
1278 }
1279 }
1280 end += 1;
1281 }
1282 writeIndicator("\"", false, false, false);
1283 }
1284
1285 private String determineBlockHints(String text) {
1286 StringBuilder hints = new StringBuilder();
1287 if (Constant.LINEBR.has(text.charAt(0), " ")) {
1288 hints.append(bestIndent);
1289 }
1290 char ch1 = text.charAt(text.length() - 1);
1291 if (Constant.LINEBR.hasNo(ch1)) {
1292 hints.append("-");
1293 } else if (text.length() == 1 || Constant.LINEBR.has(text.charAt(text.length() - 2))) {
1294 hints.append("+");
1295 }
1296 return hints.toString();
1297 }
1298
1299 void writeFolded(String text) throws IOException {
1300 String hints = determineBlockHints(text);
1301 writeIndicator(">" + hints, true, false, false);
1302 if (hints.length() > 0 && (hints.charAt(hints.length() - 1) == '+')) {
1303 openEnded = true;
1304 }
1305 writeLineBreak(null);
1306 boolean leadingSpace = true;
1307 boolean spaces = false;
1308 boolean breaks = true;
1309 int start = 0, end = 0;
1310 while (end <= text.length()) {
1311 char ch = 0;
1312 if (end < text.length()) {
1313 ch = text.charAt(end);
1314 }
1315 if (breaks) {
1316 if (ch == 0 || Constant.LINEBR.hasNo(ch)) {
1317 if (!leadingSpace && ch != 0 && ch != ' ' && text.charAt(start) == '\n') {
1318 writeLineBreak(null);
1319 }
1320 leadingSpace = (ch == ' ');
1321 String data = text.substring(start, end);
1322 for (char br : data.toCharArray()) {
1323 if (br == '\n') {
1324 writeLineBreak(null);
1325 } else {
1326 writeLineBreak(String.valueOf(br));
1327 }
1328 }
1329 if (ch != 0) {
1330 writeIndent();
1331 }
1332 start = end;
1333 }
1334 } else if (spaces) {
1335 if (ch != ' ') {
1336 if (start + 1 == end && this.column > this.bestWidth) {
1337 writeIndent();
1338 } else {
1339 int len = end - start;
1340 this.column += len;
1341 stream.write(text, start, len);
1342 }
1343 start = end;
1344 }
1345 } else {
1346 if (Constant.LINEBR.has(ch, "\0 ")) {
1347 int len = end - start;
1348 this.column += len;
1349 stream.write(text, start, len);
1350 if (ch == 0) {
1351 writeLineBreak(null);
1352 }
1353 start = end;
1354 }
1355 }
1356 if (ch != 0) {
1357 breaks = Constant.LINEBR.has(ch);
1358 spaces = (ch == ' ');
1359 }
1360 end++;
1361 }
1362 }
1363
1364 void writeLiteral(String text) throws IOException {
1365 String hints = determineBlockHints(text);
1366 writeIndicator("|" + hints, true, false, false);
1367 if (hints.length() > 0 && (hints.charAt(hints.length() - 1)) == '+') {
1368 openEnded = true;
1369 }
1370 writeLineBreak(null);
1371 boolean breaks = true;
1372 int start = 0, end = 0;
1373 while (end <= text.length()) {
1374 char ch = 0;
1375 if (end < text.length()) {
1376 ch = text.charAt(end);
1377 }
1378 if (breaks) {
1379 if (ch == 0 || Constant.LINEBR.hasNo(ch)) {
1380 String data = text.substring(start, end);
1381 for (char br : data.toCharArray()) {
1382 if (br == '\n') {
1383 writeLineBreak(null);
1384 } else {
1385 writeLineBreak(String.valueOf(br));
1386 }
1387 }
1388 if (ch != 0) {
1389 writeIndent();
1390 }
1391 start = end;
1392 }
1393 } else {
1394 if (ch == 0 || Constant.LINEBR.has(ch)) {
1395 stream.write(text, start, end - start);
1396 if (ch == 0) {
1397 writeLineBreak(null);
1398 }
1399 start = end;
1400 }
1401 }
1402 if (ch != 0) {
1403 breaks = (Constant.LINEBR.has(ch));
1404 }
1405 end++;
1406 }
1407 }
1408
1409 void writePlain(String text, boolean split) throws IOException {
1410 if (rootContext) {
1411 openEnded = true;
1412 }
1413 if (text.length() == 0) {
1414 return;
1415 }
1416 if (!this.whitespace) {
1417 this.column++;
1418 stream.write(SPACE);
1419 }
1420 this.whitespace = false;
1421 this.indention = false;
1422 boolean spaces = false;
1423 boolean breaks = false;
1424 int start = 0, end = 0;
1425 while (end <= text.length()) {
1426 char ch = 0;
1427 if (end < text.length()) {
1428 ch = text.charAt(end);
1429 }
1430 if (spaces) {
1431 if (ch != ' ') {
1432 if (start + 1 == end && this.column > this.bestWidth && split) {
1433 writeIndent();
1434 this.whitespace = false;
1435 this.indention = false;
1436 } else {
1437 int len = end - start;
1438 this.column += len;
1439 stream.write(text, start, len);
1440 }
1441 start = end;
1442 }
1443 } else if (breaks) {
1444 if (Constant.LINEBR.hasNo(ch)) {
1445 if (text.charAt(start) == '\n') {
1446 writeLineBreak(null);
1447 }
1448 String data = text.substring(start, end);
1449 for (char br : data.toCharArray()) {
1450 if (br == '\n') {
1451 writeLineBreak(null);
1452 } else {
1453 writeLineBreak(String.valueOf(br));
1454 }
1455 }
1456 writeIndent();
1457 this.whitespace = false;
1458 this.indention = false;
1459 start = end;
1460 }
1461 } else {
1462 if (ch == 0 || Constant.LINEBR.has(ch)) {
1463 int len = end - start;
1464 this.column += len;
1465 stream.write(text, start, len);
1466 start = end;
1467 }
1468 }
1469 if (ch != 0) {
1470 spaces = (ch == ' ');
1471 breaks = (Constant.LINEBR.has(ch));
1472 }
1473 end++;
1474 }
1475 }
1476 }