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