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 - 2023 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 version(dxmlTests) 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 version(dxmlTests) @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 version(dxmlTests) 1969 auto _xmlWriterTests = XMLWriter!(XMLWriterCompileTests).init; 1970 1971 1972 /++ 1973 Writes the $(D <?xml...?>) declaration to the given output range. If it's 1974 going to be used in conjunction with $(LREF XMLWriter), then either 1975 writeXMLDecl will need to be called before constructing the 1976 $(LREF XMLWriter), or $(LREF XMLWriter._output) will need to be used to 1977 write to the output range before writing anything else using the 1978 $(LREF XMLWriter). $(LREF XMLWriter) expects to be writing XML after the 1979 $(D <?xml...?>) and $(D <!DOCTYPE...>) declarations (assuming they're 1980 present at all), and it is invalid to put a $(D <?xml...?>) declaration 1981 anywhere but at the very beginning of an XML document. 1982 1983 Params: 1984 S = The string type used to infer the encoding type. Ideally, it would 1985 be inferred from the type of the _output range, but unfortunately, 1986 the _output range API does not provide that functionality. If S 1987 does not match the encoding of the _output range, then the result 1988 will be invalid XML. 1989 output = The _output range to write to. 1990 +/ 1991 void writeXMLDecl(S, OR)(ref OR output) 1992 if(isOutputRange!(OR, char) && isSomeString!S) 1993 { 1994 put(output, `<?xml version="1.0"`); 1995 static if(is(Unqual!(ElementEncodingType!S) == char)) 1996 put(output, ` encoding="UTF-8"?>`); 1997 else static if(is(Unqual!(ElementEncodingType!S) == wchar)) 1998 put(output, ` encoding="UTF-16"?>`); 1999 else 2000 put(output, ` encoding="UTF-32"?>`); 2001 } 2002 2003 /// 2004 version(dxmlTests) unittest 2005 { 2006 import std.array : appender; 2007 2008 { 2009 auto app = appender!string(); 2010 app.writeXMLDecl!string(); 2011 assert(app.data == `<?xml version="1.0" encoding="UTF-8"?>`); 2012 } 2013 { 2014 auto app = appender!wstring(); 2015 app.writeXMLDecl!wstring(); 2016 assert(app.data == `<?xml version="1.0" encoding="UTF-16"?>`w); 2017 } 2018 { 2019 auto app = appender!dstring(); 2020 app.writeXMLDecl!dstring(); 2021 assert(app.data == `<?xml version="1.0" encoding="UTF-32"?>`d); 2022 } 2023 2024 // This would be invalid XML, because the output range contains UTF-8, but 2025 // writeXMLDecl is told to write that the encoding is UTF-32. 2026 { 2027 auto app = appender!string(); 2028 app.writeXMLDecl!dstring(); 2029 assert(app.data == `<?xml version="1.0" encoding="UTF-32"?>`); 2030 } 2031 } 2032 2033 2034 /++ 2035 Helper function for writing _text which has a start tag and end tag on each 2036 side and no attributes so that it can be done with one function call instead 2037 of three. 2038 2039 writeTaggedText is essentially equivalent to calling 2040 2041 --- 2042 writer.writeStartTag(name, newline); 2043 writer.writeText(text, insertIndent, Newline.no); 2044 writer.writeEndTag(Newline.no); 2045 --- 2046 2047 with the difference being that both the name and text are validated before 2048 any data is written. So, if the text is invalid XML, then nothing will have 2049 been written to the output range when the exception is thrown (whereas if 2050 each function were called individually, then the start tag would have been 2051 written before the exception was thrown from $(LREF2 writeText, XMLWriter)). 2052 2053 If more control is needed over the formatting, or if attributes are needed 2054 on the start tag, then the functions will have to be called separately 2055 instead of calling writeTaggedText. 2056 2057 Params: 2058 writer = The $(LREF XMLWriter) to write to. 2059 name = The _name of the start tag. 2060 text = The _text to write between the start and end tags. 2061 newline = Whether a _newline followed by an indent will be written 2062 to the output range before the start tag. 2063 insertIndent = Whether an indent will be inserted after each 2064 _newline within the _text. 2065 2066 Throws: $(LREF XMLWritingException) if the given _name is an invalid XML 2067 tag _name or if the given _text contains any characters or sequence 2068 of characters which are not legal in the _text portion of an XML 2069 document. $(REF encodeText, dxml, util) can be used to encode any 2070 characters that are not legal in their literal form in the _text but 2071 are legal as entity references. 2072 2073 See_Also: $(LREF2 writeStartTag, XMLWriter)$(BR) 2074 $(LREF2 writeText, XMLWriter)$(BR) 2075 $(LREF2 writeEndTag, XMLWriter) 2076 +/ 2077 void writeTaggedText(XW, R)(ref XW writer, string name, R text, Newline newline = Newline.yes, 2078 InsertIndent insertIndent = InsertIndent.yes) 2079 if(isInstanceOf!(XMLWriter, XW) && 2080 isForwardRange!R && isSomeChar!(ElementType!R)) 2081 { 2082 writer._validateStartTag!"writeTaggedText"(name); 2083 writer._validateText!"writeTaggedText"(text.save); 2084 2085 writer._writeStartTag(name, EmptyTag.no, newline); 2086 writer._writeText(text, Newline.no, insertIndent); 2087 writer.writeEndTag(Newline.no); 2088 } 2089 2090 /// Ditto 2091 void writeTaggedText(XW, R)(ref XW writer, string name, R text, InsertIndent insertIndent, 2092 Newline newline = Newline.yes) 2093 if(isInstanceOf!(XMLWriter, XW) && 2094 isForwardRange!R && isSomeChar!(ElementType!R)) 2095 { 2096 writeTaggedText(writer, name, text, newline, insertIndent); 2097 } 2098 2099 /// 2100 version(dxmlTests) unittest 2101 { 2102 import std.array : appender; 2103 2104 { 2105 auto writer = xmlWriter(appender!string()); 2106 writer.writeStartTag("root", Newline.no); 2107 writer.writeTaggedText("foo", "Some text between foos"); 2108 writer.writeEndTag("root"); 2109 2110 assert(writer.output.data == 2111 "<root>\n" ~ 2112 " <foo>Some text between foos</foo>\n" ~ 2113 "</root>"); 2114 } 2115 2116 // With Newline.no 2117 { 2118 auto writer = xmlWriter(appender!string()); 2119 writer.writeStartTag("root", Newline.no); 2120 writer.writeTaggedText("foo", "Some text between foos", Newline.no); 2121 writer.writeEndTag("root"); 2122 2123 assert(writer.output.data == 2124 "<root><foo>Some text between foos</foo>\n" ~ 2125 "</root>"); 2126 } 2127 2128 // With InsertIndent.yes 2129 { 2130 auto writer = xmlWriter(appender!string()); 2131 writer.writeStartTag("root", Newline.no); 2132 writer.writeTaggedText("foo", "Some text\nNext line"); 2133 writer.writeEndTag("root"); 2134 2135 assert(writer.output.data == 2136 "<root>\n" ~ 2137 " <foo>Some text\n" ~ 2138 " Next line</foo>\n" ~ 2139 "</root>"); 2140 } 2141 2142 // With InsertIndent.no 2143 { 2144 auto writer = xmlWriter(appender!string()); 2145 writer.writeStartTag("root", Newline.no); 2146 writer.writeTaggedText("foo", "Some text\nNext line", InsertIndent.no); 2147 writer.writeEndTag("root"); 2148 2149 assert(writer.output.data == 2150 "<root>\n" ~ 2151 " <foo>Some text\n" ~ 2152 "Next line</foo>\n" ~ 2153 "</root>"); 2154 } 2155 } 2156 2157 version(dxmlTests) unittest 2158 { 2159 import std.array : appender; 2160 import std.exception : assertThrown; 2161 import dxml.internal : testRangeFuncs; 2162 2163 foreach(func; testRangeFuncs) 2164 { 2165 auto writer = xmlWriter(appender!string); 2166 writer.writeStartTag("root", Newline.no); 2167 writer.writeTaggedText("foo", func("hello sally")); 2168 assertThrown!XMLWritingException(writer.writeTaggedText("foo", func("\v"))); 2169 assertThrown!XMLWritingException(writer.writeTaggedText("foo", func("&bar"))); 2170 assertThrown!XMLWritingException(writer.writeTaggedText("foo", func("--<--"))); 2171 assertThrown!XMLWritingException(writer.writeTaggedText("foo", func("--&--"))); 2172 assertThrown!XMLWritingException(writer.writeTaggedText(".f", func("bar"))); 2173 writer.writeTaggedText("f.", func("--")); 2174 writer.writeTaggedText("a", func("&foo; &bar; &baz;"), Newline.no); 2175 writer.writeTaggedText("a", func("&foo; \n &bar;\n&baz;")); 2176 writer.writeTaggedText("a", func("&foo; \n &bar;\n&baz;"), InsertIndent.no); 2177 assert(writer.output.data == 2178 "<root>\n" ~ 2179 " <foo>hello sally</foo>\n" ~ 2180 " <f.>--</f.><a>&foo; &bar; &baz;</a>\n" ~ 2181 " <a>&foo; \n" ~ 2182 " &bar;\n" ~ 2183 " &baz;</a>\n" ~ 2184 " <a>&foo; \n" ~ 2185 " &bar;\n" ~ 2186 "&baz;</a>"); 2187 } 2188 } 2189 2190 // _decLevel cannot currently be pure. 2191 version(dxmlTests) @safe /+pure+/ unittest 2192 { 2193 import dxml.internal : TestAttrOR; 2194 auto writer = xmlWriter(TestAttrOR.init); 2195 writer.writeTaggedText("root", "text"); 2196 } 2197 2198 2199 private: 2200 2201 void checkName(R)(R range) 2202 { 2203 import std.format : format; 2204 import std.range : takeExactly; 2205 import std.utf : byCodeUnit, decodeFront, UseReplacementDchar; 2206 import dxml.internal : isNameStartChar, isNameChar; 2207 2208 auto text = range.byCodeUnit(); 2209 2210 size_t takeLen; 2211 { 2212 immutable decodedC = text.decodeFront!(UseReplacementDchar.yes)(takeLen); 2213 if(!isNameStartChar(decodedC)) 2214 throw new XMLWritingException(format!"Name contains invalid character: 0x%0x"(decodedC)); 2215 } 2216 2217 while(!text.empty) 2218 { 2219 size_t numCodeUnits; 2220 immutable decodedC = text.decodeFront!(UseReplacementDchar.yes)(numCodeUnits); 2221 if(!isNameChar(decodedC)) 2222 throw new XMLWritingException(format!"Name contains invalid character: 0x%0x"(decodedC)); 2223 } 2224 } 2225 2226 version(dxmlTests) @safe pure unittest 2227 { 2228 import std.exception : assertNotThrown, assertThrown; 2229 import std.range : only; 2230 import dxml.internal : testRangeFuncs; 2231 2232 static foreach(func; testRangeFuncs) 2233 { 2234 foreach(str; only("hello", "プログラミング", "h_:llo-.42", "_.", "_-", "_42", "プログラミング")) 2235 assertNotThrown!XMLWritingException(checkName(func(str))); 2236 2237 foreach(str; only(".", ".foo", "-foo", "&foo;", "foo\vbar")) 2238 assertThrown!XMLWritingException(checkName(func(str))); 2239 } 2240 } 2241 2242 void checkPIName(R)(R range) 2243 { 2244 import std.range : walkLength; 2245 import std.uni : icmp; 2246 import std.utf : byCodeUnit; 2247 2248 if(icmp(range.save.byCodeUnit(), "xml") == 0) 2249 throw new XMLWritingException("Processing instructions cannot be named xml"); 2250 checkName(range); 2251 } 2252 2253 version(dxmlTests) @safe pure unittest 2254 { 2255 import std.exception : assertNotThrown, assertThrown; 2256 import std.range : only; 2257 import dxml.internal : testRangeFuncs; 2258 2259 static foreach(func; testRangeFuncs) 2260 { 2261 foreach(str; only("hello", "プログラミング", "h_:llo-.42", "_.", "_-", "_42", "プログラミング", "xmlx")) 2262 assertNotThrown!XMLWritingException(checkPIName(func(str))); 2263 2264 foreach(str; only(".", ".foo", "-foo", "&foo;", "foo\vbar", "xml", "XML", "xMl")) 2265 assertThrown!XMLWritingException(checkPIName(func(str))); 2266 } 2267 } 2268 2269 2270 enum CheckText 2271 { 2272 attValueApos, 2273 attValueQuot, 2274 cdata, 2275 comment, 2276 pi, 2277 text 2278 } 2279 2280 void checkText(CheckText ct, R)(R range) 2281 { 2282 import std.format : format; 2283 import std.utf : byCodeUnit, decodeFront, UseReplacementDchar; 2284 2285 auto text = range.byCodeUnit(); 2286 2287 loop: while(!text.empty) 2288 { 2289 switch(text.front) 2290 { 2291 static if(ct == CheckText.attValueApos || ct == CheckText.attValueQuot || ct == CheckText.text) 2292 { 2293 case '&': 2294 { 2295 import dxml.util : parseCharRef; 2296 2297 { 2298 auto temp = text.save; 2299 auto charRef = parseCharRef(temp); 2300 if(!charRef.isNull) 2301 { 2302 static if(hasLength!(typeof(text))) 2303 text = temp; 2304 else 2305 { 2306 while(text.front != ';') 2307 text.popFront(); 2308 text.popFront(); 2309 } 2310 continue; 2311 } 2312 } 2313 2314 text.popFront(); 2315 2316 import dxml.internal : isNameStartChar, isNameChar; 2317 2318 if(text.empty) 2319 goto failedEntityRef; 2320 2321 { 2322 size_t numCodeUnits; 2323 immutable decodedC = text.decodeFront!(UseReplacementDchar.yes)(numCodeUnits); 2324 if(!isNameStartChar(decodedC)) 2325 goto failedEntityRef; 2326 } 2327 2328 while(true) 2329 { 2330 if(text.empty) 2331 goto failedEntityRef; 2332 immutable c = text.front; 2333 if(c == ';') 2334 { 2335 text.popFront(); 2336 break; 2337 } 2338 size_t numCodeUnits; 2339 immutable decodedC = text.decodeFront!(UseReplacementDchar.yes)(numCodeUnits); 2340 if(!isNameChar(decodedC)) 2341 goto failedEntityRef; 2342 } 2343 break; 2344 2345 failedEntityRef: 2346 throw new XMLWritingException("& is only legal in an attribute value as part of a " ~ 2347 "character or entity reference, and this is not a valid " ~ 2348 "character or entity reference."); 2349 } 2350 case '<': throw new XMLWritingException("< is not legal in EntityType.text"); 2351 } 2352 static if(ct == CheckText.comment) 2353 { 2354 case '-': 2355 { 2356 text.popFront(); 2357 if(text.empty) 2358 throw new XMLWritingException("- is not legal at the end of an EntityType.comment"); 2359 if(text.front == '-') 2360 throw new XMLWritingException("-- is not legal in EntityType.comment"); 2361 break; 2362 } 2363 } 2364 else static if(ct == CheckText.pi) 2365 { 2366 case '?': 2367 { 2368 text.popFront(); 2369 if(!text.empty && text.front == '>') 2370 throw new XMLWritingException("A EntityType.pi cannot contain ?>"); 2371 break; 2372 } 2373 } 2374 else static if(ct == CheckText.cdata || ct == CheckText.text) 2375 { 2376 case ']': 2377 { 2378 import std.algorithm.searching : startsWith; 2379 text.popFront(); 2380 if(text.save.startsWith("]>")) 2381 { 2382 static if(ct == CheckText.cdata) 2383 throw new XMLWritingException("]]> is not legal in EntityType.cdata"); 2384 else 2385 throw new XMLWritingException("]]> is not legal in EntityType.text"); 2386 } 2387 break; 2388 } 2389 } 2390 else static if(ct == CheckText.attValueApos) 2391 { 2392 case '\'': 2393 { 2394 throw new XMLWritingException("If a single quote is the attrbute value's delimiter, then it's " ~ 2395 "illegal for the attribute value to contain a single quote. Either " ~ 2396 "instantiate writeAttr with a double quote instead or use " ~ 2397 "' in the attribute value instead of a single quote."); 2398 } 2399 } 2400 else static if(ct == CheckText.attValueQuot) 2401 { 2402 case '"': 2403 { 2404 throw new XMLWritingException("If a double quote is the attrbute value's delimiter, then it's " ~ 2405 "illegal for the attribute value to contain a double quote. Either " ~ 2406 "instantiate writeAttr with a single quote instead or use " ~ 2407 "" in the attribute value instead of a double quote."); 2408 } 2409 } 2410 case '\n': 2411 { 2412 text.popFront(); 2413 break; 2414 } 2415 default: 2416 { 2417 import std.ascii : isASCII; 2418 import dxml.internal : isXMLChar; 2419 immutable c = text.front; 2420 if(isASCII(c)) 2421 { 2422 if(!isXMLChar(c)) 2423 throw new XMLWritingException(format!"Character is not legal in an XML File: 0x%0x"(c)); 2424 text.popFront(); 2425 } 2426 else 2427 { 2428 import std.utf : UTFException; 2429 // Annoyngly, letting decodeFront throw is the easier way to handle this, since the 2430 // replacement character is considered valid XML, and if we decoded using it, then 2431 // all of the invalid Unicode characters would come out as the replacement character 2432 // and then be treated as valid instead of being caught, which we could do, but then 2433 // the resulting XML document would contain the replacement character without the 2434 // caller knowing it, which almost certainly means that a bug would go unnoticed. 2435 try 2436 { 2437 size_t numCodeUnits; 2438 immutable decodedC = text.decodeFront!(UseReplacementDchar.no)(numCodeUnits); 2439 if(!isXMLChar(decodedC)) 2440 { 2441 enum fmt = "Character is not legal in an XML File: 0x%0x"; 2442 throw new XMLWritingException(format!fmt(decodedC)); 2443 } 2444 } 2445 catch(UTFException) 2446 throw new XMLWritingException("Text contains invalid Unicode character"); 2447 } 2448 break; 2449 } 2450 } 2451 } 2452 } 2453 2454 version(dxmlTests) unittest 2455 { 2456 import std.exception : assertNotThrown, assertThrown; 2457 import dxml.internal : testRangeFuncs; 2458 2459 static void test(alias func, CheckText ct)(string text, size_t line = __LINE__) 2460 { 2461 assertNotThrown(checkText!ct(func(text)), "unittest failure", __FILE__, line); 2462 } 2463 2464 static void testFail(alias func, CheckText ct)(string text, size_t line = __LINE__) 2465 { 2466 assertThrown!XMLWritingException(checkText!ct(func(text)), "unittest failure", __FILE__, line); 2467 } 2468 2469 static foreach(func; testRangeFuncs) 2470 { 2471 static foreach(ct; EnumMembers!CheckText) 2472 { 2473 test!(func, ct)(""); 2474 test!(func, ct)("J",); 2475 test!(func, ct)("foo"); 2476 test!(func, ct)("プログラミング"); 2477 2478 test!(func, ct)("&><"); 2479 test!(func, ct)("hello&><world"); 2480 test!(func, ct)(".....'"&....."); 2481 test!(func, ct)("ディラン"); 2482 test!(func, ct)("-hello¯*"-world"); 2483 test!(func, ct)("&foo;&bar;&baz;"); 2484 2485 test!(func, ct)("]]"); 2486 test!(func, ct)("]>"); 2487 test!(func, ct)("foo]]bar"); 2488 test!(func, ct)("foo]>bar"); 2489 test!(func, ct)("]] >"); 2490 test!(func, ct)("? >"); 2491 2492 testFail!(func, ct)("\v"); 2493 testFail!(func, ct)("\uFFFE"); 2494 testFail!(func, ct)("hello\vworld"); 2495 testFail!(func, ct)("he\nllo\vwo\nrld"); 2496 } 2497 2498 static foreach(ct; [CheckText.attValueApos, CheckText.attValueQuot, CheckText.text]) 2499 { 2500 testFail!(func, ct)("<"); 2501 testFail!(func, ct)("&"); 2502 testFail!(func, ct)("&"); 2503 testFail!(func, ct)("&x"); 2504 testFail!(func, ct)("&&;"); 2505 testFail!(func, ct)("&a"); 2506 testFail!(func, ct)("hello&;"); 2507 testFail!(func, ct)("hello&.f;"); 2508 testFail!(func, ct)("hello&f?;"); 2509 testFail!(func, ct)("hello&;world"); 2510 testFail!(func, ct)("hello&<;world"); 2511 testFail!(func, ct)("hello&world"); 2512 testFail!(func, ct)("hello world&"); 2513 testFail!(func, ct)("hello world&;"); 2514 testFail!(func, ct)("hello world&foo"); 2515 testFail!(func, ct)("&#;"); 2516 testFail!(func, ct)("&#x;"); 2517 testFail!(func, ct)("&#AF;"); 2518 testFail!(func, ct)("&#x"); 2519 testFail!(func, ct)("*"); 2520 testFail!(func, ct)("B"); 2521 testFail!(func, ct)(""); 2522 testFail!(func, ct)(""); 2523 testFail!(func, ct)("*foo\nbar&#;"); 2524 testFail!(func, ct)("*foo\nbar&#x;"); 2525 testFail!(func, ct)("*foo\nbar&#AF;"); 2526 testFail!(func, ct)("*foo\nbar&#x"); 2527 testFail!(func, ct)("*foo\nbar*"); 2528 testFail!(func, ct)("*foo\nbarB"); 2529 testFail!(func, ct)("プログラミング&"); 2530 } 2531 2532 static foreach(ct; EnumMembers!CheckText) 2533 { 2534 static if(ct == CheckText.attValueApos) 2535 testFail!(func, ct)(`foo'bar`); 2536 else 2537 test!(func, ct)(`foo'bar`); 2538 2539 static if(ct == CheckText.attValueQuot) 2540 testFail!(func, ct)(`foo"bar`); 2541 else 2542 test!(func, ct)(`foo"bar`); 2543 2544 static if(ct == CheckText.comment) 2545 { 2546 testFail!(func, ct)("-"); 2547 testFail!(func, ct)("--"); 2548 testFail!(func, ct)("--*"); 2549 } 2550 else 2551 { 2552 test!(func, ct)("-"); 2553 test!(func, ct)("--"); 2554 test!(func, ct)("--*"); 2555 } 2556 2557 static if(ct == CheckText.pi) 2558 testFail!(func, ct)("?>"); 2559 else 2560 test!(func, ct)("?>"); 2561 } 2562 2563 static foreach(ct; [CheckText.attValueApos, CheckText.attValueQuot, CheckText.pi]) 2564 { 2565 test!(func, ct)("]]>"); 2566 test!(func, ct)("foo]]>bar"); 2567 } 2568 static foreach(ct; [CheckText.cdata, CheckText.text]) 2569 { 2570 testFail!(func, ct)("]]>"); 2571 testFail!(func, ct)("foo]]>bar"); 2572 } 2573 2574 static foreach(ct; [CheckText.cdata, CheckText.comment, CheckText.pi]) 2575 { 2576 test!(func, ct)("<"); 2577 test!(func, ct)("&"); 2578 test!(func, ct)("&x"); 2579 test!(func, ct)("&&;"); 2580 test!(func, ct)("&a"); 2581 test!(func, ct)("hello&;"); 2582 test!(func, ct)("hello&;world"); 2583 test!(func, ct)("hello&<;world"); 2584 test!(func, ct)("hello&world"); 2585 test!(func, ct)("hello world&"); 2586 test!(func, ct)("hello world&;"); 2587 test!(func, ct)("hello world&foo"); 2588 test!(func, ct)("&#;"); 2589 test!(func, ct)("&#x;"); 2590 test!(func, ct)("&#AF;"); 2591 test!(func, ct)("&#x"); 2592 test!(func, ct)("*"); 2593 test!(func, ct)("B"); 2594 test!(func, ct)(""); 2595 test!(func, ct)(""); 2596 test!(func, ct)("*foo\nbar&#;"); 2597 test!(func, ct)("*foo\nbar&#x;"); 2598 test!(func, ct)("*foo\nbar&#AF;"); 2599 test!(func, ct)("*foo\nbar&#x"); 2600 test!(func, ct)("*foo\nbar*"); 2601 test!(func, ct)("*foo\nbarB"); 2602 test!(func, ct)("プログラミング&"); 2603 } 2604 } 2605 2606 // These can't be tested with testFail, because attempting to convert 2607 // invalid Unicode results in UnicodeExceptions before checkText even 2608 // gets called. 2609 import std.meta : AliasSeq; 2610 static foreach(str; AliasSeq!(cast(string)[255], cast(wstring)[0xD800], cast(dstring)[0xD800])) 2611 { 2612 static foreach(ct; EnumMembers!CheckText) 2613 { 2614 assertThrown!XMLWritingException(checkText!ct(str)); 2615 assertThrown!XMLWritingException(checkText!ct(str)); 2616 } 2617 } 2618 } 2619 2620 version(dxmlTests) @safe pure unittest 2621 { 2622 static foreach(ct; EnumMembers!CheckText) 2623 checkText!ct("foo"); 2624 }