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