1 // Copyright Ferdinand Majerech 2011. 2 // Distributed under the Boost Software License, Version 1.0. 3 // (See accompanying file LICENSE_1_0.txt or copy at 4 // http://www.boost.org/LICENSE_1_0.txt) 5 6 /** 7 * YAML emitter. 8 * Code based on PyYAML: http://www.pyyaml.org 9 */ 10 module dyaml.emitter; 11 12 13 import std.algorithm; 14 import std.array; 15 import std.ascii; 16 import std.container; 17 import std.conv; 18 import std.exception; 19 import std.format; 20 import std.range; 21 import std.string; 22 import std.system; 23 import std.typecons; 24 import std.utf; 25 26 import dyaml.stream; 27 import dyaml.anchor; 28 import dyaml.encoding; 29 import dyaml.escapes; 30 import dyaml.event; 31 import dyaml.exception; 32 import dyaml.fastcharsearch; 33 import dyaml.flags; 34 import dyaml.linebreak; 35 import dyaml.queue; 36 import dyaml.style; 37 import dyaml.tag; 38 import dyaml.tagdirective; 39 40 41 package: 42 43 /** 44 * Exception thrown at Emitter errors. 45 * 46 * See_Also: 47 * YAMLException 48 */ 49 class EmitterException : YAMLException 50 { 51 mixin ExceptionCtors; 52 } 53 54 private alias EmitterException Error; 55 56 //Stores results of analysis of a scalar, determining e.g. what scalar style to use. 57 struct ScalarAnalysis 58 { 59 //Scalar itself. 60 string scalar; 61 62 ///Analysis results. 63 Flags!("empty", "multiline", "allowFlowPlain", "allowBlockPlain", 64 "allowSingleQuoted", "allowDoubleQuoted", "allowBlock", "isNull") flags; 65 } 66 67 ///Quickly determines if a character is a newline. 68 private mixin FastCharSearch!"\n\u0085\u2028\u2029"d newlineSearch_; 69 70 // override the canFind added by the FastCharSearch mixins 71 private alias canFind = std.algorithm.canFind; 72 73 //Emits YAML events into a file/stream. 74 struct Emitter 75 { 76 private: 77 alias dyaml.tagdirective.TagDirective TagDirective; 78 79 ///Default tag handle shortcuts and replacements. 80 static TagDirective[] defaultTagDirectives_ = 81 [TagDirective("!", "!"), TagDirective("!!", "tag:yaml.org,2002:")]; 82 83 ///Stream to write to. 84 YStream stream_; 85 ///Encoding can be overriden by STREAM-START. 86 Encoding encoding_ = Encoding.UTF_8; 87 88 ///Stack of states. 89 Array!(void delegate()) states_; 90 ///Current state. 91 void delegate() state_; 92 93 ///Event queue. 94 Queue!Event events_; 95 ///Event we're currently emitting. 96 Event event_; 97 98 ///Stack of previous indentation levels. 99 Array!int indents_; 100 ///Current indentation level. 101 int indent_ = -1; 102 103 ///Level of nesting in flow context. If 0, we're in block context. 104 uint flowLevel_ = 0; 105 106 /// Describes context (where we are in the document). 107 enum Context 108 { 109 /// Root node of a document. 110 Root, 111 /// Sequence. 112 Sequence, 113 /// Mapping. 114 MappingNoSimpleKey, 115 /// Mapping, in a simple key. 116 MappingSimpleKey 117 } 118 /// Current context. 119 Context context_; 120 121 ///Characteristics of the last emitted character: 122 123 ///Line. 124 uint line_ = 0; 125 ///Column. 126 uint column_ = 0; 127 ///Whitespace character? 128 bool whitespace_ = true; 129 ///indentation space, '-', '?', or ':'? 130 bool indentation_ = true; 131 132 ///Does the document require an explicit document indicator? 133 bool openEnded_; 134 135 ///Formatting details. 136 137 ///Canonical scalar format? 138 bool canonical_; 139 ///Best indentation width. 140 uint bestIndent_ = 2; 141 ///Best text width. 142 uint bestWidth_ = 80; 143 ///Best line break character/s. 144 LineBreak bestLineBreak_; 145 146 ///Tag directive handle - prefix pairs. 147 TagDirective[] tagDirectives_; 148 149 ///Anchor/alias to process. 150 string preparedAnchor_ = null; 151 ///Tag to process. 152 string preparedTag_ = null; 153 154 ///Analysis result of the current scalar. 155 ScalarAnalysis analysis_; 156 ///Style of the current scalar. 157 ScalarStyle style_ = ScalarStyle.Invalid; 158 159 public: 160 @disable int opCmp(ref Emitter); 161 @disable bool opEquals(ref Emitter); 162 163 /** 164 * Construct an emitter. 165 * 166 * Params: stream = YStream to write to. Must be writable. 167 * canonical = Write scalars in canonical form? 168 * indent = Indentation width. 169 * lineBreak = Line break character/s. 170 */ 171 this(YStream stream, const bool canonical, const int indent, const int width, 172 const LineBreak lineBreak) @trusted 173 in{assert(stream.writeable, "Can't emit YAML to a non-writable stream");} 174 body 175 { 176 states_.reserve(32); 177 indents_.reserve(32); 178 stream_ = stream; 179 canonical_ = canonical; 180 state_ = &expectStreamStart; 181 182 if(indent > 1 && indent < 10){bestIndent_ = indent;} 183 if(width > bestIndent_ * 2) {bestWidth_ = width;} 184 bestLineBreak_ = lineBreak; 185 186 analysis_.flags.isNull = true; 187 } 188 189 ///Destroy the emitter. 190 @trusted ~this() 191 { 192 stream_ = null; 193 states_.destroy(); 194 events_.destroy(); 195 indents_.destroy(); 196 tagDirectives_.destroy(); 197 tagDirectives_ = null; 198 preparedAnchor_.destroy(); 199 preparedAnchor_ = null; 200 preparedTag_.destroy(); 201 preparedTag_ = null; 202 } 203 204 ///Emit an event. Throws EmitterException on error. 205 void emit(Event event) @trusted 206 { 207 events_.push(event); 208 while(!needMoreEvents()) 209 { 210 event_ = events_.pop(); 211 state_(); 212 event_.destroy(); 213 } 214 } 215 216 private: 217 ///Pop and return the newest state in states_. 218 void delegate() popState() @trusted 219 { 220 enforce(states_.length > 0, 221 new YAMLException("Emitter: Need to pop a state but there are no states left")); 222 const result = states_.back; 223 states_.length = states_.length - 1; 224 return result; 225 } 226 227 ///Pop and return the newest indent in indents_. 228 int popIndent() @trusted 229 { 230 enforce(indents_.length > 0, 231 new YAMLException("Emitter: Need to pop an indent level but there" ~ 232 " are no indent levels left")); 233 const result = indents_.back; 234 indents_.length = indents_.length - 1; 235 return result; 236 } 237 238 ///Write a string to the file/stream. 239 void writeString(const string str) @system 240 { 241 try final switch(encoding_) 242 { 243 case Encoding.UTF_8: 244 stream_.writeExact(str.ptr, str.length * char.sizeof); 245 break; 246 case Encoding.UTF_16: 247 const buffer = to!wstring(str); 248 stream_.writeExact(buffer.ptr, buffer.length * wchar.sizeof); 249 break; 250 case Encoding.UTF_32: 251 const buffer = to!dstring(str); 252 stream_.writeExact(buffer.ptr, buffer.length * dchar.sizeof); 253 break; 254 } 255 catch(Exception e) 256 { 257 throw new Error("Unable to write to stream: " ~ e.msg); 258 } 259 } 260 261 ///In some cases, we wait for a few next events before emitting. 262 bool needMoreEvents() @trusted nothrow 263 { 264 if(events_.length == 0){return true;} 265 266 const event = events_.peek(); 267 if(event.id == EventID.DocumentStart){return needEvents(1);} 268 if(event.id == EventID.SequenceStart){return needEvents(2);} 269 if(event.id == EventID.MappingStart) {return needEvents(3);} 270 271 return false; 272 } 273 274 ///Determines if we need specified number of more events. 275 bool needEvents(in uint count) @system nothrow 276 { 277 int level = 0; 278 279 //Rather ugly, but good enough for now. 280 //Couldn't be bothered writing a range as events_ should eventually 281 //become a Phobos queue/linked list. 282 events_.startIteration(); 283 events_.next(); 284 while(!events_.iterationOver()) 285 { 286 const event = events_.next(); 287 static starts = [EventID.DocumentStart, EventID.SequenceStart, EventID.MappingStart]; 288 static ends = [EventID.DocumentEnd, EventID.SequenceEnd, EventID.MappingEnd]; 289 if(starts.canFind(event.id)) {++level;} 290 else if(ends.canFind(event.id)){--level;} 291 else if(event.id == EventID.StreamStart){level = -1;} 292 293 if(level < 0) 294 { 295 return false; 296 } 297 } 298 299 return events_.length < (count + 1); 300 } 301 302 ///Increase indentation level. 303 void increaseIndent(const Flag!"flow" flow = No.flow, const bool indentless = false) @trusted 304 { 305 indents_ ~= indent_; 306 if(indent_ == -1) 307 { 308 indent_ = flow ? bestIndent_ : 0; 309 } 310 else if(!indentless) 311 { 312 indent_ += bestIndent_; 313 } 314 } 315 316 ///Determines if the type of current event is as specified. Throws if no event. 317 bool eventTypeIs(in EventID id) const pure @trusted 318 { 319 enforce(!event_.isNull, 320 new Error("Expected an event, but no event is available.")); 321 return event_.id == id; 322 } 323 324 325 //States. 326 327 328 //Stream handlers. 329 330 ///Handle start of a file/stream. 331 void expectStreamStart() @trusted 332 { 333 enforce(eventTypeIs(EventID.StreamStart), 334 new Error("Expected YStreamStart, but got " ~ event_.idString)); 335 336 encoding_ = event_.encoding; 337 writeStreamStart(); 338 state_ = &expectDocumentStart!(Yes.first); 339 } 340 341 ///Expect nothing, throwing if we still have something. 342 void expectNothing() const @trusted 343 { 344 throw new Error("Expected nothing, but got " ~ event_.idString); 345 } 346 347 //Document handlers. 348 349 ///Handle start of a document. 350 void expectDocumentStart(Flag!"first" first)() @trusted 351 { 352 enforce(eventTypeIs(EventID.DocumentStart) || eventTypeIs(EventID.StreamEnd), 353 new Error("Expected DocumentStart or YStreamEnd, but got " 354 ~ event_.idString)); 355 356 if(event_.id == EventID.DocumentStart) 357 { 358 const YAMLVersion = event_.value; 359 auto tagDirectives = event_.tagDirectives; 360 if(openEnded_ && (YAMLVersion !is null || tagDirectives !is null)) 361 { 362 writeIndicator("...", Yes.needWhitespace); 363 writeIndent(); 364 } 365 366 if(YAMLVersion !is null) 367 { 368 writeVersionDirective(prepareVersion(YAMLVersion)); 369 } 370 371 if(tagDirectives !is null) 372 { 373 tagDirectives_ = tagDirectives; 374 sort!"icmp(a.handle, b.handle) < 0"(tagDirectives_); 375 376 foreach(ref pair; tagDirectives_) 377 { 378 writeTagDirective(prepareTagHandle(pair.handle), 379 prepareTagPrefix(pair.prefix)); 380 } 381 } 382 383 bool eq(ref TagDirective a, ref TagDirective b){return a.handle == b.handle;} 384 //Add any default tag directives that have not been overriden. 385 foreach(ref def; defaultTagDirectives_) 386 { 387 if(!std.algorithm.canFind!eq(tagDirectives_, def)) 388 { 389 tagDirectives_ ~= def; 390 } 391 } 392 393 const implicit = first && !event_.explicitDocument && !canonical_ && 394 YAMLVersion is null && tagDirectives is null && 395 !checkEmptyDocument(); 396 if(!implicit) 397 { 398 writeIndent(); 399 writeIndicator("---", Yes.needWhitespace); 400 if(canonical_){writeIndent();} 401 } 402 state_ = &expectRootNode; 403 } 404 else if(event_.id == EventID.StreamEnd) 405 { 406 if(openEnded_) 407 { 408 writeIndicator("...", Yes.needWhitespace); 409 writeIndent(); 410 } 411 writeStreamEnd(); 412 state_ = &expectNothing; 413 } 414 } 415 416 ///Handle end of a document. 417 void expectDocumentEnd() @trusted 418 { 419 enforce(eventTypeIs(EventID.DocumentEnd), 420 new Error("Expected DocumentEnd, but got " ~ event_.idString)); 421 422 writeIndent(); 423 if(event_.explicitDocument) 424 { 425 writeIndicator("...", Yes.needWhitespace); 426 writeIndent(); 427 } 428 stream_.flush(); 429 state_ = &expectDocumentStart!(No.first); 430 } 431 432 ///Handle the root node of a document. 433 void expectRootNode() @trusted 434 { 435 states_ ~= &expectDocumentEnd; 436 expectNode(Context.Root); 437 } 438 439 ///Handle a mapping node. 440 // 441 //Params: simpleKey = Are we in a simple key? 442 void expectMappingNode(const bool simpleKey = false) 443 { 444 expectNode(simpleKey ? Context.MappingSimpleKey : Context.MappingNoSimpleKey); 445 } 446 447 ///Handle a sequence node. 448 void expectSequenceNode() 449 { 450 expectNode(Context.Sequence); 451 } 452 453 ///Handle a new node. Context specifies where in the document we are. 454 void expectNode(const Context context) @trusted 455 { 456 context_ = context; 457 458 const flowCollection = event_.collectionStyle == CollectionStyle.Flow; 459 460 switch(event_.id) 461 { 462 case EventID.Alias: expectAlias(); break; 463 case EventID.Scalar: 464 processAnchor("&"); 465 processTag(); 466 expectScalar(); 467 break; 468 case EventID.SequenceStart: 469 processAnchor("&"); 470 processTag(); 471 if(flowLevel_ > 0 || canonical_ || flowCollection || checkEmptySequence()) 472 { 473 expectFlowSequence(); 474 } 475 else 476 { 477 expectBlockSequence(); 478 } 479 break; 480 case EventID.MappingStart: 481 processAnchor("&"); 482 processTag(); 483 if(flowLevel_ > 0 || canonical_ || flowCollection || checkEmptyMapping()) 484 { 485 expectFlowMapping(); 486 } 487 else 488 { 489 expectBlockMapping(); 490 } 491 break; 492 default: 493 throw new Error("Expected Alias, Scalar, SequenceStart or " ~ 494 "MappingStart, but got: " ~ event_.idString); 495 } 496 } 497 ///Handle an alias. 498 void expectAlias() @trusted 499 { 500 enforce(!event_.anchor.isNull(), new Error("Anchor is not specified for alias")); 501 processAnchor("*"); 502 state_ = popState(); 503 } 504 505 ///Handle a scalar. 506 void expectScalar() @trusted 507 { 508 increaseIndent(Yes.flow); 509 processScalar(); 510 indent_ = popIndent(); 511 state_ = popState(); 512 } 513 514 //Flow sequence handlers. 515 516 ///Handle a flow sequence. 517 void expectFlowSequence() @trusted 518 { 519 writeIndicator("[", Yes.needWhitespace, Yes.whitespace); 520 ++flowLevel_; 521 increaseIndent(Yes.flow); 522 state_ = &expectFlowSequenceItem!(Yes.first); 523 } 524 525 ///Handle a flow sequence item. 526 void expectFlowSequenceItem(Flag!"first" first)() @trusted 527 { 528 if(event_.id == EventID.SequenceEnd) 529 { 530 indent_ = popIndent(); 531 --flowLevel_; 532 static if(!first) if(canonical_) 533 { 534 writeIndicator(",", No.needWhitespace); 535 writeIndent(); 536 } 537 writeIndicator("]", No.needWhitespace); 538 state_ = popState(); 539 return; 540 } 541 static if(!first){writeIndicator(",", No.needWhitespace);} 542 if(canonical_ || column_ > bestWidth_){writeIndent();} 543 states_ ~= &expectFlowSequenceItem!(No.first); 544 expectSequenceNode(); 545 } 546 547 //Flow mapping handlers. 548 549 ///Handle a flow mapping. 550 void expectFlowMapping() @trusted 551 { 552 writeIndicator("{", Yes.needWhitespace, Yes.whitespace); 553 ++flowLevel_; 554 increaseIndent(Yes.flow); 555 state_ = &expectFlowMappingKey!(Yes.first); 556 } 557 558 ///Handle a key in a flow mapping. 559 void expectFlowMappingKey(Flag!"first" first)() @trusted 560 { 561 if(event_.id == EventID.MappingEnd) 562 { 563 indent_ = popIndent(); 564 --flowLevel_; 565 static if (!first) if(canonical_) 566 { 567 writeIndicator(",", No.needWhitespace); 568 writeIndent(); 569 } 570 writeIndicator("}", No.needWhitespace); 571 state_ = popState(); 572 return; 573 } 574 575 static if(!first){writeIndicator(",", No.needWhitespace);} 576 if(canonical_ || column_ > bestWidth_){writeIndent();} 577 if(!canonical_ && checkSimpleKey()) 578 { 579 states_ ~= &expectFlowMappingSimpleValue; 580 expectMappingNode(true); 581 return; 582 } 583 584 writeIndicator("?", Yes.needWhitespace); 585 states_ ~= &expectFlowMappingValue; 586 expectMappingNode(); 587 } 588 589 ///Handle a simple value in a flow mapping. 590 void expectFlowMappingSimpleValue() @trusted 591 { 592 writeIndicator(":", No.needWhitespace); 593 states_ ~= &expectFlowMappingKey!(No.first); 594 expectMappingNode(); 595 } 596 597 ///Handle a complex value in a flow mapping. 598 void expectFlowMappingValue() @trusted 599 { 600 if(canonical_ || column_ > bestWidth_){writeIndent();} 601 writeIndicator(":", Yes.needWhitespace); 602 states_ ~= &expectFlowMappingKey!(No.first); 603 expectMappingNode(); 604 } 605 606 //Block sequence handlers. 607 608 ///Handle a block sequence. 609 void expectBlockSequence() @safe 610 { 611 const indentless = (context_ == Context.MappingNoSimpleKey || 612 context_ == Context.MappingSimpleKey) && !indentation_; 613 increaseIndent(No.flow, indentless); 614 state_ = &expectBlockSequenceItem!(Yes.first); 615 } 616 617 ///Handle a block sequence item. 618 void expectBlockSequenceItem(Flag!"first" first)() @trusted 619 { 620 static if(!first) if(event_.id == EventID.SequenceEnd) 621 { 622 indent_ = popIndent(); 623 state_ = popState(); 624 return; 625 } 626 627 writeIndent(); 628 writeIndicator("-", Yes.needWhitespace, No.whitespace, Yes.indentation); 629 states_ ~= &expectBlockSequenceItem!(No.first); 630 expectSequenceNode(); 631 } 632 633 //Block mapping handlers. 634 635 ///Handle a block mapping. 636 void expectBlockMapping() @safe 637 { 638 increaseIndent(No.flow); 639 state_ = &expectBlockMappingKey!(Yes.first); 640 } 641 642 ///Handle a key in a block mapping. 643 void expectBlockMappingKey(Flag!"first" first)() @trusted 644 { 645 static if(!first) if(event_.id == EventID.MappingEnd) 646 { 647 indent_ = popIndent(); 648 state_ = popState(); 649 return; 650 } 651 652 writeIndent(); 653 if(checkSimpleKey()) 654 { 655 states_ ~= &expectBlockMappingSimpleValue; 656 expectMappingNode(true); 657 return; 658 } 659 660 writeIndicator("?", Yes.needWhitespace, No.whitespace, Yes.indentation); 661 states_ ~= &expectBlockMappingValue; 662 expectMappingNode(); 663 } 664 665 ///Handle a simple value in a block mapping. 666 void expectBlockMappingSimpleValue() @trusted 667 { 668 writeIndicator(":", No.needWhitespace); 669 states_ ~= &expectBlockMappingKey!(No.first); 670 expectMappingNode(); 671 } 672 673 ///Handle a complex value in a block mapping. 674 void expectBlockMappingValue() @trusted 675 { 676 writeIndent(); 677 writeIndicator(":", Yes.needWhitespace, No.whitespace, Yes.indentation); 678 states_ ~= &expectBlockMappingKey!(No.first); 679 expectMappingNode(); 680 } 681 682 //Checkers. 683 684 ///Check if an empty sequence is next. 685 bool checkEmptySequence() const @trusted pure nothrow 686 { 687 return event_.id == EventID.SequenceStart && events_.length > 0 688 && events_.peek().id == EventID.SequenceEnd; 689 } 690 691 ///Check if an empty mapping is next. 692 bool checkEmptyMapping() const @trusted pure nothrow 693 { 694 return event_.id == EventID.MappingStart && events_.length > 0 695 && events_.peek().id == EventID.MappingEnd; 696 } 697 698 ///Check if an empty document is next. 699 bool checkEmptyDocument() const @trusted pure nothrow 700 { 701 if(event_.id != EventID.DocumentStart || events_.length == 0) 702 { 703 return false; 704 } 705 706 const event = events_.peek(); 707 const emptyScalar = event.id == EventID.Scalar && event.anchor.isNull() && 708 event.tag.isNull() && event.implicit && event.value == ""; 709 return emptyScalar; 710 } 711 712 ///Check if a simple key is next. 713 bool checkSimpleKey() @trusted 714 { 715 uint length = 0; 716 const id = event_.id; 717 const scalar = id == EventID.Scalar; 718 const collectionStart = id == EventID.MappingStart || 719 id == EventID.SequenceStart; 720 721 if((id == EventID.Alias || scalar || collectionStart) 722 && !event_.anchor.isNull()) 723 { 724 if(preparedAnchor_ is null) 725 { 726 preparedAnchor_ = prepareAnchor(event_.anchor); 727 } 728 length += preparedAnchor_.length; 729 } 730 731 if((scalar || collectionStart) && !event_.tag.isNull()) 732 { 733 if(preparedTag_ is null){preparedTag_ = prepareTag(event_.tag);} 734 length += preparedTag_.length; 735 } 736 737 if(scalar) 738 { 739 if(analysis_.flags.isNull){analysis_ = analyzeScalar(event_.value);} 740 length += analysis_.scalar.length; 741 } 742 743 if(length >= 128){return false;} 744 745 return id == EventID.Alias || 746 (scalar && !analysis_.flags.empty && !analysis_.flags.multiline) || 747 checkEmptySequence() || 748 checkEmptyMapping(); 749 } 750 751 ///Process and write a scalar. 752 void processScalar() @trusted 753 { 754 if(analysis_.flags.isNull){analysis_ = analyzeScalar(event_.value);} 755 if(style_ == ScalarStyle.Invalid) 756 { 757 style_ = chooseScalarStyle(); 758 } 759 760 //if(analysis_.flags.multiline && (context_ != Context.MappingSimpleKey) && 761 // ([ScalarStyle.Invalid, ScalarStyle.Plain, ScalarStyle.SingleQuoted, ScalarStyle.DoubleQuoted) 762 // .canFind(style_)) 763 //{ 764 // writeIndent(); 765 //} 766 auto writer = ScalarWriter(this, analysis_.scalar, 767 context_ != Context.MappingSimpleKey); 768 with(writer) final switch(style_) 769 { 770 case ScalarStyle.Invalid: assert(false); 771 case ScalarStyle.DoubleQuoted: writeDoubleQuoted(); break; 772 case ScalarStyle.SingleQuoted: writeSingleQuoted(); break; 773 case ScalarStyle.Folded: writeFolded(); break; 774 case ScalarStyle.Literal: writeLiteral(); break; 775 case ScalarStyle.Plain: writePlain(); break; 776 } 777 analysis_.flags.isNull = true; 778 style_ = ScalarStyle.Invalid; 779 } 780 781 ///Process and write an anchor/alias. 782 void processAnchor(const string indicator) @trusted 783 { 784 if(event_.anchor.isNull()) 785 { 786 preparedAnchor_ = null; 787 return; 788 } 789 if(preparedAnchor_ is null) 790 { 791 preparedAnchor_ = prepareAnchor(event_.anchor); 792 } 793 if(preparedAnchor_ !is null && preparedAnchor_ != "") 794 { 795 writeIndicator(indicator, Yes.needWhitespace); 796 writeString(preparedAnchor_); 797 } 798 preparedAnchor_ = null; 799 } 800 801 ///Process and write a tag. 802 void processTag() @trusted 803 { 804 Tag tag = event_.tag; 805 806 if(event_.id == EventID.Scalar) 807 { 808 if(style_ == ScalarStyle.Invalid){style_ = chooseScalarStyle();} 809 if((!canonical_ || tag.isNull()) && 810 (style_ == ScalarStyle.Plain ? event_.implicit : event_.implicit_2)) 811 { 812 preparedTag_ = null; 813 return; 814 } 815 if(event_.implicit && tag.isNull()) 816 { 817 tag = Tag("!"); 818 preparedTag_ = null; 819 } 820 } 821 else if((!canonical_ || tag.isNull()) && event_.implicit) 822 { 823 preparedTag_ = null; 824 return; 825 } 826 827 enforce(!tag.isNull(), new Error("Tag is not specified")); 828 if(preparedTag_ is null){preparedTag_ = prepareTag(tag);} 829 if(preparedTag_ !is null && preparedTag_ != "") 830 { 831 writeIndicator(preparedTag_, Yes.needWhitespace); 832 } 833 preparedTag_ = null; 834 } 835 836 ///Determine style to write the current scalar in. 837 ScalarStyle chooseScalarStyle() @trusted 838 { 839 if(analysis_.flags.isNull){analysis_ = analyzeScalar(event_.value);} 840 841 const style = event_.scalarStyle; 842 const invalidOrPlain = style == ScalarStyle.Invalid || style == ScalarStyle.Plain; 843 const block = style == ScalarStyle.Literal || style == ScalarStyle.Folded; 844 const singleQuoted = style == ScalarStyle.SingleQuoted; 845 const doubleQuoted = style == ScalarStyle.DoubleQuoted; 846 847 const allowPlain = flowLevel_ > 0 ? analysis_.flags.allowFlowPlain 848 : analysis_.flags.allowBlockPlain; 849 //simple empty or multiline scalars can't be written in plain style 850 const simpleNonPlain = (context_ == Context.MappingSimpleKey) && 851 (analysis_.flags.empty || analysis_.flags.multiline); 852 853 if(doubleQuoted || canonical_) 854 { 855 return ScalarStyle.DoubleQuoted; 856 } 857 858 if(invalidOrPlain && event_.implicit && !simpleNonPlain && allowPlain) 859 { 860 return ScalarStyle.Plain; 861 } 862 863 if(block && flowLevel_ == 0 && context_ != Context.MappingSimpleKey && 864 analysis_.flags.allowBlock) 865 { 866 return style; 867 } 868 869 if((invalidOrPlain || singleQuoted) && 870 analysis_.flags.allowSingleQuoted && 871 !(context_ == Context.MappingSimpleKey && analysis_.flags.multiline)) 872 { 873 return ScalarStyle.SingleQuoted; 874 } 875 876 return ScalarStyle.DoubleQuoted; 877 } 878 879 ///Prepare YAML version string for output. 880 static string prepareVersion(const string YAMLVersion) @trusted 881 { 882 enforce(YAMLVersion.split(".")[0] == "1", 883 new Error("Unsupported YAML version: " ~ YAMLVersion)); 884 return YAMLVersion; 885 } 886 887 ///Encode an Unicode character for tag directive and write it to writer. 888 static void encodeChar(Writer)(ref Writer writer, in dchar c) @trusted 889 { 890 char[4] data; 891 const bytes = encode(data, c); 892 //For each byte add string in format %AB , where AB are hex digits of the byte. 893 foreach(const char b; data[0 .. bytes]) 894 { 895 formattedWrite(writer, "%%%02X", cast(ubyte)b); 896 } 897 } 898 899 ///Prepare tag directive handle for output. 900 static string prepareTagHandle(const string handle) @trusted 901 { 902 enforce(handle !is null && handle != "", 903 new Error("Tag handle must not be empty")); 904 905 if(handle.length > 1) foreach(const dchar c; handle[1 .. $ - 1]) 906 { 907 enforce(isAlphaNum(c) || "-_"d.canFind(c), 908 new Error("Invalid character: " ~ to!string(c) ~ 909 " in tag handle " ~ handle)); 910 } 911 return handle; 912 } 913 914 ///Prepare tag directive prefix for output. 915 static string prepareTagPrefix(const string prefix) @trusted 916 { 917 enforce(prefix !is null && prefix != "", 918 new Error("Tag prefix must not be empty")); 919 920 auto appender = appender!string(); 921 const offset = prefix[0] == '!' ? 1 : 0; 922 size_t start = 0; 923 size_t end = 0; 924 925 foreach(const size_t i, const dchar c; prefix) 926 { 927 const size_t idx = i + offset; 928 if(isAlphaNum(c) || "-;/?:@&=+$,_.!~*\'()[]%"d.canFind(c)) 929 { 930 end = idx + 1; 931 continue; 932 } 933 934 if(start < idx){appender.put(prefix[start .. idx]);} 935 start = end = idx + 1; 936 937 encodeChar(appender, c); 938 } 939 940 end = min(end, prefix.length); 941 if(start < end){appender.put(prefix[start .. end]);} 942 return appender.data; 943 } 944 945 ///Prepare tag for output. 946 string prepareTag(in Tag tag) @trusted 947 { 948 enforce(!tag.isNull(), new Error("Tag must not be empty")); 949 950 string tagString = tag.get; 951 if(tagString == "!"){return tagString;} 952 string handle = null; 953 string suffix = tagString; 954 955 //Sort lexicographically by prefix. 956 sort!"icmp(a.prefix, b.prefix) < 0"(tagDirectives_); 957 foreach(ref pair; tagDirectives_) 958 { 959 auto prefix = pair.prefix; 960 if(tagString.startsWith(prefix) && 961 (prefix != "!" || prefix.length < tagString.length)) 962 { 963 handle = pair.handle; 964 suffix = tagString[prefix.length .. $]; 965 } 966 } 967 968 auto appender = appender!string(); 969 appender.put(handle !is null && handle != "" ? handle : "!<"); 970 size_t start = 0; 971 size_t end = 0; 972 foreach(const dchar c; suffix) 973 { 974 if(isAlphaNum(c) || "-;/?:@&=+$,_.~*\'()[]"d.canFind(c) || 975 (c == '!' && handle != "!")) 976 { 977 ++end; 978 continue; 979 } 980 if(start < end){appender.put(suffix[start .. end]);} 981 start = end = end + 1; 982 983 encodeChar(appender, c); 984 } 985 986 if(start < end){appender.put(suffix[start .. end]);} 987 if(handle is null || handle == ""){appender.put(">");} 988 989 return appender.data; 990 } 991 992 ///Prepare anchor for output. 993 static string prepareAnchor(const Anchor anchor) @trusted 994 { 995 enforce(!anchor.isNull() && anchor.get != "", 996 new Error("Anchor must not be empty")); 997 const str = anchor.get; 998 foreach(const dchar c; str) 999 { 1000 enforce(isAlphaNum(c) || "-_"d.canFind(c), 1001 new Error("Invalid character: " ~ to!string(c) ~ " in anchor: " ~ str)); 1002 } 1003 return str; 1004 } 1005 1006 ///Analyze specifed scalar and return the analysis result. 1007 static ScalarAnalysis analyzeScalar(string scalar) @safe 1008 { 1009 ScalarAnalysis analysis; 1010 analysis.flags.isNull = false; 1011 analysis.scalar = scalar; 1012 1013 //Empty scalar is a special case. 1014 with(analysis.flags) if(scalar is null || scalar == "") 1015 { 1016 empty = true; 1017 multiline = false; 1018 allowFlowPlain = false; 1019 allowBlockPlain = true; 1020 allowSingleQuoted = true; 1021 allowDoubleQuoted = true; 1022 allowBlock = false; 1023 return analysis; 1024 } 1025 1026 //Indicators and special characters (All false by default). 1027 bool blockIndicators, flowIndicators, lineBreaks, specialCharacters; 1028 1029 //Important whitespace combinations (All false by default). 1030 bool leadingSpace, leadingBreak, trailingSpace, trailingBreak, 1031 breakSpace, spaceBreak; 1032 1033 //Check document indicators. 1034 if(scalar.startsWith("---", "...")) 1035 { 1036 blockIndicators = flowIndicators = true; 1037 } 1038 1039 //First character or preceded by a whitespace. 1040 bool preceededByWhitespace = true; 1041 1042 //Last character or followed by a whitespace. 1043 bool followedByWhitespace = scalar.length == 1 || 1044 " \t\0\n\r\u0085\u2028\u2029"d.canFind(scalar[1]); 1045 1046 //The previous character is a space/break (false by default). 1047 bool previousSpace, previousBreak; 1048 1049 foreach(const size_t index, const dchar c; scalar) 1050 { 1051 mixin FastCharSearch!("#,[]{}&*!|>\'\"%@`"d, 128) specialCharSearch; 1052 mixin FastCharSearch!(",?[]{}"d, 128) flowIndicatorSearch; 1053 1054 //Check for indicators. 1055 if(index == 0) 1056 { 1057 //Leading indicators are special characters. 1058 if(specialCharSearch.canFind(c)) 1059 { 1060 flowIndicators = blockIndicators = true; 1061 } 1062 if(':' == c || '?' == c) 1063 { 1064 flowIndicators = true; 1065 if(followedByWhitespace){blockIndicators = true;} 1066 } 1067 if(c == '-' && followedByWhitespace) 1068 { 1069 flowIndicators = blockIndicators = true; 1070 } 1071 } 1072 else 1073 { 1074 //Some indicators cannot appear within a scalar as well. 1075 if(flowIndicatorSearch.canFind(c)){flowIndicators = true;} 1076 if(c == ':') 1077 { 1078 flowIndicators = true; 1079 if(followedByWhitespace){blockIndicators = true;} 1080 } 1081 if(c == '#' && preceededByWhitespace) 1082 { 1083 flowIndicators = blockIndicators = true; 1084 } 1085 } 1086 1087 //Check for line breaks, special, and unicode characters. 1088 if(newlineSearch_.canFind(c)){lineBreaks = true;} 1089 if(!(c == '\n' || (c >= '\x20' && c <= '\x7E')) && 1090 !((c == '\u0085' || (c >= '\xA0' && c <= '\uD7FF') || 1091 (c >= '\uE000' && c <= '\uFFFD')) && c != '\uFEFF')) 1092 { 1093 specialCharacters = true; 1094 } 1095 1096 //Detect important whitespace combinations. 1097 if(c == ' ') 1098 { 1099 if(index == 0){leadingSpace = true;} 1100 if(index == scalar.length - 1){trailingSpace = true;} 1101 if(previousBreak){breakSpace = true;} 1102 previousSpace = true; 1103 previousBreak = false; 1104 } 1105 else if(newlineSearch_.canFind(c)) 1106 { 1107 if(index == 0){leadingBreak = true;} 1108 if(index == scalar.length - 1){trailingBreak = true;} 1109 if(previousSpace){spaceBreak = true;} 1110 previousSpace = false; 1111 previousBreak = true; 1112 } 1113 else 1114 { 1115 previousSpace = previousBreak = false; 1116 } 1117 1118 mixin FastCharSearch! "\0\n\r\u0085\u2028\u2029 \t"d spaceSearch; 1119 //Prepare for the next character. 1120 preceededByWhitespace = spaceSearch.canFind(c); 1121 followedByWhitespace = index + 2 >= scalar.length || 1122 spaceSearch.canFind(scalar[index + 2]); 1123 } 1124 1125 with(analysis.flags) 1126 { 1127 //Let's decide what styles are allowed. 1128 allowFlowPlain = allowBlockPlain = allowSingleQuoted 1129 = allowDoubleQuoted = allowBlock = true; 1130 1131 //Leading and trailing whitespaces are bad for plain scalars. 1132 if(leadingSpace || leadingBreak || trailingSpace || trailingBreak) 1133 { 1134 allowFlowPlain = allowBlockPlain = false; 1135 } 1136 1137 //We do not permit trailing spaces for block scalars. 1138 if(trailingSpace){allowBlock = false;} 1139 1140 //Spaces at the beginning of a new line are only acceptable for block 1141 //scalars. 1142 if(breakSpace) 1143 { 1144 allowFlowPlain = allowBlockPlain = allowSingleQuoted = false; 1145 } 1146 1147 //Spaces followed by breaks, as well as special character are only 1148 //allowed for double quoted scalars. 1149 if(spaceBreak || specialCharacters) 1150 { 1151 allowFlowPlain = allowBlockPlain = allowSingleQuoted = allowBlock = false; 1152 } 1153 1154 //Although the plain scalar writer supports breaks, we never emit 1155 //multiline plain scalars. 1156 if(lineBreaks){allowFlowPlain = allowBlockPlain = false;} 1157 1158 //Flow indicators are forbidden for flow plain scalars. 1159 if(flowIndicators){allowFlowPlain = false;} 1160 1161 //Block indicators are forbidden for block plain scalars. 1162 if(blockIndicators){allowBlockPlain = false;} 1163 1164 empty = false; 1165 multiline = lineBreaks; 1166 } 1167 1168 return analysis; 1169 } 1170 1171 //Writers. 1172 1173 ///Start the YAML stream (write the unicode byte order mark). 1174 void writeStreamStart() @system 1175 { 1176 immutable(ubyte)[] bom; 1177 //Write BOM (always, even for UTF-8) 1178 final switch(encoding_) 1179 { 1180 case Encoding.UTF_8: 1181 bom = ByteOrderMarks[BOM.UTF8]; 1182 break; 1183 case Encoding.UTF_16: 1184 bom = std.system.endian == Endian.littleEndian 1185 ? ByteOrderMarks[BOM.UTF16LE] 1186 : ByteOrderMarks[BOM.UTF16BE]; 1187 break; 1188 case Encoding.UTF_32: 1189 bom = std.system.endian == Endian.littleEndian 1190 ? ByteOrderMarks[BOM.UTF32LE] 1191 : ByteOrderMarks[BOM.UTF32BE]; 1192 break; 1193 } 1194 1195 enforce(stream_.write(bom) == bom.length, new Error("Unable to write to stream")); 1196 } 1197 1198 ///End the YAML stream. 1199 void writeStreamEnd() @system {stream_.flush();} 1200 1201 ///Write an indicator (e.g. ":", "[", ">", etc.). 1202 void writeIndicator(const string indicator, 1203 const Flag!"needWhitespace" needWhitespace, 1204 const Flag!"whitespace" whitespace = No.whitespace, 1205 const Flag!"indentation" indentation = No.indentation) @system 1206 { 1207 const bool prefixSpace = !whitespace_ && needWhitespace; 1208 whitespace_ = whitespace; 1209 indentation_ = indentation_ && indentation; 1210 openEnded_ = false; 1211 column_ += indicator.length; 1212 if(prefixSpace) 1213 { 1214 ++column_; 1215 writeString(" "); 1216 } 1217 writeString(indicator); 1218 } 1219 1220 ///Write indentation. 1221 void writeIndent() @system 1222 { 1223 const indent = indent_ == -1 ? 0 : indent_; 1224 1225 if(!indentation_ || column_ > indent || (column_ == indent && !whitespace_)) 1226 { 1227 writeLineBreak(); 1228 } 1229 if(column_ < indent) 1230 { 1231 whitespace_ = true; 1232 1233 //Used to avoid allocation of arbitrary length strings. 1234 static immutable spaces = " "; 1235 size_t numSpaces = indent - column_; 1236 column_ = indent; 1237 while(numSpaces >= spaces.length) 1238 { 1239 writeString(spaces); 1240 numSpaces -= spaces.length; 1241 } 1242 writeString(spaces[0 .. numSpaces]); 1243 } 1244 } 1245 1246 ///Start new line. 1247 void writeLineBreak(const string data = null) @system 1248 { 1249 whitespace_ = indentation_ = true; 1250 ++line_; 1251 column_ = 0; 1252 writeString(data is null ? lineBreak(bestLineBreak_) : data); 1253 } 1254 1255 ///Write a YAML version directive. 1256 void writeVersionDirective(const string versionText) @system 1257 { 1258 writeString("%YAML "); 1259 writeString(versionText); 1260 writeLineBreak(); 1261 } 1262 1263 ///Write a tag directive. 1264 void writeTagDirective(const string handle, const string prefix) @system 1265 { 1266 writeString("%TAG "); 1267 writeString(handle); 1268 writeString(" "); 1269 writeString(prefix); 1270 writeLineBreak(); 1271 } 1272 } 1273 1274 1275 private: 1276 1277 ///RAII struct used to write out scalar values. 1278 struct ScalarWriter 1279 { 1280 invariant() 1281 { 1282 assert(emitter_.bestIndent_ > 0 && emitter_.bestIndent_ < 10, 1283 "Emitter bestIndent must be 1 to 9 for one-character indent hint"); 1284 } 1285 1286 private: 1287 @disable int opCmp(ref Emitter); 1288 @disable bool opEquals(ref Emitter); 1289 1290 ///Used as "null" UTF-32 character. 1291 static immutable dcharNone = dchar.max; 1292 1293 ///Emitter used to emit the scalar. 1294 Emitter* emitter_; 1295 1296 ///UTF-8 encoded text of the scalar to write. 1297 string text_; 1298 1299 ///Can we split the scalar into multiple lines? 1300 bool split_; 1301 ///Are we currently going over spaces in the text? 1302 bool spaces_; 1303 ///Are we currently going over line breaks in the text? 1304 bool breaks_; 1305 1306 ///Start and end byte of the text range we're currently working with. 1307 size_t startByte_, endByte_; 1308 ///End byte of the text range including the currently processed character. 1309 size_t nextEndByte_; 1310 ///Start and end character of the text range we're currently working with. 1311 long startChar_, endChar_; 1312 1313 public: 1314 ///Construct a ScalarWriter using emitter to output text. 1315 this(ref Emitter emitter, string text, const bool split = true) @trusted nothrow 1316 { 1317 emitter_ = &emitter; 1318 text_ = text; 1319 split_ = split; 1320 } 1321 1322 ///Destroy the ScalarWriter. 1323 @trusted nothrow ~this() 1324 { 1325 text_ = null; 1326 } 1327 1328 ///Write text as single quoted scalar. 1329 void writeSingleQuoted() @system 1330 { 1331 emitter_.writeIndicator("\'", Yes.needWhitespace); 1332 spaces_ = breaks_ = false; 1333 resetTextPosition(); 1334 1335 do 1336 { 1337 const dchar c = nextChar(); 1338 if(spaces_) 1339 { 1340 if(c != ' ' && tooWide() && split_ && 1341 startByte_ != 0 && endByte_ != text_.length) 1342 { 1343 writeIndent(Flag!"ResetSpace".no); 1344 updateRangeStart(); 1345 } 1346 else if(c != ' ') 1347 { 1348 writeCurrentRange(Flag!"UpdateColumn".yes); 1349 } 1350 } 1351 else if(breaks_) 1352 { 1353 if(!newlineSearch_.canFind(c)) 1354 { 1355 writeStartLineBreak(); 1356 writeLineBreaks(); 1357 emitter_.writeIndent(); 1358 } 1359 } 1360 else if((c == dcharNone || c == '\'' || c == ' ' || newlineSearch_.canFind(c)) 1361 && startChar_ < endChar_) 1362 { 1363 writeCurrentRange(Flag!"UpdateColumn".yes); 1364 } 1365 if(c == '\'') 1366 { 1367 emitter_.column_ += 2; 1368 emitter_.writeString("\'\'"); 1369 startByte_ = endByte_ + 1; 1370 startChar_ = endChar_ + 1; 1371 } 1372 updateBreaks(c, Flag!"UpdateSpaces".yes); 1373 }while(endByte_ < text_.length); 1374 1375 emitter_.writeIndicator("\'", No.needWhitespace); 1376 } 1377 1378 ///Write text as double quoted scalar. 1379 void writeDoubleQuoted() @system 1380 { 1381 resetTextPosition(); 1382 emitter_.writeIndicator("\"", Yes.needWhitespace); 1383 do 1384 { 1385 const dchar c = nextChar(); 1386 //handle special characters 1387 if(c == dcharNone || "\"\\\u0085\u2028\u2029\uFEFF"d.canFind(c) || 1388 !((c >= '\x20' && c <= '\x7E') || 1389 ((c >= '\xA0' && c <= '\uD7FF') || (c >= '\uE000' && c <= '\uFFFD')))) 1390 { 1391 if(startChar_ < endChar_) 1392 { 1393 writeCurrentRange(Flag!"UpdateColumn".yes); 1394 } 1395 if(c != dcharNone) 1396 { 1397 auto appender = appender!string(); 1398 if((c in dyaml.escapes.toEscapes) !is null) 1399 { 1400 appender.put('\\'); 1401 appender.put(dyaml.escapes.toEscapes[c]); 1402 } 1403 else 1404 { 1405 //Write an escaped Unicode character. 1406 const format = c <= 255 ? "\\x%02X": 1407 c <= 65535 ? "\\u%04X": "\\U%08X"; 1408 formattedWrite(appender, format, cast(uint)c); 1409 } 1410 1411 emitter_.column_ += appender.data.length; 1412 emitter_.writeString(appender.data); 1413 startChar_ = endChar_ + 1; 1414 startByte_ = nextEndByte_; 1415 } 1416 } 1417 if((endByte_ > 0 && endByte_ < text_.length - strideBack(text_, text_.length)) 1418 && (c == ' ' || startChar_ >= endChar_) 1419 && (emitter_.column_ + endChar_ - startChar_ > emitter_.bestWidth_) 1420 && split_) 1421 { 1422 //text_[2:1] is ok in Python but not in D, so we have to use min() 1423 emitter_.writeString(text_[min(startByte_, endByte_) .. endByte_]); 1424 emitter_.writeString("\\"); 1425 emitter_.column_ += startChar_ - endChar_ + 1; 1426 startChar_ = max(startChar_, endChar_); 1427 startByte_ = max(startByte_, endByte_); 1428 1429 writeIndent(Flag!"ResetSpace".yes); 1430 if(charAtStart() == ' ') 1431 { 1432 emitter_.writeString("\\"); 1433 ++emitter_.column_; 1434 } 1435 } 1436 }while(endByte_ < text_.length); 1437 emitter_.writeIndicator("\"", No.needWhitespace); 1438 } 1439 1440 ///Write text as folded block scalar. 1441 void writeFolded() @system 1442 { 1443 initBlock('>'); 1444 bool leadingSpace = true; 1445 spaces_ = false; 1446 breaks_ = true; 1447 resetTextPosition(); 1448 1449 do 1450 { 1451 const dchar c = nextChar(); 1452 if(breaks_) 1453 { 1454 if(!newlineSearch_.canFind(c)) 1455 { 1456 if(!leadingSpace && c != dcharNone && c != ' ') 1457 { 1458 writeStartLineBreak(); 1459 } 1460 leadingSpace = (c == ' '); 1461 writeLineBreaks(); 1462 if(c != dcharNone){emitter_.writeIndent();} 1463 } 1464 } 1465 else if(spaces_) 1466 { 1467 if(c != ' ' && tooWide()) 1468 { 1469 writeIndent(Flag!"ResetSpace".no); 1470 updateRangeStart(); 1471 } 1472 else if(c != ' ') 1473 { 1474 writeCurrentRange(Flag!"UpdateColumn".yes); 1475 } 1476 } 1477 else if(c == dcharNone || newlineSearch_.canFind(c) || c == ' ') 1478 { 1479 writeCurrentRange(Flag!"UpdateColumn".yes); 1480 if(c == dcharNone){emitter_.writeLineBreak();} 1481 } 1482 updateBreaks(c, Flag!"UpdateSpaces".yes); 1483 }while(endByte_ < text_.length); 1484 } 1485 1486 ///Write text as literal block scalar. 1487 void writeLiteral() @system 1488 { 1489 initBlock('|'); 1490 breaks_ = true; 1491 resetTextPosition(); 1492 1493 do 1494 { 1495 const dchar c = nextChar(); 1496 if(breaks_) 1497 { 1498 if(!newlineSearch_.canFind(c)) 1499 { 1500 writeLineBreaks(); 1501 if(c != dcharNone){emitter_.writeIndent();} 1502 } 1503 } 1504 else if(c == dcharNone || newlineSearch_.canFind(c)) 1505 { 1506 writeCurrentRange(Flag!"UpdateColumn".no); 1507 if(c == dcharNone){emitter_.writeLineBreak();} 1508 } 1509 updateBreaks(c, Flag!"UpdateSpaces".no); 1510 }while(endByte_ < text_.length); 1511 } 1512 1513 ///Write text as plain scalar. 1514 void writePlain() @system 1515 { 1516 if(emitter_.context_ == Emitter.Context.Root){emitter_.openEnded_ = true;} 1517 if(text_ == ""){return;} 1518 if(!emitter_.whitespace_) 1519 { 1520 ++emitter_.column_; 1521 emitter_.writeString(" "); 1522 } 1523 emitter_.whitespace_ = emitter_.indentation_ = false; 1524 spaces_ = breaks_ = false; 1525 resetTextPosition(); 1526 1527 do 1528 { 1529 const dchar c = nextChar(); 1530 if(spaces_) 1531 { 1532 if(c != ' ' && tooWide() && split_) 1533 { 1534 writeIndent(Flag!"ResetSpace".yes); 1535 updateRangeStart(); 1536 } 1537 else if(c != ' ') 1538 { 1539 writeCurrentRange(Flag!"UpdateColumn".yes); 1540 } 1541 } 1542 else if(breaks_) 1543 { 1544 if(!newlineSearch_.canFind(c)) 1545 { 1546 writeStartLineBreak(); 1547 writeLineBreaks(); 1548 writeIndent(Flag!"ResetSpace".yes); 1549 } 1550 } 1551 else if(c == dcharNone || newlineSearch_.canFind(c) || c == ' ') 1552 { 1553 writeCurrentRange(Flag!"UpdateColumn".yes); 1554 } 1555 updateBreaks(c, Flag!"UpdateSpaces".yes); 1556 }while(endByte_ < text_.length); 1557 } 1558 1559 private: 1560 ///Get next character and move end of the text range to it. 1561 @property dchar nextChar() pure @safe 1562 { 1563 ++endChar_; 1564 endByte_ = nextEndByte_; 1565 if(endByte_ >= text_.length){return dcharNone;} 1566 const c = text_[nextEndByte_]; 1567 //c is ascii, no need to decode. 1568 if(c < 0x80) 1569 { 1570 ++nextEndByte_; 1571 return c; 1572 } 1573 return decode(text_, nextEndByte_); 1574 } 1575 1576 ///Get character at start of the text range. 1577 @property dchar charAtStart() const pure @safe 1578 { 1579 size_t idx = startByte_; 1580 return decode(text_, idx); 1581 } 1582 1583 ///Is the current line too wide? 1584 @property bool tooWide() const pure @safe nothrow 1585 { 1586 return startChar_ + 1 == endChar_ && 1587 emitter_.column_ > emitter_.bestWidth_; 1588 } 1589 1590 ///Determine hints (indicators) for block scalar. 1591 size_t determineBlockHints(char[] hints, uint bestIndent) const pure @trusted 1592 { 1593 size_t hintsIdx = 0; 1594 if(text_.length == 0){return hintsIdx;} 1595 1596 dchar lastChar(const string str, ref size_t end) 1597 { 1598 size_t idx = end = end - strideBack(str, end); 1599 return decode(text_, idx); 1600 } 1601 1602 size_t end = text_.length; 1603 const last = lastChar(text_, end); 1604 const secondLast = end > 0 ? lastChar(text_, end) : 0; 1605 1606 if(newlineSearch_.canFind(text_[0]) || text_[0] == ' ') 1607 { 1608 hints[hintsIdx++] = cast(char)('0' + bestIndent); 1609 } 1610 if(!newlineSearch_.canFind(last)) 1611 { 1612 hints[hintsIdx++] = '-'; 1613 } 1614 else if(std.utf.count(text_) == 1 || newlineSearch_.canFind(secondLast)) 1615 { 1616 hints[hintsIdx++] = '+'; 1617 } 1618 return hintsIdx; 1619 } 1620 1621 ///Initialize for block scalar writing with specified indicator. 1622 void initBlock(const char indicator) @system 1623 { 1624 char[4] hints; 1625 hints[0] = indicator; 1626 const hintsLength = 1 + determineBlockHints(hints[1 .. $], emitter_.bestIndent_); 1627 emitter_.writeIndicator(cast(string)hints[0 .. hintsLength], Yes.needWhitespace); 1628 if(hints.length > 0 && hints[$ - 1] == '+') 1629 { 1630 emitter_.openEnded_ = true; 1631 } 1632 emitter_.writeLineBreak(); 1633 } 1634 1635 ///Write out the current text range. 1636 void writeCurrentRange(const Flag!"UpdateColumn" updateColumn) @system 1637 { 1638 emitter_.writeString(text_[startByte_ .. endByte_]); 1639 if(updateColumn){emitter_.column_ += endChar_ - startChar_;} 1640 updateRangeStart(); 1641 } 1642 1643 ///Write line breaks in the text range. 1644 void writeLineBreaks() @system 1645 { 1646 foreach(const dchar br; text_[startByte_ .. endByte_]) 1647 { 1648 if(br == '\n'){emitter_.writeLineBreak();} 1649 else 1650 { 1651 char[4] brString; 1652 const bytes = encode(brString, br); 1653 emitter_.writeLineBreak(cast(string)brString[0 .. bytes]); 1654 } 1655 } 1656 updateRangeStart(); 1657 } 1658 1659 ///Write line break if start of the text range is a newline. 1660 void writeStartLineBreak() @system 1661 { 1662 if(charAtStart == '\n'){emitter_.writeLineBreak();} 1663 } 1664 1665 ///Write indentation, optionally resetting whitespace/indentation flags. 1666 void writeIndent(const Flag!"ResetSpace" resetSpace) @system 1667 { 1668 emitter_.writeIndent(); 1669 if(resetSpace) 1670 { 1671 emitter_.whitespace_ = emitter_.indentation_ = false; 1672 } 1673 } 1674 1675 ///Move start of text range to its end. 1676 void updateRangeStart() pure @safe nothrow 1677 { 1678 startByte_ = endByte_; 1679 startChar_ = endChar_; 1680 } 1681 1682 ///Update the line breaks_ flag, optionally updating the spaces_ flag. 1683 void updateBreaks(in dchar c, const Flag!"UpdateSpaces" updateSpaces) pure @trusted 1684 { 1685 if(c == dcharNone){return;} 1686 breaks_ = newlineSearch_.canFind(c); 1687 if(updateSpaces){spaces_ = c == ' ';} 1688 } 1689 1690 ///Move to the beginning of text. 1691 void resetTextPosition() pure @safe nothrow 1692 { 1693 startByte_ = endByte_ = nextEndByte_ = 0; 1694 startChar_ = endChar_ = -1; 1695 } 1696 }