1 // Written in the D programming language 2 3 /++ 4 This module provides functionality for creating XML 1.0 documents. 5 6 $(H3 Primary Symbols) 7 $(TABLE 8 $(TR $(TH Symbol) $(TH Description)) 9 $(TR $(TD $(LREF XMLWriter)) 10 $(TD Type used for writing XML documents.)) 11 $(TR $(TD $(LREF xmlWriter)) 12 $(TD Function used to create an $(LREF XMLWriter).)) 13 ) 14 15 $(H3 Helper Types) 16 $(TABLE 17 $(TR $(TD $(LREF XMLWritingException)) 18 $(TD Thrown by $(LREF XMLWriter) when it's given data that would 19 result in invalid XML.)) 20 ) 21 22 $(H3 Helper Functions) 23 $(TABLE 24 $(TR $(TH Symbol) $(TH Description)) 25 $(TR $(TD $(LREF writeTaggedText)) 26 $(TD Shortcut for writing text enclosed by tags. e.g. 27 $(D_CODE_STRING $(LT)tag>text$(LT)/tag>).)) 28 $(TR $(TD $(LREF writeXMLDecl)) 29 $(TD Writes the optional XML declaration that goes at the top of 30 an XML document to an ouput range.)) 31 ) 32 33 Copyright: Copyright 2018 - 2020 34 License: $(HTTP www.boost.org/LICENSE_1_0.txt, Boost License 1.0). 35 Authors: $(HTTPS jmdavisprog.com, Jonathan M Davis) 36 Source: $(LINK_TO_SRC dxml/_writer.d) 37 38 See_Also: $(LINK2 http://www.w3.org/TR/REC-xml/, Official Specification for XML 1.0) 39 +/ 40 module dxml.writer; 41 42 import std.range.primitives; 43 import std.traits; 44 import std.typecons : Flag; 45 46 47 /++ 48 Exception thrown when the writer is given data that would result in invalid 49 XML. 50 +/ 51 class XMLWritingException : Exception 52 { 53 private: 54 55 this(string msg, string file = __FILE__, size_t line = __LINE__) @safe pure nothrow @nogc 56 { 57 super(msg, file, line); 58 } 59 } 60 61 62 /++ 63 $(PHOBOS_REF Flag, std, typecons) indicating whether 64 $(LREF2 closeStartTag, XMLWriter) or $(LREF2 writeStartTag, XMLWriter) or 65 will write an empty element tag (which then does not require a corresponding 66 end tag). 67 +/ 68 alias EmptyTag = Flag!"EmptyTag"; 69 70 71 /++ 72 $(PHOBOS_REF Flag, std, typecons) indicating whether a write function of 73 $(LREF XMLWriter) will write a newline followed by an indent before the 74 entity being written. 75 +/ 76 alias Newline = Flag!"Newline"; 77 78 79 /++ 80 $(PHOBOS_REF Flag, std, typecons) indicating whether a write function of 81 $(LREF XMLWriter) which accepts text which may include newlines will write 82 an indent after each newline is written. 83 +/ 84 alias InsertIndent = Flag!"InsertIndent"; 85 86 87 /++ 88 Writes XML to an output range of characters. 89 90 Note that default initialization, copying, and assignment are disabled for 91 XMLWriter. This is because XMLWriter is essentially a reference type, but 92 in many cases, it doesn't need to be passed around, and forcing it to be 93 allocated on the heap in order to be a reference type seemed like an 94 unnecessary heap allocation. So, it's a struct with default initialization, 95 copying, and assignment disabled so that like a reference type, it will not 96 be copied or overwritten. Code that needs to pass it around can pass it by 97 $(K_REF) or use the $(LREF_ALTTEXT constructor, _XMLWriter.this) to 98 explicitly allocate it on the heap and then pass around the resulting 99 pointer. 100 101 The optional $(LREF Newline) and $(LREF InsertIndent) parameters to the 102 various write functions are used to control the formatting of the XML, and 103 $(LREF2 writeIndent, _XMLWriter) and $(LREF2 _output, _XMLWriter) can be 104 used for additional control over the formatting. 105 106 The indent provided to the XMLWriter is the base indent that will be used 107 whenever $(LREF2 writeIndent, _XMLWriter) and any write functions using 108 $(D Newline.yes) or $(D InsertIndent.yes) are called - e.g. 109 if the base indent is 4 spaces, $(D $(LREF2 tagDepth, _XMLWriter) == 3), and 110 $(D Newline.yes) is passed to $(LREF2 writeComment, _XMLWriter), then a 111 newline followed by 12 spaces will be written to the output range after the 112 comment. 113 114 $(LREF writeXMLDecl) can be used to write the $(D <?xml...?>) declaration 115 to the output range before constructing an XML writer, but if an application 116 wishes to do anything with a DTD section, it will have to write that to the 117 output range on its own before constructing the XMLWriter. XMLWriter expects 118 to start writing XML after any $(D <?xml...?>) or $(D <!DOCTYPE...>) 119 declarations. 120 121 The write functions check the arguments prior to writing anything to the 122 output range, so the XMLWriter is not in an invalid state after an 123 $(LREF XMLWritingException) is thrown, but it $(I is) in an invalid state 124 if any other exception is thrown (which will only occur if an input range 125 that is passed to a write function throws or if the ouput range throws when 126 XMLWriter calls $(PHOBOS_REF_ALTTEXT put, put, std, range, primitives) on 127 it). 128 129 Params: 130 output = The _output range that the XML will be written to. 131 baseIndent = Optional argument indicating the base indent to be used 132 when an indent is inserted after a newline in the XML (with 133 the actual indent being the base indent inserted once for 134 each level of the $(LREF2 tagDepth, _XMLWriter)). 135 The default is four spaces. baseIndent may only contain 136 spaces and/or tabs. 137 138 See_Also: $(LREF writeXMLDecl)$(BR) 139 $(REF encodeAttr, dxml, util)$(BR) 140 $(REF encodeText, dxml, util)$(BR) 141 $(REF StdEntityRef, dxml, util)$(BR) 142 $(REF toCharRef, dxml, util)$(BR) 143 +/ 144 struct XMLWriter(OR) 145 if(isOutputRange!(OR, char)) 146 { 147 import std.range.primitives; 148 import std.traits; 149 150 enum compileInTests = is(OR == XMLWriterCompileTests); 151 152 public: 153 154 155 /++ 156 Writes the first portion of a start tag to the given output range. 157 158 Once openStartTag has been called, 159 $(LREF2 writeAttr, XMLWriter) can be called to add attributes 160 to the start tag. $(LREF2 closeStartTag, XMLWriter) writes the closing 161 portion of the start tag. 162 163 Once openStartTag has been called, it is an error to call any 164 function on XMLWriter other than $(LREF2 closeStartTag, XMLWriter), 165 $(LREF2 writeAttr, XMLWriter), $(LREF2 writeIndent, XMLWriter), 166 $(LREF2 tagDepth, XMLWriter), $(LREF2 baseIndent, XMLWriter), or 167 $(LREF2 output, XMLWriter) until $(LREF2 closeStartTag, XMLWriter) has 168 been called (basically, any function that involves writing XML that is 169 not legal in a start tag can't be called until the start tag has been 170 properly closed). 171 172 It is also an error to call openStartTag after the end tag for the root 173 element has been written. 174 175 Params: 176 name = The name of the start tag. 177 newline = Whether a _newline followed by an indent will be written 178 to the output range before the start tag. 179 180 Throws: $(LREF XMLWritingException) if the given _name is not a valid 181 XML tag _name. 182 183 See_Also: $(LREF2 writeStartTag, XMLWriter)$(BR) 184 $(LREF2 writeAttr, XMLWriter)$(BR) 185 $(LREF2 closeStartTag, XMLWriter)$(BR) 186 $(LREF2 writeEndTag, XMLWriter)$(BR) 187 $(LINK http://www.w3.org/TR/REC-xml/#NT-STag) 188 +/ 189 void openStartTag(string name, Newline newline = Newline.yes) 190 { 191 _validateStartTag!"openStartTag"(name); 192 if(newline == Newline.yes) 193 put(_output, _getIndent(tagDepth)); 194 _startTagOpen = true; 195 _incLevel(name); 196 put(_output, '<'); 197 put(_output, name); 198 } 199 200 // This is so that openStartTag, writeStartTag, and writeTaggedText can 201 // share this code. 202 private void _validateStartTag(string funcName)(string name) 203 { 204 assert(!_startTagOpen, funcName ~ " cannot be called when a start tag is already open"); 205 // FIXME It seems like a bug that version(assert) would be required to 206 // reference a symbol declared with version(assert) when it's being 207 // referenced inside an assertion. 208 version(assert) 209 assert(!_writtenRootEnd, funcName ~ " cannot be called after the root element's end tag has been written."); 210 checkName(name); 211 } 212 213 /// 214 static if(compileInTests) unittest 215 { 216 import std.array : appender; 217 import std.exception : assertThrown; 218 219 auto writer = xmlWriter(appender!string()); 220 221 writer.openStartTag("root", Newline.no); 222 assert(writer.output.data == "<root"); 223 224 writer.closeStartTag(); 225 assert(writer.output.data == "<root>"); 226 227 // Neither < nor > is allowed in a tag name. 228 assertThrown!XMLWritingException(writer.openStartTag("<tag>")); 229 230 // Unchanged after an XMLWritingException is thrown. 231 assert(writer.output.data == "<root>"); 232 233 writer.openStartTag("foo"); 234 assert(writer.output.data == 235 "<root>\n" ~ 236 " <foo"); 237 238 writer.writeAttr("answer", "42"); 239 assert(writer.output.data == 240 "<root>\n" ~ 241 ` <foo answer="42"`); 242 243 writer.closeStartTag(EmptyTag.yes); 244 assert(writer.output.data == 245 "<root>\n" ~ 246 ` <foo answer="42"/>`); 247 248 writer.writeEndTag(); 249 assert(writer.output.data == 250 "<root>\n" ~ 251 ` <foo answer="42"/>` ~ "\n" ~ 252 "</root>"); 253 } 254 255 static if(compileInTests) @safe pure unittest 256 { 257 import dxml.internal : TestAttrOR; 258 auto writer = xmlWriter(TestAttrOR.init); 259 writer.openStartTag("root"); 260 } 261 262 263 /++ 264 Writes an attribute for a start tag to the output range. 265 266 It is an error to call writeAttr except between calls to 267 $(LREF2 openStartTag, XMLWriter) and $(LREF2 closeStartTag, XMLWriter). 268 269 Params: 270 quote = The quote character to use for the attribute value's 271 delimiter. 272 name = The name of the attribute. 273 value = The value of the attribute. 274 newline = Whether a _newline followed by an indent will be written 275 to the output range before the attribute. Note that unlike 276 most write functions, the default is $(D Newline.no) 277 (since it's more common to not want newlines between 278 attributes). 279 280 Throws: $(LREF XMLWritingException) if the given _name is not a valid 281 XML attribute _name, if the given _value is not a valid XML 282 attribute _value, or if the given _name has already been written 283 to the current start tag. $(REF encodeAttr, dxml, util) can be 284 used to encode any characters that are not legal in their 285 literal form in an attribute _value but are legal as entity 286 references. 287 288 See_Also: $(REF encodeAttr, dxml, util)$(BR) 289 $(REF StdEntityRef, dxml, util)$(BR) 290 $(REF toCharRef, dxml, util)$(BR) 291 $(LINK http://www.w3.org/TR/REC-xml/#NT-Attribute) 292 +/ 293 void writeAttr(char quote = '"', R)(string name, R value, Newline newline = Newline.no) 294 if((quote == '"' || quote == '\'') && 295 isForwardRange!R && isSomeChar!(ElementType!R)) 296 { 297 assert(_startTagOpen, "writeAttr cannot be called except when a start tag is open"); 298 299 checkName(name); 300 static if(quote == '"') 301 checkText!(CheckText.attValueQuot)(value.save); 302 else 303 checkText!(CheckText.attValueApos)(value.save); 304 305 import std.algorithm.searching : canFind; 306 if(_attributes.canFind(name)) 307 throw new XMLWritingException("Duplicate attribute name: " ~ name); 308 _attributes ~= name; 309 310 if(newline == Newline.yes) 311 put(_output, _getIndent(tagDepth)); 312 else 313 put(_output, ' '); 314 put(_output, name); 315 put(_output, "=" ~ quote); 316 put(_output, value); 317 put(_output, quote); 318 } 319 320 /// 321 static if(compileInTests) unittest 322 { 323 import std.array : appender; 324 import std.exception : assertThrown; 325 import dxml.util : encodeAttr; 326 327 auto writer = xmlWriter(appender!string()); 328 329 writer.openStartTag("root", Newline.no); 330 assert(writer.output.data == "<root"); 331 332 writer.writeAttr("a", "one"); 333 assert(writer.output.data == `<root a="one"`); 334 335 writer.writeAttr("b", "two"); 336 assert(writer.output.data == `<root a="one" b="two"`); 337 338 // It's illegal for two attributes on the same start tag 339 // to have the same name. 340 assertThrown!XMLWritingException(writer.writeAttr("a", "three")); 341 342 // Invalid name. 343 assertThrown!XMLWritingException(writer.writeAttr("=", "value")); 344 345 // Can't have a quote that matches the enclosing quote. 346 assertThrown!XMLWritingException(writer.writeAttr("c", `foo"bar`)); 347 assertThrown!XMLWritingException(writer.writeAttr!'\''("c", "foo'bar")); 348 349 // Unchanged after an XMLWritingException is thrown. 350 assert(writer.output.data == `<root a="one" b="two"`); 351 352 writer.closeStartTag(); 353 assert(writer.output.data == `<root a="one" b="two">`); 354 355 writer.openStartTag("foobar"); 356 assert(writer.output.data == 357 `<root a="one" b="two">` ~ "\n" ~ 358 " <foobar"); 359 360 // " is the default for the quote character, but ' can be specified. 361 writer.writeAttr!'\''("answer", "42"); 362 assert(writer.output.data == 363 `<root a="one" b="two">` ~ "\n" ~ 364 " <foobar answer='42'"); 365 366 writer.writeAttr("base", "13", Newline.yes); 367 assert(writer.output.data == 368 `<root a="one" b="two">` ~ "\n" ~ 369 " <foobar answer='42'\n" ~ 370 ` base="13"`); 371 372 writer.closeStartTag(); 373 assert(writer.output.data == 374 `<root a="one" b="two">` ~ "\n" ~ 375 " <foobar answer='42'\n" ~ 376 ` base="13">`); 377 378 writer.openStartTag("tag"); 379 assert(writer.output.data == 380 `<root a="one" b="two">` ~ "\n" ~ 381 " <foobar answer='42'\n" ~ 382 ` base="13">` ~ "\n" ~ 383 " <tag"); 384 385 // &, <, and > are not legal in an attribute value. 386 assertThrown!XMLWritingException(writer.writeAttr("foo", "&")); 387 388 // Unchanged after an XMLWritingException is thrown. 389 assert(writer.output.data == 390 `<root a="one" b="two">` ~ "\n" ~ 391 " <foobar answer='42'\n" ~ 392 ` base="13">` ~ "\n" ~ 393 " <tag"); 394 395 // Use dxml.util.encodeAttr to encode characters that aren't 396 // legal in an attribute value but can legally be encoded. 397 writer.writeAttr("foo", encodeAttr("&")); 398 assert(writer.output.data == 399 `<root a="one" b="two">` ~ "\n" ~ 400 " <foobar answer='42'\n" ~ 401 ` base="13">` ~ "\n" ~ 402 ` <tag foo="&"`); 403 404 writer.closeStartTag(EmptyTag.yes); 405 assert(writer.output.data == 406 `<root a="one" b="two">` ~ "\n" ~ 407 " <foobar answer='42'\n" ~ 408 ` base="13">` ~ "\n" ~ 409 ` <tag foo="&"/>`); 410 411 writer.writeEndTag(); 412 writer.writeEndTag(); 413 assert(writer.output.data == 414 `<root a="one" b="two">` ~ "\n" ~ 415 " <foobar answer='42'\n" ~ 416 ` base="13">` ~ "\n" ~ 417 ` <tag foo="&"/>` ~ "\n" ~ 418 " </foobar>\n" ~ 419 "</root>"); 420 } 421 422 static if(compileInTests) unittest 423 { 424 import std.array : appender; 425 import std.exception : assertThrown; 426 import dxml.internal : testRangeFuncs; 427 428 foreach(func; testRangeFuncs) 429 { 430 auto writer = xmlWriter(appender!string); 431 writer.openStartTag("root", Newline.no); 432 writer.writeAttr("a", func("foo")); 433 writer.writeAttr("b", func("bar")); 434 assertThrown!XMLWritingException(writer.writeAttr("a", func("silly"))); 435 assertThrown!XMLWritingException(writer.writeAttr("c", func("&foo"))); 436 assertThrown!XMLWritingException(writer.writeAttr("c", func("\v"))); 437 assertThrown!XMLWritingException(writer.writeAttr("c", func("<"))); 438 assertThrown!XMLWritingException(writer.writeAttr("c", func("foo&bar"))); 439 assertThrown!XMLWritingException(writer.writeAttr("c", func("foo\vbar"))); 440 assertThrown!XMLWritingException(writer.writeAttr("c", func("foo<bar"))); 441 assertThrown!XMLWritingException(writer.writeAttr("c", func(`foo"bar`))); 442 assertThrown!XMLWritingException(writer.writeAttr!'\''("c", func("foo'bar"))); 443 writer.writeAttr("c", func("bar")); 444 writer.writeAttr("d", func("foo&bar;baz"), Newline.yes); 445 writer.writeAttr("e", func("]]>")); 446 writer.writeAttr("f", func("'''")); 447 writer.writeAttr!'\''("g", func(`"""`)); 448 assert(writer._attributes.length == 7); 449 assert(writer.output.data == `<root a="foo" b="bar" c="bar"` ~ "\n" ~ 450 ` d="foo&bar;baz" e="]]>" f="'''" g='"""'`); 451 writer.closeStartTag(); 452 assert(writer._attributes.empty); 453 454 writer.openStartTag("foo"); 455 writer.writeAttr("a", func("foo")); 456 writer.writeAttr("b", func("bar")); 457 writer.writeAttr("c", func("bar")); 458 writer.closeStartTag(EmptyTag.yes); 459 assert(writer._attributes.empty); 460 assert(writer.output.data == `<root a="foo" b="bar" c="bar"` ~ "\n" ~ 461 ` d="foo&bar;baz" e="]]>" f="'''" g='"""'>` ~ "\n" ~ 462 ` <foo a="foo" b="bar" c="bar"/>`); 463 464 writer.openStartTag("foo"); 465 writer.writeAttr("a", func("foo")); 466 writer.writeAttr("b", func("bar")); 467 writer.writeAttr("c", func("bar")); 468 assertThrown!XMLWritingException(writer.writeAttr("c", func("baz"))); 469 writer.closeStartTag(); 470 assert(writer._attributes.empty); 471 assert(writer.output.data == `<root a="foo" b="bar" c="bar"` ~ "\n" ~ 472 ` d="foo&bar;baz" e="]]>" f="'''" g='"""'>` ~ "\n" ~ 473 ` <foo a="foo" b="bar" c="bar"/>` ~ "\n" ~ 474 ` <foo a="foo" b="bar" c="bar">`); 475 } 476 } 477 478 static if(compileInTests) @safe pure unittest 479 { 480 import dxml.internal : TestAttrOR; 481 auto writer = xmlWriter(TestAttrOR.init); 482 writer.openStartTag("root"); 483 writer.writeAttr("attr", "42"); 484 } 485 486 487 /++ 488 Writes the end of a start tag to the ouput range. 489 490 It is an error to call closeStartTag unless a start tag has been opened 491 and not yet closed. 492 493 Params: 494 emptyTag = Whether the start tag will be empty (i.e. terminated with 495 $(D_CODE_STRING "/>") so that there is no corresponding 496 end tag). 497 498 See_Also: $(LREF2 openStartTag, XMLWriter)$(BR) 499 $(LREF2 writeAttr, XMLWriter)$(BR) 500 $(LREF2 writeStartTag, XMLWriter)$(BR) 501 $(LREF2 writeEndTag, XMLWriter)$(BR) 502 $(LINK http://www.w3.org/TR/REC-xml/#NT-STag) 503 +/ 504 void closeStartTag(EmptyTag emptyTag = EmptyTag.no) 505 { 506 assert(_startTagOpen, "closeStartTag cannot be called when a start tag is not open"); 507 if(emptyTag == EmptyTag.yes) 508 { 509 put(_output, "/>"); 510 _decLevel(); 511 } 512 else 513 put(_output, '>'); 514 _startTagOpen = false; 515 _attributes.length = 0; 516 () @trusted { _attributes.assumeSafeAppend(); } (); 517 } 518 519 /// 520 static if(compileInTests) unittest 521 { 522 import std.array : appender; 523 524 auto writer = xmlWriter(appender!string()); 525 526 writer.openStartTag("root", Newline.no); 527 assert(writer.output.data == "<root"); 528 529 writer.closeStartTag(); 530 assert(writer.output.data == "<root>"); 531 532 writer.openStartTag("foo"); 533 assert(writer.output.data == 534 "<root>\n" ~ 535 " <foo"); 536 537 writer.closeStartTag(EmptyTag.yes); 538 assert(writer.output.data == 539 "<root>\n" ~ 540 " <foo/>"); 541 542 writer.writeEndTag(); 543 assert(writer.output.data == 544 "<root>\n" ~ 545 " <foo/>\n" ~ 546 "</root>"); 547 } 548 549 // _decLevel currently can't be pure. 550 static if(compileInTests) @safe /+pure+/ unittest 551 { 552 import dxml.internal : TestAttrOR; 553 auto writer = xmlWriter(TestAttrOR.init); 554 writer.openStartTag("root"); 555 writer.closeStartTag(); 556 } 557 558 559 /++ 560 Writes a start tag with no attributes. 561 562 This is equivalent to calling $(LREF2 openStartTag, XMLWriter) 563 immediately followed by $(LREF2 closeStartTag, XMLWriter). 564 565 It is an error to call writeStartTag after the end tag for the root 566 element has been written. 567 568 Params: 569 name = The name of the start tag. 570 emptyTag = Whether the start tag will be empty (i.e. terminated with 571 $(D_CODE_STRING "/>") so that there is no corresponding 572 end tag). 573 newline = Whether a _newline followed by an indent will be written 574 to the output range before the start tag. 575 576 Throws: $(LREF XMLWritingException) if the given _name is not a valid 577 XML tag _name. 578 579 See_Also: $(LREF2 openStartTag, XMLWriter)$(BR) 580 $(LREF2 writeAttr, XMLWriter)$(BR) 581 $(LREF2 closeStartTag, XMLWriter)$(BR) 582 $(LREF2 writeEndTag, XMLWriter)$(BR) 583 $(LREF writeTaggedText)$(BR) 584 $(LINK http://www.w3.org/TR/REC-xml/#NT-STag)$(BR) 585 $(LINK http://www.w3.org/TR/REC-xml/#NT-ETag) 586 +/ 587 void writeStartTag(string name, EmptyTag emptyTag = EmptyTag.no, Newline newline = Newline.yes) 588 { 589 _validateStartTag!"writeStartTag"(name); 590 _writeStartTag(name, emptyTag, newline); 591 } 592 593 /// Ditto 594 void writeStartTag(string name, Newline newline, EmptyTag emptyTag = EmptyTag.no) 595 { 596 _validateStartTag!"writeStartTag"(name); 597 _writeStartTag(name, emptyTag, newline); 598 } 599 600 // This is so that writeTaggedText can check validate both the name and text 601 // before writing anything to the output range. 602 void _writeStartTag(string name, EmptyTag emptyTag, Newline newline) 603 { 604 if(newline == Newline.yes) 605 put(_output, _getIndent(tagDepth)); 606 put(_output, '<'); 607 put(_output, name); 608 if(emptyTag == EmptyTag.yes) 609 put(_output, "/>"); 610 else 611 { 612 _incLevel(name); 613 put(_output, '>'); 614 } 615 } 616 617 /// 618 static if(compileInTests) unittest 619 { 620 import std.array : appender; 621 import std.exception : assertThrown; 622 623 auto writer = xmlWriter(appender!string()); 624 writer.writeStartTag("root", Newline.no); 625 assert(writer.output.data == "<root>"); 626 627 writer.writeStartTag("foo"); 628 assert(writer.output.data == 629 "<root>\n" ~ 630 " <foo>"); 631 632 // = is not legal in a tag name. 633 assertThrown!XMLWritingException(writer.writeStartTag("=")); 634 635 // Unchanged after an XMLWritingException is thrown. 636 assert(writer.output.data == 637 "<root>\n" ~ 638 " <foo>"); 639 640 writer.writeStartTag("bar", EmptyTag.yes); 641 assert(writer.output.data == 642 "<root>\n" ~ 643 " <foo>\n" ~ 644 " <bar/>"); 645 646 writer.writeStartTag("baz", EmptyTag.yes, Newline.no); 647 assert(writer.output.data == 648 "<root>\n" ~ 649 " <foo>\n" ~ 650 " <bar/><baz/>"); 651 652 writer.writeStartTag("bloop"); 653 assert(writer.output.data == 654 "<root>\n" ~ 655 " <foo>\n" ~ 656 " <bar/><baz/>\n" ~ 657 " <bloop>"); 658 659 writer.writeEndTag(); 660 writer.writeEndTag(); 661 writer.writeEndTag(); 662 assert(writer.output.data == 663 "<root>\n" ~ 664 " <foo>\n" ~ 665 " <bar/><baz/>\n" ~ 666 " <bloop>\n" ~ 667 " </bloop>\n" ~ 668 " </foo>\n" ~ 669 "</root>"); 670 } 671 672 static if(compileInTests) @safe pure unittest 673 { 674 import dxml.internal : TestAttrOR; 675 auto writer = xmlWriter(TestAttrOR.init); 676 writer.writeStartTag("root"); 677 } 678 679 680 /++ 681 Writes an end tag to the output range with the name of the start tag 682 that was most recently written and does not yet have a matching end tag. 683 684 If a name is provided, then it will be validated against the matching 685 start tag. 686 687 Params: 688 name = Name to check against the matching start tag. 689 newline = Whether a _newline followed by an indent will be written 690 to the output range before the end tag. 691 692 Throws: $(LREF XMLWritingException) if no start tag is waiting for a 693 matching end tag or if the given _name does not match the _name 694 of the start tag that needs to be matched next. 695 696 697 See_Also: $(LREF2 openStartTag, XMLWriter)$(BR) 698 $(LREF2 writeAttr, XMLWriter)$(BR) 699 $(LREF2 closeStartTag, XMLWriter)$(BR) 700 $(LREF2 writeEndTag, XMLWriter)$(BR) 701 $(LREF writeTaggedText)$(BR) 702 $(LINK http://www.w3.org/TR/REC-xml/#NT-ETag) 703 +/ 704 void writeEndTag(string name, Newline newline = Newline.yes) 705 { 706 assert(!_startTagOpen, "writeEndTag cannot be called when a start tag is open"); 707 708 if(name != _tagStack.back) 709 { 710 import std.format : format; 711 auto msg = format!"End tag name does not match start tag name: <%s> vs </%s>"(_tagStack.back, name); 712 throw new XMLWritingException(msg); 713 } 714 715 writeEndTag(newline); 716 } 717 718 /// Ditto 719 void writeEndTag(Newline newline = Newline.yes) 720 { 721 assert(!_startTagOpen, "writeEndTag cannot be called when a start tag is open"); 722 723 immutable name = _tagStack.back; 724 _decLevel(); 725 if(newline == Newline.yes) 726 put(_output, _getIndent(tagDepth)); 727 put(_output, "</"); 728 put(_output, name); 729 put(_output, ">"); 730 731 version(assert) 732 _writtenRootEnd = tagDepth == 0; 733 } 734 735 /// 736 static if(compileInTests) unittest 737 { 738 import std.array : appender; 739 import std.exception : assertThrown; 740 741 auto writer = xmlWriter(appender!string()); 742 writer.writeStartTag("root", Newline.no); 743 assert(writer.output.data == "<root>"); 744 745 writer.writeStartTag("foo"); 746 assert(writer.output.data == 747 "<root>\n" ~ 748 " <foo>"); 749 750 // Name doesn't match start tag, which is <foo>. 751 assertThrown!XMLWritingException(writer.writeEndTag("bar")); 752 753 // Unchanged after an XMLWritingException is thrown. 754 assert(writer.output.data == 755 "<root>\n" ~ 756 " <foo>"); 757 758 writer.writeEndTag("foo", Newline.no); 759 assert(writer.output.data == 760 "<root>\n" ~ 761 " <foo></foo>"); 762 763 writer.writeStartTag("bar"); 764 assert(writer.output.data == 765 "<root>\n" ~ 766 " <foo></foo>\n" ~ 767 " <bar>"); 768 769 writer.writeEndTag("bar"); 770 assert(writer.output.data == 771 "<root>\n" ~ 772 " <foo></foo>\n" ~ 773 " <bar>\n" ~ 774 " </bar>"); 775 776 // No name is required, but if it is not provided, then the code cannot 777 // validate that it's writing the end tag that it thinks it's writing. 778 writer.writeEndTag(); 779 assert(writer.output.data == 780 "<root>\n" ~ 781 " <foo></foo>\n" ~ 782 " <bar>\n" ~ 783 " </bar>\n" ~ 784 "</root>"); 785 } 786 787 // _decLevel currently can't be pure. 788 static if(compileInTests) @safe /+pure+/ unittest 789 { 790 import dxml.internal : TestAttrOR; 791 auto writer = xmlWriter(TestAttrOR.init); 792 writer.writeStartTag("root"); 793 writer.writeStartTag("tag"); 794 writer.writeEndTag("tag"); 795 () @safe nothrow { writer.writeEndTag(); } (); 796 } 797 798 799 /++ 800 This writes the text that goes between start tags and end tags. 801 802 It can be called multiple times in a row, and the given text will just 803 end up being appended to the current text field. 804 805 It is an error to call writeText after the end tag for the root element 806 has been written. 807 808 Params: 809 text = The text to write. 810 newline = Whether a _newline followed by an indent will be written 811 to the output range before the text. It will not include 812 an indent if $(D insertIndent == InsertIndent.no). 813 insertIndent = Whether an indent will be inserted after each 814 _newline within the _text. 815 816 Throws: $(LREF XMLWritingException) if any characters or sequence of 817 characters in the given _text are not legal in the _text portion 818 of an XML document. $(REF encodeText, dxml, util) can be used 819 to encode any characters that are not legal in their literal 820 form but are legal as entity references. 821 822 See_Also: $(LREF writeTaggedText)$(BR) 823 $(REF encodeText, dxml, util)$(BR) 824 $(REF StdEntityRef, dxml, util)$(BR) 825 $(REF toCharRef, dxml, util)$(BR) 826 $(LINK http://www.w3.org/TR/REC-xml/#syntax) 827 +/ 828 void writeText(R)(R text, Newline newline = Newline.yes, InsertIndent insertIndent = InsertIndent.yes) 829 if(isForwardRange!R && isSomeChar!(ElementType!R)) 830 { 831 _validateText!"writeText"(text.save); 832 _writeText(text, newline, insertIndent); 833 } 834 835 /// Ditto 836 void writeText(R)(R text, InsertIndent insertIndent, Newline newline = Newline.yes) 837 if(isForwardRange!R && isSomeChar!(ElementType!R)) 838 { 839 _validateText!"writeText"(text.save); 840 _writeText(text, newline, insertIndent); 841 } 842 843 // This is so that openStartTag, writeStartTag, and writeTaggedText can 844 // share this code. 845 private void _validateText(string funcName, R)(R text) 846 { 847 assert(!_startTagOpen, funcName ~ " cannot be called when a start tag is open"); 848 // FIXME It seems like a bug that version(assert) would be required to 849 // reference a symbol declared with version(assert) when it's being 850 // referenced inside an assertion. 851 version(assert) 852 assert(!_writtenRootEnd, funcName ~ " cannot be called after the root end tag has been written"); 853 // In the case of writeTaggedText, the check is done before the start 854 // tag has been written, and because it's writing the start tag, it can 855 // guarantee that the root tag has been written before the text. 856 static if(funcName != "writeTaggedText") 857 assert(tagDepth != 0, funcName ~ " cannot be called before the root start tag has been written"); 858 checkText!(CheckText.text)(text); 859 } 860 861 // This is separated out so that writeTaggedText can call it and not check 862 // the text a second time. 863 private void _writeText(R)(R text, Newline newline, InsertIndent insertIndent) 864 { 865 if(newline == Newline.yes) 866 put(_output, insertIndent == InsertIndent.yes ? _getIndent(tagDepth) : "\n"); 867 if(insertIndent == InsertIndent.yes) 868 _insertIndent(text, tagDepth); 869 else 870 put(_output, text); 871 } 872 873 /// 874 static if(compileInTests) unittest 875 { 876 import std.array : appender; 877 import std.exception : assertThrown; 878 import dxml.util : encodeText; 879 880 { 881 auto writer = xmlWriter(appender!string()); 882 writer.writeStartTag("root", Newline.no); 883 writer.writeStartTag("foo"); 884 885 // By default, a newline is inserted before the text, and the text 886 // is indented. 887 writer.writeText("hello world"); 888 assert(writer.output.data == 889 "<root>\n" ~ 890 " <foo>\n" ~ 891 " hello world"); 892 893 writer.writeEndTag("foo"); 894 assert(writer.output.data == 895 "<root>\n" ~ 896 " <foo>\n" ~ 897 " hello world\n" ~ 898 " </foo>"); 899 900 writer.writeStartTag("foo"); 901 902 // With Newline.no, no newline is inserted prior to the text. 903 writer.writeText("hello world", Newline.no); 904 writer.writeEndTag("foo"); 905 assert(writer.output.data == 906 "<root>\n" ~ 907 " <foo>\n" ~ 908 " hello world\n" ~ 909 " </foo>\n" ~ 910 " <foo>hello world\n" ~ 911 " </foo>"); 912 913 writer.writeStartTag("foo"); 914 writer.writeText("hello world", Newline.no); 915 916 // Newline.no on the end tag also makes it so that there is no 917 // newline after the text. 918 writer.writeEndTag("foo", Newline.no); 919 assert(writer.output.data == 920 "<root>\n" ~ 921 " <foo>\n" ~ 922 " hello world\n" ~ 923 " </foo>\n" ~ 924 " <foo>hello world\n" ~ 925 " </foo>\n" ~ 926 " <foo>hello world</foo>"); 927 928 } 929 { 930 auto writer = xmlWriter(appender!string()); 931 writer.writeStartTag("root", Newline.no); 932 writer.writeStartTag("bar"); 933 934 // By default, if there are newlines in the text, they are indented. 935 writer.writeText("The dish\nran away\nwith the spoon"); 936 writer.writeEndTag("bar"); 937 assert(writer.output.data == 938 "<root>\n" ~ 939 " <bar>\n" ~ 940 " The dish\n" ~ 941 " ran away\n" ~ 942 " with the spoon\n" ~ 943 " </bar>"); 944 } 945 { 946 auto writer = xmlWriter(appender!string()); 947 writer.writeStartTag("root", Newline.no); 948 writer.writeStartTag("bar"); 949 950 // InsertIndent.no tells it to not indent each line. 951 writer.writeText("The dish\nran away\nwith the spoon", 952 InsertIndent.no); 953 writer.writeEndTag("bar"); 954 assert(writer.output.data == 955 "<root>\n" ~ 956 " <bar>\n" ~ 957 "The dish\n" ~ 958 "ran away\n" ~ 959 "with the spoon\n" ~ 960 " </bar>"); 961 } 962 { 963 auto writer = xmlWriter(appender!string()); 964 writer.writeStartTag("root", Newline.no); 965 writer.writeStartTag("bar"); 966 967 // Of course, Newline.no and InsertIndent.no can be combined. 968 writer.writeText("The dish\nran away\nwith the spoon", 969 Newline.no, InsertIndent.no); 970 writer.writeEndTag("bar"); 971 assert(writer.output.data == 972 "<root>\n" ~ 973 " <bar>The dish\n" ~ 974 "ran away\n" ~ 975 "with the spoon\n" ~ 976 " </bar>"); 977 } 978 { 979 auto writer = xmlWriter(appender!string()); 980 writer.writeStartTag("root", Newline.no); 981 writer.writeStartTag("code"); 982 assert(writer.output.data == 983 "<root>\n" ~ 984 " <code>"); 985 986 auto text = "if(--foo > bar && bar < baz)\n" ~ 987 " doSomething();"; 988 989 // & and < are not legal in XML text. 990 assertThrown!XMLWritingException(writer.writeText(text)); 991 992 // Unchanged after an XMLWritingException is thrown. 993 assert(writer.output.data == 994 "<root>\n" ~ 995 " <code>"); 996 997 // Use dxml.util.encodeText to encode characters that aren't 998 // legal in a text field but can legally be encoded. 999 writer.writeText(encodeText(text)); 1000 writer.writeEndTag("code"); 1001 assert(writer.output.data == 1002 "<root>\n" ~ 1003 " <code>\n" ~ 1004 " if(--foo > bar && bar < baz)\n" ~ 1005 " doSomething();\n" ~ 1006 " </code>"); 1007 } 1008 } 1009 1010 static if(compileInTests) unittest 1011 { 1012 import std.array : appender; 1013 import std.exception : assertThrown; 1014 import dxml.internal : testRangeFuncs; 1015 1016 foreach(func; testRangeFuncs) 1017 { 1018 auto writer = xmlWriter(appender!string); 1019 writer.writeStartTag("root", Newline.no); 1020 writer.writeText(func("hello sally"), Newline.no); 1021 assertThrown!XMLWritingException(writer.writeText(func("&foo"))); 1022 assertThrown!XMLWritingException(writer.writeText(func("\v"))); 1023 assertThrown!XMLWritingException(writer.writeText(func("<"))); 1024 assertThrown!XMLWritingException(writer.writeText(func("]]>"))); 1025 assertThrown!XMLWritingException(writer.writeText(func("foo&bar"))); 1026 assertThrown!XMLWritingException(writer.writeText(func("foo\vbar"))); 1027 assertThrown!XMLWritingException(writer.writeText(func("foo<bar"))); 1028 assertThrown!XMLWritingException(writer.writeText(func("foo]]>bar"))); 1029 writer.writeText(func("&foo;"), Newline.no); 1030 writer.writeText(func("] ]> > goodbye jack")); 1031 writer.writeText(func("so silly\nindeed\nindeed")); 1032 writer.writeText(func("foo&bar; \n baz\nfrobozz"), InsertIndent.no); 1033 assert(writer.output.data == 1034 "<root>hello sally&foo;\n ] ]> > goodbye jack\n" ~ 1035 " so silly\n" ~ 1036 " indeed\n" ~ 1037 " indeed\n" ~ 1038 "foo&bar; \n" ~ 1039 " baz\n" ~ 1040 "frobozz"); 1041 } 1042 } 1043 1044 static if(compileInTests) @safe pure unittest 1045 { 1046 import dxml.internal : TestAttrOR; 1047 auto writer = xmlWriter(TestAttrOR.init); 1048 writer.writeStartTag("root"); 1049 writer.writeText(""); 1050 } 1051 1052 1053 /++ 1054 Writes a comment to the output range. 1055 1056 Params: 1057 text = The text of the comment. 1058 newline = Whether a _newline followed by an indent will be written 1059 to the output range before the comment tag. 1060 insertIndent = Whether an indent will be inserted after each 1061 _newline within the _text. 1062 1063 Throws: $(LREF XMLWritingException) if the given _text contains 1064 $(D_CODE_STRING "--") or ends with $(D_CODE_STRING "-"). 1065 1066 See_Also: $(LINK http://www.w3.org/TR/REC-xml/#NT-Comment) 1067 +/ 1068 void writeComment(R)(R text, Newline newline = Newline.yes, InsertIndent insertIndent = InsertIndent.yes) 1069 if(isForwardRange!R && isSomeChar!(ElementType!R)) 1070 { 1071 assert(!_startTagOpen, "writeComment cannot be called when a start tag is open"); 1072 checkText!(CheckText.comment)(text.save); 1073 if(newline == Newline.yes) 1074 put(_output, _getIndent(tagDepth)); 1075 put(_output, "<!--"); 1076 if(insertIndent == InsertIndent.yes) 1077 _insertIndent(text, tagDepth + 1); 1078 else 1079 put(_output, text); 1080 put(_output, "-->"); 1081 } 1082 1083 /// Ditto 1084 void writeComment(R)(R text, InsertIndent insertIndent, Newline newline = Newline.yes) 1085 if(isForwardRange!R && isSomeChar!(ElementType!R)) 1086 { 1087 writeComment(text, newline, insertIndent); 1088 } 1089 1090 /// 1091 static if(compileInTests) unittest 1092 { 1093 import std.array : appender; 1094 import std.exception : assertThrown; 1095 1096 auto writer = xmlWriter(appender!string()); 1097 1098 writer.writeComment(" And so it begins... ", Newline.no); 1099 writer.writeStartTag("root"); 1100 writer.writeComment("A comment"); 1101 writer.writeComment("Another comment"); 1102 writer.writeComment("No preceding newline", Newline.no); 1103 writer.writeComment("A comment\nwith a newline"); 1104 writer.writeComment("Another newline\nbut no indent", 1105 InsertIndent.no); 1106 writer.writeStartTag("tag"); 1107 writer.writeComment("Deeper comment"); 1108 writer.writeEndTag("tag"); 1109 writer.writeEndTag("root"); 1110 writer.writeComment(" And so it ends... "); 1111 1112 assert(writer.output.data == 1113 "<!-- And so it begins... -->\n" ~ 1114 "<root>\n" ~ 1115 " <!--A comment-->\n" ~ 1116 " <!--Another comment--><!--No preceding newline-->\n" ~ 1117 " <!--A comment\n" ~ 1118 " with a newline-->\n" ~ 1119 " <!--Another newline\n" ~ 1120 "but no indent-->\n" ~ 1121 " <tag>\n" ~ 1122 " <!--Deeper comment-->\n" ~ 1123 " </tag>\n" ~ 1124 "</root>\n" ~ 1125 "<!-- And so it ends... -->"); 1126 1127 // -- is not legal in an XML comment. 1128 assertThrown!XMLWritingException(writer.writeComment("foo--bar")); 1129 1130 // - is not legal at the end of an XML comment. 1131 assertThrown!XMLWritingException(writer.writeComment("foo-")); 1132 1133 // Unchanged after an XMLWritingException is thrown. 1134 assert(writer.output.data == 1135 "<!-- And so it begins... -->\n" ~ 1136 "<root>\n" ~ 1137 " <!--A comment-->\n" ~ 1138 " <!--Another comment--><!--No preceding newline-->\n" ~ 1139 " <!--A comment\n" ~ 1140 " with a newline-->\n" ~ 1141 " <!--Another newline\n" ~ 1142 "but no indent-->\n" ~ 1143 " <tag>\n" ~ 1144 " <!--Deeper comment-->\n" ~ 1145 " </tag>\n" ~ 1146 "</root>\n" ~ 1147 "<!-- And so it ends... -->"); 1148 } 1149 1150 static if(compileInTests) unittest 1151 { 1152 import std.array : appender; 1153 import std.exception : assertThrown; 1154 import dxml.internal : testRangeFuncs; 1155 1156 foreach(func; testRangeFuncs) 1157 { 1158 auto writer = xmlWriter(appender!string); 1159 writer.writeComment(func("hello sally"), Newline.no); 1160 assertThrown!XMLWritingException(writer.writeComment(func("-"))); 1161 assertThrown!XMLWritingException(writer.writeComment(func("--"))); 1162 assertThrown!XMLWritingException(writer.writeComment(func("--foobar"))); 1163 assertThrown!XMLWritingException(writer.writeComment(func("-foobar-"))); 1164 assertThrown!XMLWritingException(writer.writeComment(func("foobar-"))); 1165 writer.writeComment(func("-foobar")); 1166 writer.writeComment(func("&foo &bar &baz;")); 1167 writer.writeComment(func("&foo \n &bar\n&baz;")); 1168 writer.writeComment(func("&foo \n &bar\n&baz;"), InsertIndent.no); 1169 assert(writer.output.data == 1170 "<!--hello sally-->\n" ~ 1171 "<!---foobar-->\n" ~ 1172 "<!--&foo &bar &baz;-->\n" ~ 1173 "<!--&foo \n" ~ 1174 " &bar\n" ~ 1175 " &baz;-->\n" ~ 1176 "<!--&foo \n" ~ 1177 " &bar\n" ~ 1178 "&baz;-->"); 1179 } 1180 } 1181 1182 static if(compileInTests) @safe pure unittest 1183 { 1184 import dxml.internal : TestAttrOR; 1185 auto writer = xmlWriter(TestAttrOR.init); 1186 writer.writeComment(""); 1187 } 1188 1189 1190 /++ 1191 Writes a $(D <![CDATA[...]]>) section with the given text between the 1192 brackets. 1193 1194 Params: 1195 text = The text of the CDATA section. 1196 newline = Whether a _newline followed by an indent will be written 1197 to the output range before the cdata section. 1198 insertIndent = Whether an indent will be inserted after each 1199 _newline within the _text. 1200 1201 Throws: $(LREF XMLWritingException) if the given _text contains 1202 $(D_CODE_STRING "]]>"). 1203 1204 See_Also: $(LINK http://www.w3.org/TR/REC-xml/#NT-CDSect) 1205 +/ 1206 void writeCDATA(R)(R text, Newline newline = Newline.yes, InsertIndent insertIndent = InsertIndent.yes) 1207 if(isForwardRange!R && isSomeChar!(ElementType!R)) 1208 { 1209 assert(!_startTagOpen, "writeCDATA cannot be called when a start tag is open"); 1210 checkText!(CheckText.cdata)(text.save); 1211 if(newline == Newline.yes) 1212 put(_output, _getIndent(tagDepth)); 1213 put(_output, "<![CDATA["); 1214 if(insertIndent == InsertIndent.yes) 1215 _insertIndent(text, tagDepth + 1); 1216 else 1217 put(_output, text); 1218 put(_output, "]]>"); 1219 } 1220 1221 /// Ditto 1222 void writeCDATA(R)(R text, InsertIndent insertIndent, Newline newline = Newline.yes) 1223 if(isForwardRange!R && isSomeChar!(ElementType!R)) 1224 { 1225 writeCDATA(text, newline, insertIndent); 1226 } 1227 1228 /// 1229 static if(compileInTests) unittest 1230 { 1231 import std.array : appender; 1232 import std.exception : assertThrown; 1233 1234 auto writer = xmlWriter(appender!string()); 1235 1236 writer.writeStartTag("root", Newline.no); 1237 writer.writeCDATA("see data run"); 1238 writer.writeCDATA("More data"); 1239 writer.writeCDATA("No preceding newline", Newline.no); 1240 writer.writeCDATA("some data\nwith a newline"); 1241 writer.writeCDATA("Another newline\nbut no indent", InsertIndent.no); 1242 writer.writeStartTag("tag"); 1243 writer.writeCDATA(" Deeper data <><> "); 1244 writer.writeEndTag("tag"); 1245 writer.writeEndTag("root"); 1246 1247 assert(writer.output.data == 1248 "<root>\n" ~ 1249 " <![CDATA[see data run]]>\n" ~ 1250 " <![CDATA[More data]]><![CDATA[No preceding newline]]>\n" ~ 1251 " <![CDATA[some data\n" ~ 1252 " with a newline]]>\n" ~ 1253 " <![CDATA[Another newline\n" ~ 1254 "but no indent]]>\n" ~ 1255 " <tag>\n" ~ 1256 " <![CDATA[ Deeper data <><> ]]>\n" ~ 1257 " </tag>\n" ~ 1258 "</root>"); 1259 1260 // ]]> is not legal in a CDATA section. 1261 assertThrown!XMLWritingException(writer.writeCDATA("]]>")); 1262 1263 // Unchanged after an XMLWritingException is thrown. 1264 assert(writer.output.data == 1265 "<root>\n" ~ 1266 " <![CDATA[see data run]]>\n" ~ 1267 " <![CDATA[More data]]><![CDATA[No preceding newline]]>\n" ~ 1268 " <![CDATA[some data\n" ~ 1269 " with a newline]]>\n" ~ 1270 " <![CDATA[Another newline\n" ~ 1271 "but no indent]]>\n" ~ 1272 " <tag>\n" ~ 1273 " <![CDATA[ Deeper data <><> ]]>\n" ~ 1274 " </tag>\n" ~ 1275 "</root>"); 1276 } 1277 1278 static if(compileInTests) unittest 1279 { 1280 import std.array : appender; 1281 import std.exception : assertThrown; 1282 import dxml.internal : testRangeFuncs; 1283 1284 foreach(func; testRangeFuncs) 1285 { 1286 auto writer = xmlWriter(appender!string); 1287 writer.writeStartTag("root", Newline.no); 1288 writer.writeCDATA(func("hello sally"), Newline.no); 1289 assertThrown!XMLWritingException(writer.writeCDATA(func("]]>"))); 1290 writer.writeCDATA(func("]] ]> ] ]>")); 1291 writer.writeCDATA(func("--foobar--")); 1292 writer.writeCDATA(func("&foo &bar &baz;")); 1293 writer.writeCDATA(func("&foo \n &bar\n&baz;")); 1294 writer.writeCDATA(func("&foo \n &bar\n&baz;"), InsertIndent.no); 1295 assert(writer.output.data == 1296 "<root><![CDATA[hello sally]]>\n" ~ 1297 " <![CDATA[]] ]> ] ]>]]>\n" ~ 1298 " <![CDATA[--foobar--]]>\n" ~ 1299 " <![CDATA[&foo &bar &baz;]]>\n" ~ 1300 " <![CDATA[&foo \n" ~ 1301 " &bar\n" ~ 1302 " &baz;]]>\n" ~ 1303 " <![CDATA[&foo \n" ~ 1304 " &bar\n" ~ 1305 "&baz;]]>"); 1306 } 1307 } 1308 1309 static if(compileInTests) @safe pure unittest 1310 { 1311 import dxml.internal : TestAttrOR; 1312 auto writer = xmlWriter(TestAttrOR.init); 1313 writer.writeStartTag("root"); 1314 writer.writeCDATA(""); 1315 } 1316 1317 1318 /++ 1319 Writes a parsing instruction to the output range. 1320 1321 Params: 1322 name = The name of the parsing instruction. 1323 text = The text of the parsing instruction. 1324 newline = Whether a _newline followed by an indent will be written 1325 to the output range before the processing instruction. 1326 insertIndent = Whether an indent will be inserted after each 1327 _newline within the _text. 1328 1329 Throws: $(LREF XMLWritingException) if the given _name or _text is not 1330 legal in an XML processing instruction. 1331 1332 See_Also: $(LINK http://www.w3.org/TR/REC-xml/#NT-PI) 1333 +/ 1334 void writePI(R)(R name, Newline newline = Newline.yes) 1335 if(isForwardRange!R && isSomeChar!(ElementType!R)) 1336 { 1337 assert(!_startTagOpen, "writePI cannot be called when a start tag is open"); 1338 checkPIName(name.save); 1339 if(newline == Newline.yes) 1340 put(_output, _getIndent(tagDepth)); 1341 put(_output, "<?"); 1342 put(_output, name); 1343 put(_output, "?>"); 1344 } 1345 1346 /// Ditto 1347 void writePI(R1, R2)(R1 name, R2 text, Newline newline = Newline.yes, InsertIndent insertIndent = InsertIndent.yes) 1348 if(isForwardRange!R1 && isSomeChar!(ElementType!R1) && 1349 isForwardRange!R2 && isSomeChar!(ElementType!R2)) 1350 { 1351 assert(!_startTagOpen, "writePI cannot be called when a start tag is open"); 1352 checkPIName(name.save); 1353 checkText!(CheckText.pi)(text.save); 1354 if(newline == Newline.yes) 1355 put(_output, _getIndent(tagDepth)); 1356 put(_output, "<?"); 1357 put(_output, name); 1358 put(_output, ' '); 1359 if(insertIndent == InsertIndent.yes) 1360 _insertIndent(text, tagDepth + 1); 1361 else 1362 put(_output, text); 1363 put(_output, "?>"); 1364 } 1365 1366 /// Ditto 1367 void writePI(R1, R2)(R1 name, R2 text, InsertIndent insertIndent, Newline newline = Newline.yes) 1368 if(isForwardRange!R1 && isSomeChar!(ElementType!R1) && 1369 isForwardRange!R2 && isSomeChar!(ElementType!R2)) 1370 { 1371 writePI(name, text, newline, insertIndent); 1372 } 1373 1374 /// 1375 static if(compileInTests) unittest 1376 { 1377 import std.array : appender; 1378 import std.exception : assertThrown; 1379 1380 auto writer = xmlWriter(appender!string()); 1381 1382 writer.writePI("pi", Newline.no); 1383 writer.writeStartTag("root"); 1384 writer.writePI("Poirot", "has a cane"); 1385 writer.writePI("Sherlock"); 1386 writer.writePI("No", "preceding newline", Newline.no); 1387 writer.writePI("Ditto", Newline.no); 1388 writer.writePI("target", "some data\nwith a newline"); 1389 writer.writePI("name", "Another newline\nbut no indent", 1390 InsertIndent.no); 1391 writer.writeStartTag("tag"); 1392 writer.writePI("Deep", "Thought"); 1393 writer.writeEndTag("tag"); 1394 writer.writeEndTag("root"); 1395 1396 assert(writer.output.data == 1397 "<?pi?>\n" ~ 1398 "<root>\n" ~ 1399 " <?Poirot has a cane?>\n" ~ 1400 " <?Sherlock?><?No preceding newline?><?Ditto?>\n" ~ 1401 " <?target some data\n" ~ 1402 " with a newline?>\n" ~ 1403 " <?name Another newline\n" ~ 1404 "but no indent?>\n" ~ 1405 " <tag>\n" ~ 1406 " <?Deep Thought?>\n" ~ 1407 " </tag>\n" ~ 1408 "</root>"); 1409 1410 // The name xml (no matter the casing) is illegal as a name for 1411 // processing instructions (so that it can't be confused for the 1412 // optional <?xml...> declaration at the top of an XML document). 1413 assertThrown!XMLWritingException(writer.writePI("xml", "bar")); 1414 1415 // ! is not legal in a processing instruction's name. 1416 assertThrown!XMLWritingException(writer.writePI("!", "bar")); 1417 1418 // ?> is not legal in a processing instruction. 1419 assertThrown!XMLWritingException(writer.writePI("foo", "?>")); 1420 1421 // Unchanged after an XMLWritingException is thrown. 1422 assert(writer.output.data == 1423 "<?pi?>\n" ~ 1424 "<root>\n" ~ 1425 " <?Poirot has a cane?>\n" ~ 1426 " <?Sherlock?><?No preceding newline?><?Ditto?>\n" ~ 1427 " <?target some data\n" ~ 1428 " with a newline?>\n" ~ 1429 " <?name Another newline\n" ~ 1430 "but no indent?>\n" ~ 1431 " <tag>\n" ~ 1432 " <?Deep Thought?>\n" ~ 1433 " </tag>\n" ~ 1434 "</root>"); 1435 } 1436 1437 static if(compileInTests) unittest 1438 { 1439 import std.array : appender; 1440 import std.exception : assertThrown; 1441 import dxml.internal : testRangeFuncs; 1442 1443 foreach(func1; testRangeFuncs) 1444 { 1445 foreach(func2; testRangeFuncs) 1446 { 1447 auto writer = xmlWriter(appender!string); 1448 writer.writePI(func1("hello"), Newline.no); 1449 writer.writePI(func1("hello"), func2("sally")); 1450 assertThrown!XMLWritingException(writer.writePI(func1("hello sally"))); 1451 assertThrown!XMLWritingException(writer.writePI(func1("?"))); 1452 assertThrown!XMLWritingException(writer.writePI(func1("foo"), func2("?>"))); 1453 assertThrown!XMLWritingException(writer.writePI(func1("-"))); 1454 assertThrown!XMLWritingException(writer.writePI(func1("--"))); 1455 assertThrown!XMLWritingException(writer.writePI(func1(".foo"))); 1456 writer.writePI(func1("f."), func2(".foo")); 1457 writer.writePI(func1("f."), func2("?")); 1458 writer.writePI(func1("f."), func2("? >")); 1459 writer.writePI(func1("a"), func2("&foo &bar &baz;")); 1460 writer.writePI(func1("a"), func2("&foo \n &bar\n&baz;")); 1461 writer.writePI(func1("pi"), func2("&foo \n &bar\n&baz;"), InsertIndent.no); 1462 assert(writer.output.data == 1463 "<?hello?>\n" ~ 1464 "<?hello sally?>\n" ~ 1465 "<?f. .foo?>\n" ~ 1466 "<?f. ??>\n" ~ 1467 "<?f. ? >?>\n" ~ 1468 "<?a &foo &bar &baz;?>\n" ~ 1469 "<?a &foo \n" ~ 1470 " &bar\n" ~ 1471 " &baz;?>\n" ~ 1472 "<?pi &foo \n" ~ 1473 " &bar\n" ~ 1474 "&baz;?>"); 1475 } 1476 } 1477 } 1478 1479 static if(compileInTests) @safe pure unittest 1480 { 1481 import dxml.internal : TestAttrOR; 1482 auto writer = xmlWriter(TestAttrOR.init); 1483 writer.writePI("name"); 1484 writer.writePI("name", "text"); 1485 } 1486 1487 1488 /++ 1489 The current depth of the tag stack. 1490 +/ 1491 @property int tagDepth() @safe const pure nothrow @nogc 1492 { 1493 return cast(int)_tagStack.length; 1494 } 1495 1496 /// 1497 static if(compileInTests) unittest 1498 { 1499 import std.array : appender; 1500 1501 auto writer = xmlWriter(appender!string()); 1502 assert(writer.tagDepth == 0); 1503 1504 writer.writeStartTag("root", Newline.no); 1505 assert(writer.tagDepth == 1); 1506 assert(writer.output.data == "<root>"); 1507 1508 writer.writeStartTag("a"); 1509 assert(writer.tagDepth == 2); 1510 assert(writer.output.data == 1511 "<root>\n" ~ 1512 " <a>"); 1513 1514 // The tag depth is increased as soon as a start tag is opened, so 1515 // any calls to writeIndent or writeAttr while a start tag is open 1516 // will use the same tag depth as the children of the start tag. 1517 writer.openStartTag("b"); 1518 assert(writer.tagDepth == 3); 1519 assert(writer.output.data == 1520 "<root>\n" ~ 1521 " <a>\n" ~ 1522 " <b"); 1523 1524 writer.closeStartTag(); 1525 assert(writer.tagDepth == 3); 1526 assert(writer.output.data == 1527 "<root>\n" ~ 1528 " <a>\n" ~ 1529 " <b>"); 1530 1531 writer.writeEndTag("b"); 1532 assert(writer.tagDepth == 2); 1533 assert(writer.output.data == 1534 "<root>\n" ~ 1535 " <a>\n" ~ 1536 " <b>\n" ~ 1537 " </b>"); 1538 1539 // Only start tags and end tags affect the tag depth. 1540 writer.writeComment("comment"); 1541 assert(writer.tagDepth == 2); 1542 assert(writer.output.data == 1543 "<root>\n" ~ 1544 " <a>\n" ~ 1545 " <b>\n" ~ 1546 " </b>\n" ~ 1547 " <!--comment-->"); 1548 1549 writer.writeEndTag("a"); 1550 assert(writer.tagDepth == 1); 1551 assert(writer.output.data == 1552 "<root>\n" ~ 1553 " <a>\n" ~ 1554 " <b>\n" ~ 1555 " </b>\n" ~ 1556 " <!--comment-->\n" ~ 1557 " </a>"); 1558 1559 writer.writeEndTag("root"); 1560 assert(writer.tagDepth == 0); 1561 assert(writer.output.data == 1562 "<root>\n" ~ 1563 " <a>\n" ~ 1564 " <b>\n" ~ 1565 " </b>\n" ~ 1566 " <!--comment-->\n" ~ 1567 " </a>\n" ~ 1568 "</root>"); 1569 } 1570 1571 1572 /++ 1573 The text that will be written for each level of the tag depth when an 1574 indent is written. 1575 +/ 1576 @property string baseIndent() @safe const pure nothrow @nogc 1577 { 1578 return _baseIndent; 1579 } 1580 1581 /// 1582 static if(compileInTests) unittest 1583 { 1584 import std.array : appender; 1585 { 1586 auto writer = xmlWriter(appender!string()); 1587 assert(writer.baseIndent == " "); 1588 } 1589 { 1590 auto writer = xmlWriter(appender!string(), " "); 1591 assert(writer.baseIndent == " "); 1592 } 1593 { 1594 auto writer = xmlWriter(appender!string(), "\t"); 1595 assert(writer.baseIndent == "\t"); 1596 } 1597 } 1598 1599 1600 /++ 1601 Writes a newline followed by an indent to the output range. 1602 1603 In general, the various write functions already provide this 1604 functionality via their $(LREF Newline) parameter, but there may be 1605 cases where it is desirable to insert a newline independently of calling 1606 a write function. 1607 1608 If arbitrary whitespace needs to be inserted, then 1609 $(LREF2 output, XMLWriter) can be used to get at the output range so 1610 that it can be written to directly. 1611 +/ 1612 void writeIndent() 1613 { 1614 put(_output, _getIndent(tagDepth)); 1615 } 1616 1617 /// 1618 static if(compileInTests) unittest 1619 { 1620 import std.array : appender; 1621 1622 auto writer = xmlWriter(appender!string()); 1623 writer.writeStartTag("root", Newline.no); 1624 assert(writer.output.data == "<root>"); 1625 1626 writer.writeIndent(); 1627 assert(writer.output.data == 1628 "<root>\n" ~ 1629 " "); 1630 1631 writer.writeStartTag("foo"); 1632 assert(writer.output.data == 1633 "<root>\n" ~ 1634 " \n" ~ 1635 " <foo>"); 1636 1637 writer.writeIndent(); 1638 assert(writer.output.data == 1639 "<root>\n" ~ 1640 " \n" ~ 1641 " <foo>\n" ~ 1642 " "); 1643 1644 writer.writeText("some text"); 1645 assert(writer.output.data == 1646 "<root>\n" ~ 1647 " \n" ~ 1648 " <foo>\n" ~ 1649 " \n" ~ 1650 " some text"); 1651 1652 writer.writeIndent(); 1653 assert(writer.output.data == 1654 "<root>\n" ~ 1655 " \n" ~ 1656 " <foo>\n" ~ 1657 " \n" ~ 1658 " some text\n" ~ 1659 " "); 1660 1661 writer.writeEndTag(); 1662 writer.writeEndTag(); 1663 assert(writer.output.data == 1664 "<root>\n" ~ 1665 " \n" ~ 1666 " <foo>\n" ~ 1667 " \n" ~ 1668 " some text\n" ~ 1669 " \n" ~ 1670 " </foo>\n" ~ 1671 "</root>"); 1672 } 1673 1674 static if(compileInTests) unittest 1675 { 1676 import std.array : appender; 1677 1678 { 1679 auto writer = xmlWriter(appender!string(), "\t"); 1680 writer.writeIndent(); 1681 assert(writer.output.data == "\n"); 1682 writer.writeStartTag("root", Newline.no); 1683 assert(writer.output.data == "\n<root>"); 1684 writer.writeIndent(); 1685 assert(writer.output.data == "\n<root>\n\t"); 1686 writer.writeEndTag(Newline.no); 1687 assert(writer.output.data == "\n<root>\n\t</root>"); 1688 writer.writeIndent(); 1689 assert(writer.output.data == "\n<root>\n\t</root>\n"); 1690 } 1691 { 1692 auto writer = xmlWriter(appender!string(), ""); 1693 writer.writeIndent(); 1694 assert(writer.output.data == "\n"); 1695 writer.writeStartTag("root", Newline.no); 1696 assert(writer.output.data == "\n<root>"); 1697 writer.writeIndent(); 1698 assert(writer.output.data == "\n<root>\n"); 1699 writer.writeEndTag(Newline.no); 1700 assert(writer.output.data == "\n<root>\n</root>"); 1701 writer.writeIndent(); 1702 assert(writer.output.data == "\n<root>\n</root>\n"); 1703 } 1704 } 1705 1706 static if(compileInTests) @safe pure nothrow unittest 1707 { 1708 import dxml.internal : TestAttrOR; 1709 auto writer = xmlWriter(TestAttrOR.init); 1710 writer.writeIndent(); 1711 } 1712 1713 1714 /++ 1715 Provides access to the _output range that's used by XMLWriter. 1716 1717 Note that any is data written to the _output range without using 1718 XMLWriter could result in invalid XML. 1719 1720 This property is here primarily to provide easy access to the output 1721 range when XMLWriter is done writing (e.g. to get at its $(D data) 1722 member if it's a $(PHOBOS_REF Appender, std, array)), but programs can 1723 use it to write other data (such as whitespace other than the indent) 1724 to the output range while XMLWriter is still writing so long as it's 1725 understood that unlike when the XMLWriter's write functions are called, 1726 calling $(D put) on the output range directly is unchecked and 1727 therefore does risk making the XML invalid. 1728 1729 Also, depending on the type of the _output range, copying it will cause 1730 problems (e.g. if it's not a reference type, writing to a copy may not 1731 write to the _output range inside of XMLWriter), So in general, if the 1732 _output range is going to be written to, it should be written to by 1733 using output directly rather than assigning it to a variable. 1734 +/ 1735 @property ref output() @safe pure nothrow @nogc 1736 { 1737 return _output; 1738 } 1739 1740 1741 // See main ddoc comment for XMLWriter. 1742 @disable this(); 1743 @disable this(this); 1744 @disable void opAssign(XMLWriter); 1745 1746 1747 /++ 1748 In general, it's more user-friendly to use $(LREF xmlWriter) rather than 1749 calling the constructor directly, because then the type of the output 1750 range can be inferred. However, in the case where a pointer is desirable, 1751 then the constructor needs to be called instead of $(LREF xmlWriter). 1752 1753 Params: 1754 output = The _output range that the XML will be written to. 1755 baseIndent = Optional argument indicating the base indent to be 1756 used when an indent is inserted after a newline in the 1757 XML (with the actual indent being the base indent 1758 inserted once for each level of the 1759 $(LREF2 tagDepth, XMLWriter)). The default is four 1760 spaces. 1761 1762 See_Also: $(LREF xmlWriter) 1763 +/ 1764 this(OR output, string baseIndent = " ") 1765 { 1766 import std.algorithm.searching : find; 1767 import std.utf : byCodeUnit; // Allows this code to be nothrow 1768 1769 assert(baseIndent.byCodeUnit().find!(a => a != ' ' && a != '\t')().empty, 1770 "XMLWriter's base indent can only contain ' ' and '\t'"); 1771 1772 _output = output; 1773 _tagStack.reserve(10); 1774 _attributes.reserve(10); 1775 1776 static makeIndent(string baseIndent) pure @safe nothrow 1777 { 1778 import std.array : uninitializedArray; 1779 1780 immutable indentLen = baseIndent.length; 1781 auto retval = uninitializedArray!(char[])(indentLen * 10 + 1); 1782 retval[0] = '\n'; 1783 foreach(i; 0 .. 10) 1784 { 1785 immutable start = i * indentLen + 1; 1786 retval[start .. start + indentLen] = baseIndent; 1787 } 1788 return retval; 1789 } 1790 1791 _baseIndent = baseIndent; 1792 _totalIndent = makeIndent(_baseIndent); 1793 } 1794 1795 /// 1796 static if(compileInTests) unittest 1797 { 1798 import std.array : Appender, appender; 1799 1800 auto writer = new XMLWriter!(Appender!string)(appender!string()); 1801 writer.writeStartTag("root", Newline.no, EmptyTag.yes); 1802 assert(writer.output.data == "<root/>"); 1803 } 1804 1805 1806 private: 1807 1808 void _incLevel(string tagName) @safe pure nothrow 1809 { 1810 _tagStack ~= tagName; 1811 } 1812 1813 1814 void _decLevel() @safe /+pure+/ nothrow 1815 { 1816 --_tagStack.length; 1817 () @trusted { _tagStack.assumeSafeAppend(); } (); 1818 } 1819 1820 1821 string _getIndent(int depth) @safe pure nothrow 1822 { 1823 immutable targetLen = _baseIndent.length * depth + 1; 1824 while(targetLen > _totalIndent.length) 1825 _totalIndent ~= _baseIndent; 1826 return _totalIndent[0 .. targetLen]; 1827 } 1828 1829 static if(compileInTests) unittest 1830 { 1831 import std.array : appender, replicate; 1832 1833 { 1834 auto writer = xmlWriter(appender!string()); 1835 // We want to make sure that we have to append to _totalIndent at 1836 // least once. 1837 foreach(i; 0 .. 20) 1838 assert(writer._getIndent(i) == "\n" ~ " ".replicate(i)); 1839 foreach_reverse(i; 0 .. 20) 1840 assert(writer._getIndent(i) == "\n" ~ " ".replicate(i)); 1841 } 1842 { 1843 immutable indent = " "; 1844 auto writer = xmlWriter(appender!string(), indent); 1845 foreach(i; 0 .. 20) 1846 assert(writer._getIndent(i) == "\n" ~ indent.replicate(i)); 1847 foreach_reverse(i; 0 .. 20) 1848 assert(writer._getIndent(i) == "\n" ~ indent.replicate(i)); 1849 } 1850 } 1851 1852 1853 void _insertIndent(R)(R text, int depth) 1854 { 1855 import std.algorithm.searching : find; 1856 import std.range : takeExactly; 1857 import std.utf : byCodeUnit; 1858 1859 auto bcu = text.byCodeUnit(); 1860 static if(hasLength!(typeof(bcu)) && hasSlicing!(typeof(bcu))) 1861 { 1862 while(true) 1863 { 1864 auto found = bcu.save.find('\n'); 1865 if(found.empty) 1866 { 1867 put(_output, bcu); 1868 break; 1869 } 1870 put(_output, bcu[0 .. bcu.length - found.length]); 1871 put(_output, _getIndent(depth)); 1872 bcu = found[1 .. found.length]; 1873 } 1874 } 1875 else 1876 { 1877 foreach(c; bcu) 1878 { 1879 if(c == '\n') 1880 put(_output, _getIndent(depth)); 1881 else 1882 put(_output, c); 1883 } 1884 } 1885 } 1886 1887 1888 OR _output; 1889 string[] _tagStack; 1890 string[] _attributes; 1891 string _baseIndent; 1892 string _totalIndent; 1893 bool _startTagOpen; 1894 version(assert) bool _writtenRootEnd; 1895 } 1896 1897 /// Ditto 1898 auto xmlWriter(OR)(OR output, string baseIndent = " ") 1899 { 1900 return XMLWriter!OR(output, baseIndent); 1901 } 1902 1903 /// 1904 unittest 1905 { 1906 import std.array : appender; 1907 { 1908 auto writer = xmlWriter(appender!string()); 1909 writer.writeStartTag("root"); 1910 1911 writer.openStartTag("foo"); 1912 writer.writeAttr("a", "42"); 1913 writer.closeStartTag(); 1914 1915 writer.writeText("bar"); 1916 1917 writer.writeEndTag("foo"); 1918 1919 writer.writeEndTag("root"); 1920 1921 assert(writer.output.data == 1922 "\n" ~ 1923 "<root>\n" ~ 1924 ` <foo a="42">` ~ "\n" ~ 1925 " bar\n" ~ 1926 " </foo>\n" ~ 1927 "</root>"); 1928 } 1929 1930 // Newline.no can be used to avoid inserting newlines. 1931 { 1932 auto writer = xmlWriter(appender!string()); 1933 1934 // Unless writeXMLDecl was used, Newline.no is needed on the first 1935 // entity to avoid having the document start with a newline. 1936 writer.writeStartTag("root", Newline.no); 1937 1938 writer.openStartTag("foo"); 1939 writer.writeAttr("a", "42"); 1940 writer.closeStartTag(); 1941 1942 writer.writeText("bar", Newline.no); 1943 1944 writer.writeEndTag("foo", Newline.no); 1945 1946 writer.writeEndTag("root"); 1947 1948 assert(writer.output.data == 1949 "<root>\n" ~ 1950 ` <foo a="42">bar</foo>` ~ "\n" ~ 1951 "</root>"); 1952 } 1953 } 1954 1955 @safe pure nothrow unittest 1956 { 1957 import dxml.internal : TestAttrOR; 1958 auto writer = xmlWriter(TestAttrOR.init); 1959 } 1960 1961 // This is purely to provide a way to trigger the unittest blocks in XMLWriter 1962 // without compiling them in normally. 1963 private struct XMLWriterCompileTests 1964 { 1965 void put(char c) @safe pure nothrow @nogc { assert(0); } 1966 } 1967 1968 unittest 1969 { 1970 auto _xmlWriterTests = XMLWriter!(XMLWriterCompileTests).init; 1971 } 1972 1973 1974 /++ 1975 Writes the $(D <?xml...?>) declaration to the given output range. If it's 1976 going to be used in conjunction with $(LREF XMLWriter), then either 1977 writeXMLDecl will need to be called before constructing the 1978 $(LREF XMLWriter), or $(LREF XMLWriter._output) will need to be used to 1979 write to the output range before writing anything else using the 1980 $(LREF XMLWriter). $(LREF XMLWriter) expects to be writing XML after the 1981 $(D <?xml...?>) and $(D <!DOCTYPE...>) declarations (assuming they're 1982 present at all), and it is invalid to put a $(D <?xml...?>) declaration 1983 anywhere but at the very beginning of an XML document. 1984 1985 Params: 1986 S = The string type used to infer the encoding type. Ideally, it would 1987 be inferred from the type of the _output range, but unfortunately, 1988 the _output range API does not provide that functionality. If S 1989 does not match the encoding of the _output range, then the result 1990 will be invalid XML. 1991 output = The _output range to write to. 1992 +/ 1993 void writeXMLDecl(S, OR)(ref OR output) 1994 if(isOutputRange!(OR, char) && isSomeString!S) 1995 { 1996 put(output, `<?xml version="1.0"`); 1997 static if(is(Unqual!(ElementEncodingType!S) == char)) 1998 put(output, ` encoding="UTF-8"?>`); 1999 else static if(is(Unqual!(ElementEncodingType!S) == wchar)) 2000 put(output, ` encoding="UTF-16"?>`); 2001 else 2002 put(output, ` encoding="UTF-32"?>`); 2003 } 2004 2005 /// 2006 unittest 2007 { 2008 import std.array : appender; 2009 2010 { 2011 auto app = appender!string(); 2012 app.writeXMLDecl!string(); 2013 assert(app.data == `<?xml version="1.0" encoding="UTF-8"?>`); 2014 } 2015 { 2016 auto app = appender!wstring(); 2017 app.writeXMLDecl!wstring(); 2018 assert(app.data == `<?xml version="1.0" encoding="UTF-16"?>`w); 2019 } 2020 { 2021 auto app = appender!dstring(); 2022 app.writeXMLDecl!dstring(); 2023 assert(app.data == `<?xml version="1.0" encoding="UTF-32"?>`d); 2024 } 2025 2026 // This would be invalid XML, because the output range contains UTF-8, but 2027 // writeXMLDecl is told to write that the encoding is UTF-32. 2028 { 2029 auto app = appender!string(); 2030 app.writeXMLDecl!dstring(); 2031 assert(app.data == `<?xml version="1.0" encoding="UTF-32"?>`); 2032 } 2033 } 2034 2035 2036 /++ 2037 Helper function for writing _text which has a start tag and end tag on each 2038 side and no attributes so that it can be done with one function call instead 2039 of three. 2040 2041 writeTaggedText is essentially equivalent to calling 2042 2043 --- 2044 writer.writeStartTag(name, newline); 2045 writer.writeText(text, insertIndent, Newline.no); 2046 writer.writeEndTag(Newline.no); 2047 --- 2048 2049 with the difference being that both the name and text are validated before 2050 any data is written. So, if the text is invalid XML, then nothing will have 2051 been written to the output range when the exception is thrown (whereas if 2052 each function were called individually, then the start tag would have been 2053 written before the exception was thrown from $(LREF2 writeText, XMLWriter)). 2054 2055 If more control is needed over the formatting, or if attributes are needed 2056 on the start tag, then the functions will have to be called separately 2057 instead of calling writeTaggedText. 2058 2059 Params: 2060 writer = The $(LREF XMLWriter) to write to. 2061 name = The _name of the start tag. 2062 text = The _text to write between the start and end tags. 2063 newline = Whether a _newline followed by an indent will be written 2064 to the output range before the start tag. 2065 insertIndent = Whether an indent will be inserted after each 2066 _newline within the _text. 2067 2068 Throws: $(LREF XMLWritingException) if the given _name is an invalid XML 2069 tag _name or if the given _text contains any characters or sequence 2070 of characters which are not legal in the _text portion of an XML 2071 document. $(REF encodeText, dxml, util) can be used to encode any 2072 characters that are not legal in their literal form in the _text but 2073 are legal as entity references. 2074 2075 See_Also: $(LREF2 writeStartTag, XMLWriter)$(BR) 2076 $(LREF2 writeText, XMLWriter)$(BR) 2077 $(LREF2 writeEndTag, XMLWriter) 2078 +/ 2079 void writeTaggedText(XW, R)(ref XW writer, string name, R text, Newline newline = Newline.yes, 2080 InsertIndent insertIndent = InsertIndent.yes) 2081 if(isInstanceOf!(XMLWriter, XW) && 2082 isForwardRange!R && isSomeChar!(ElementType!R)) 2083 { 2084 writer._validateStartTag!"writeTaggedText"(name); 2085 writer._validateText!"writeTaggedText"(text.save); 2086 2087 writer._writeStartTag(name, EmptyTag.no, newline); 2088 writer._writeText(text, Newline.no, insertIndent); 2089 writer.writeEndTag(Newline.no); 2090 } 2091 2092 /// Ditto 2093 void writeTaggedText(XW, R)(ref XW writer, string name, R text, InsertIndent insertIndent, 2094 Newline newline = Newline.yes) 2095 if(isInstanceOf!(XMLWriter, XW) && 2096 isForwardRange!R && isSomeChar!(ElementType!R)) 2097 { 2098 writeTaggedText(writer, name, text, newline, insertIndent); 2099 } 2100 2101 /// 2102 unittest 2103 { 2104 import std.array : appender; 2105 2106 { 2107 auto writer = xmlWriter(appender!string()); 2108 writer.writeStartTag("root", Newline.no); 2109 writer.writeTaggedText("foo", "Some text between foos"); 2110 writer.writeEndTag("root"); 2111 2112 assert(writer.output.data == 2113 "<root>\n" ~ 2114 " <foo>Some text between foos</foo>\n" ~ 2115 "</root>"); 2116 } 2117 2118 // With Newline.no 2119 { 2120 auto writer = xmlWriter(appender!string()); 2121 writer.writeStartTag("root", Newline.no); 2122 writer.writeTaggedText("foo", "Some text between foos", Newline.no); 2123 writer.writeEndTag("root"); 2124 2125 assert(writer.output.data == 2126 "<root><foo>Some text between foos</foo>\n" ~ 2127 "</root>"); 2128 } 2129 2130 // With InsertIndent.yes 2131 { 2132 auto writer = xmlWriter(appender!string()); 2133 writer.writeStartTag("root", Newline.no); 2134 writer.writeTaggedText("foo", "Some text\nNext line"); 2135 writer.writeEndTag("root"); 2136 2137 assert(writer.output.data == 2138 "<root>\n" ~ 2139 " <foo>Some text\n" ~ 2140 " Next line</foo>\n" ~ 2141 "</root>"); 2142 } 2143 2144 // With InsertIndent.no 2145 { 2146 auto writer = xmlWriter(appender!string()); 2147 writer.writeStartTag("root", Newline.no); 2148 writer.writeTaggedText("foo", "Some text\nNext line", InsertIndent.no); 2149 writer.writeEndTag("root"); 2150 2151 assert(writer.output.data == 2152 "<root>\n" ~ 2153 " <foo>Some text\n" ~ 2154 "Next line</foo>\n" ~ 2155 "</root>"); 2156 } 2157 } 2158 2159 unittest 2160 { 2161 import std.array : appender; 2162 import std.exception : assertThrown; 2163 import dxml.internal : testRangeFuncs; 2164 2165 foreach(func; testRangeFuncs) 2166 { 2167 auto writer = xmlWriter(appender!string); 2168 writer.writeStartTag("root", Newline.no); 2169 writer.writeTaggedText("foo", func("hello sally")); 2170 assertThrown!XMLWritingException(writer.writeTaggedText("foo", func("\v"))); 2171 assertThrown!XMLWritingException(writer.writeTaggedText("foo", func("&bar"))); 2172 assertThrown!XMLWritingException(writer.writeTaggedText("foo", func("--<--"))); 2173 assertThrown!XMLWritingException(writer.writeTaggedText("foo", func("--&--"))); 2174 assertThrown!XMLWritingException(writer.writeTaggedText(".f", func("bar"))); 2175 writer.writeTaggedText("f.", func("--")); 2176 writer.writeTaggedText("a", func("&foo; &bar; &baz;"), Newline.no); 2177 writer.writeTaggedText("a", func("&foo; \n &bar;\n&baz;")); 2178 writer.writeTaggedText("a", func("&foo; \n &bar;\n&baz;"), InsertIndent.no); 2179 assert(writer.output.data == 2180 "<root>\n" ~ 2181 " <foo>hello sally</foo>\n" ~ 2182 " <f.>--</f.><a>&foo; &bar; &baz;</a>\n" ~ 2183 " <a>&foo; \n" ~ 2184 " &bar;\n" ~ 2185 " &baz;</a>\n" ~ 2186 " <a>&foo; \n" ~ 2187 " &bar;\n" ~ 2188 "&baz;</a>"); 2189 } 2190 } 2191 2192 // _decLevel cannot currently be pure. 2193 @safe /+pure+/ unittest 2194 { 2195 import dxml.internal : TestAttrOR; 2196 auto writer = xmlWriter(TestAttrOR.init); 2197 writer.writeTaggedText("root", "text"); 2198 } 2199 2200 2201 private: 2202 2203 void checkName(R)(R range) 2204 { 2205 import std.format : format; 2206 import std.range : takeExactly; 2207 import std.utf : byCodeUnit, decodeFront, UseReplacementDchar; 2208 import dxml.internal : isNameStartChar, isNameChar; 2209 2210 auto text = range.byCodeUnit(); 2211 2212 size_t takeLen; 2213 { 2214 immutable decodedC = text.decodeFront!(UseReplacementDchar.yes)(takeLen); 2215 if(!isNameStartChar(decodedC)) 2216 throw new XMLWritingException(format!"Name contains invalid character: 0x%0x"(decodedC)); 2217 } 2218 2219 while(!text.empty) 2220 { 2221 size_t numCodeUnits; 2222 immutable decodedC = text.decodeFront!(UseReplacementDchar.yes)(numCodeUnits); 2223 if(!isNameChar(decodedC)) 2224 throw new XMLWritingException(format!"Name contains invalid character: 0x%0x"(decodedC)); 2225 } 2226 } 2227 2228 @safe pure unittest 2229 { 2230 import std.exception : assertNotThrown, assertThrown; 2231 import std.range : only; 2232 import dxml.internal : testRangeFuncs; 2233 2234 static foreach(func; testRangeFuncs) 2235 { 2236 foreach(str; only("hello", "プログラミング", "h_:llo-.42", "_.", "_-", "_42", "プログラミング")) 2237 assertNotThrown!XMLWritingException(checkName(func(str))); 2238 2239 foreach(str; only(".", ".foo", "-foo", "&foo;", "foo\vbar")) 2240 assertThrown!XMLWritingException(checkName(func(str))); 2241 } 2242 } 2243 2244 void checkPIName(R)(R range) 2245 { 2246 import std.range : walkLength; 2247 import std.uni : icmp; 2248 import std.utf : byCodeUnit; 2249 2250 if(icmp(range.save.byCodeUnit(), "xml") == 0) 2251 throw new XMLWritingException("Processing instructions cannot be named xml"); 2252 checkName(range); 2253 } 2254 2255 @safe pure unittest 2256 { 2257 import std.exception : assertNotThrown, assertThrown; 2258 import std.range : only; 2259 import dxml.internal : testRangeFuncs; 2260 2261 static foreach(func; testRangeFuncs) 2262 { 2263 foreach(str; only("hello", "プログラミング", "h_:llo-.42", "_.", "_-", "_42", "プログラミング", "xmlx")) 2264 assertNotThrown!XMLWritingException(checkPIName(func(str))); 2265 2266 foreach(str; only(".", ".foo", "-foo", "&foo;", "foo\vbar", "xml", "XML", "xMl")) 2267 assertThrown!XMLWritingException(checkPIName(func(str))); 2268 } 2269 } 2270 2271 2272 enum CheckText 2273 { 2274 attValueApos, 2275 attValueQuot, 2276 cdata, 2277 comment, 2278 pi, 2279 text 2280 } 2281 2282 void checkText(CheckText ct, R)(R range) 2283 { 2284 import std.format : format; 2285 import std.utf : byCodeUnit, decodeFront, UseReplacementDchar; 2286 2287 auto text = range.byCodeUnit(); 2288 2289 loop: while(!text.empty) 2290 { 2291 switch(text.front) 2292 { 2293 static if(ct == CheckText.attValueApos || ct == CheckText.attValueQuot || ct == CheckText.text) 2294 { 2295 case '&': 2296 { 2297 import dxml.util : parseCharRef; 2298 2299 { 2300 auto temp = text.save; 2301 auto charRef = parseCharRef(temp); 2302 if(!charRef.isNull) 2303 { 2304 static if(hasLength!(typeof(text))) 2305 text = temp; 2306 else 2307 { 2308 while(text.front != ';') 2309 text.popFront(); 2310 text.popFront(); 2311 } 2312 continue; 2313 } 2314 } 2315 2316 text.popFront(); 2317 2318 import dxml.internal : isNameStartChar, isNameChar; 2319 2320 if(text.empty) 2321 goto failedEntityRef; 2322 2323 { 2324 size_t numCodeUnits; 2325 immutable decodedC = text.decodeFront!(UseReplacementDchar.yes)(numCodeUnits); 2326 if(!isNameStartChar(decodedC)) 2327 goto failedEntityRef; 2328 } 2329 2330 while(true) 2331 { 2332 if(text.empty) 2333 goto failedEntityRef; 2334 immutable c = text.front; 2335 if(c == ';') 2336 { 2337 text.popFront(); 2338 break; 2339 } 2340 size_t numCodeUnits; 2341 immutable decodedC = text.decodeFront!(UseReplacementDchar.yes)(numCodeUnits); 2342 if(!isNameChar(decodedC)) 2343 goto failedEntityRef; 2344 } 2345 break; 2346 2347 failedEntityRef: 2348 throw new XMLWritingException("& is only legal in an attribute value as part of a " ~ 2349 "character or entity reference, and this is not a valid " ~ 2350 "character or entity reference."); 2351 } 2352 case '<': throw new XMLWritingException("< is not legal in EntityType.text"); 2353 } 2354 static if(ct == CheckText.comment) 2355 { 2356 case '-': 2357 { 2358 text.popFront(); 2359 if(text.empty) 2360 throw new XMLWritingException("- is not legal at the end of an EntityType.comment"); 2361 if(text.front == '-') 2362 throw new XMLWritingException("-- is not legal in EntityType.comment"); 2363 break; 2364 } 2365 } 2366 else static if(ct == CheckText.pi) 2367 { 2368 case '?': 2369 { 2370 text.popFront(); 2371 if(!text.empty && text.front == '>') 2372 throw new XMLWritingException("A EntityType.pi cannot contain ?>"); 2373 break; 2374 } 2375 } 2376 else static if(ct == CheckText.cdata || ct == CheckText.text) 2377 { 2378 case ']': 2379 { 2380 import std.algorithm.searching : startsWith; 2381 text.popFront(); 2382 if(text.save.startsWith("]>")) 2383 { 2384 static if(ct == CheckText.cdata) 2385 throw new XMLWritingException("]]> is not legal in EntityType.cdata"); 2386 else 2387 throw new XMLWritingException("]]> is not legal in EntityType.text"); 2388 } 2389 break; 2390 } 2391 } 2392 else static if(ct == CheckText.attValueApos) 2393 { 2394 case '\'': 2395 { 2396 throw new XMLWritingException("If a single quote is the attrbute value's delimiter, then it's " ~ 2397 "illegal for the attribute value to contain a single quote. Either " ~ 2398 "instantiate writeAttr with a double quote instead or use " ~ 2399 "' in the attribute value instead of a single quote."); 2400 } 2401 } 2402 else static if(ct == CheckText.attValueQuot) 2403 { 2404 case '"': 2405 { 2406 throw new XMLWritingException("If a double quote is the attrbute value's delimiter, then it's " ~ 2407 "illegal for the attribute value to contain a double quote. Either " ~ 2408 "instantiate writeAttr with a single quote instead or use " ~ 2409 "" in the attribute value instead of a double quote."); 2410 } 2411 } 2412 case '\n': 2413 { 2414 text.popFront(); 2415 break; 2416 } 2417 default: 2418 { 2419 import std.ascii : isASCII; 2420 import dxml.internal : isXMLChar; 2421 immutable c = text.front; 2422 if(isASCII(c)) 2423 { 2424 if(!isXMLChar(c)) 2425 throw new XMLWritingException(format!"Character is not legal in an XML File: 0x%0x"(c)); 2426 text.popFront(); 2427 } 2428 else 2429 { 2430 import std.utf : UTFException; 2431 // Annoyngly, letting decodeFront throw is the easier way to handle this, since the 2432 // replacement character is considered valid XML, and if we decoded using it, then 2433 // all of the invalid Unicode characters would come out as the replacement character 2434 // and then be treated as valid instead of being caught, which we could do, but then 2435 // the resulting XML document would contain the replacement character without the 2436 // caller knowing it, which almost certainly means that a bug would go unnoticed. 2437 try 2438 { 2439 size_t numCodeUnits; 2440 immutable decodedC = text.decodeFront!(UseReplacementDchar.no)(numCodeUnits); 2441 if(!isXMLChar(decodedC)) 2442 { 2443 enum fmt = "Character is not legal in an XML File: 0x%0x"; 2444 throw new XMLWritingException(format!fmt(decodedC)); 2445 } 2446 } 2447 catch(UTFException) 2448 throw new XMLWritingException("Text contains invalid Unicode character"); 2449 } 2450 break; 2451 } 2452 } 2453 } 2454 } 2455 2456 unittest 2457 { 2458 import std.exception : assertNotThrown, assertThrown; 2459 import dxml.internal : testRangeFuncs; 2460 2461 static void test(alias func, CheckText ct)(string text, size_t line = __LINE__) 2462 { 2463 assertNotThrown(checkText!ct(func(text)), "unittest failure", __FILE__, line); 2464 } 2465 2466 static void testFail(alias func, CheckText ct)(string text, size_t line = __LINE__) 2467 { 2468 assertThrown!XMLWritingException(checkText!ct(func(text)), "unittest failure", __FILE__, line); 2469 } 2470 2471 static foreach(func; testRangeFuncs) 2472 { 2473 static foreach(ct; EnumMembers!CheckText) 2474 { 2475 test!(func, ct)(""); 2476 test!(func, ct)("J",); 2477 test!(func, ct)("foo"); 2478 test!(func, ct)("プログラミング"); 2479 2480 test!(func, ct)("&><"); 2481 test!(func, ct)("hello&><world"); 2482 test!(func, ct)(".....'"&....."); 2483 test!(func, ct)("ディラン"); 2484 test!(func, ct)("-hello¯*"-world"); 2485 test!(func, ct)("&foo;&bar;&baz;"); 2486 2487 test!(func, ct)("]]"); 2488 test!(func, ct)("]>"); 2489 test!(func, ct)("foo]]bar"); 2490 test!(func, ct)("foo]>bar"); 2491 test!(func, ct)("]] >"); 2492 test!(func, ct)("? >"); 2493 2494 testFail!(func, ct)("\v"); 2495 testFail!(func, ct)("\uFFFE"); 2496 testFail!(func, ct)("hello\vworld"); 2497 testFail!(func, ct)("he\nllo\vwo\nrld"); 2498 } 2499 2500 static foreach(ct; [CheckText.attValueApos, CheckText.attValueQuot, CheckText.text]) 2501 { 2502 testFail!(func, ct)("<"); 2503 testFail!(func, ct)("&"); 2504 testFail!(func, ct)("&"); 2505 testFail!(func, ct)("&x"); 2506 testFail!(func, ct)("&&;"); 2507 testFail!(func, ct)("&a"); 2508 testFail!(func, ct)("hello&;"); 2509 testFail!(func, ct)("hello&.f;"); 2510 testFail!(func, ct)("hello&f?;"); 2511 testFail!(func, ct)("hello&;world"); 2512 testFail!(func, ct)("hello&<;world"); 2513 testFail!(func, ct)("hello&world"); 2514 testFail!(func, ct)("hello world&"); 2515 testFail!(func, ct)("hello world&;"); 2516 testFail!(func, ct)("hello world&foo"); 2517 testFail!(func, ct)("&#;"); 2518 testFail!(func, ct)("&#x;"); 2519 testFail!(func, ct)("&#AF;"); 2520 testFail!(func, ct)("&#x"); 2521 testFail!(func, ct)("*"); 2522 testFail!(func, ct)("B"); 2523 testFail!(func, ct)(""); 2524 testFail!(func, ct)(""); 2525 testFail!(func, ct)("*foo\nbar&#;"); 2526 testFail!(func, ct)("*foo\nbar&#x;"); 2527 testFail!(func, ct)("*foo\nbar&#AF;"); 2528 testFail!(func, ct)("*foo\nbar&#x"); 2529 testFail!(func, ct)("*foo\nbar*"); 2530 testFail!(func, ct)("*foo\nbarB"); 2531 testFail!(func, ct)("プログラミング&"); 2532 } 2533 2534 static foreach(ct; EnumMembers!CheckText) 2535 { 2536 static if(ct == CheckText.attValueApos) 2537 testFail!(func, ct)(`foo'bar`); 2538 else 2539 test!(func, ct)(`foo'bar`); 2540 2541 static if(ct == CheckText.attValueQuot) 2542 testFail!(func, ct)(`foo"bar`); 2543 else 2544 test!(func, ct)(`foo"bar`); 2545 2546 static if(ct == CheckText.comment) 2547 { 2548 testFail!(func, ct)("-"); 2549 testFail!(func, ct)("--"); 2550 testFail!(func, ct)("--*"); 2551 } 2552 else 2553 { 2554 test!(func, ct)("-"); 2555 test!(func, ct)("--"); 2556 test!(func, ct)("--*"); 2557 } 2558 2559 static if(ct == CheckText.pi) 2560 testFail!(func, ct)("?>"); 2561 else 2562 test!(func, ct)("?>"); 2563 } 2564 2565 static foreach(ct; [CheckText.attValueApos, CheckText.attValueQuot, CheckText.pi]) 2566 { 2567 test!(func, ct)("]]>"); 2568 test!(func, ct)("foo]]>bar"); 2569 } 2570 static foreach(ct; [CheckText.cdata, CheckText.text]) 2571 { 2572 testFail!(func, ct)("]]>"); 2573 testFail!(func, ct)("foo]]>bar"); 2574 } 2575 2576 static foreach(ct; [CheckText.cdata, CheckText.comment, CheckText.pi]) 2577 { 2578 test!(func, ct)("<"); 2579 test!(func, ct)("&"); 2580 test!(func, ct)("&x"); 2581 test!(func, ct)("&&;"); 2582 test!(func, ct)("&a"); 2583 test!(func, ct)("hello&;"); 2584 test!(func, ct)("hello&;world"); 2585 test!(func, ct)("hello&<;world"); 2586 test!(func, ct)("hello&world"); 2587 test!(func, ct)("hello world&"); 2588 test!(func, ct)("hello world&;"); 2589 test!(func, ct)("hello world&foo"); 2590 test!(func, ct)("&#;"); 2591 test!(func, ct)("&#x;"); 2592 test!(func, ct)("&#AF;"); 2593 test!(func, ct)("&#x"); 2594 test!(func, ct)("*"); 2595 test!(func, ct)("B"); 2596 test!(func, ct)(""); 2597 test!(func, ct)(""); 2598 test!(func, ct)("*foo\nbar&#;"); 2599 test!(func, ct)("*foo\nbar&#x;"); 2600 test!(func, ct)("*foo\nbar&#AF;"); 2601 test!(func, ct)("*foo\nbar&#x"); 2602 test!(func, ct)("*foo\nbar*"); 2603 test!(func, ct)("*foo\nbarB"); 2604 test!(func, ct)("プログラミング&"); 2605 } 2606 } 2607 2608 // These can't be tested with testFail, because attempting to convert 2609 // invalid Unicode results in UnicodeExceptions before checkText even 2610 // gets called. 2611 import std.meta : AliasSeq; 2612 static foreach(str; AliasSeq!(cast(string)[255], cast(wstring)[0xD800], cast(dstring)[0xD800])) 2613 { 2614 static foreach(ct; EnumMembers!CheckText) 2615 { 2616 assertThrown!XMLWritingException(checkText!ct(str)); 2617 assertThrown!XMLWritingException(checkText!ct(str)); 2618 } 2619 } 2620 } 2621 2622 @safe pure unittest 2623 { 2624 static foreach(ct; EnumMembers!CheckText) 2625 checkText!ct("foo"); 2626 }