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="&amp;"`);
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="&amp;"/>`);
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="&amp;"/>` ~ "\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 &amp;&amp; bar &lt; 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                                                   "&apos; 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                                                   "&quot; 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)("&amp;&gt;&lt;");
2479             test!(func, ct)("hello&amp;&gt;&lt;world");
2480             test!(func, ct)(".....&apos;&quot;&amp;.....");
2481             test!(func, ct)("&#12487;&#12451;&#12521;&#12531;");
2482             test!(func, ct)("-hello&#xAF;&#42;&quot;-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)("&#42");
2520             testFail!(func, ct)("&#x42");
2521             testFail!(func, ct)("&#12;");
2522             testFail!(func, ct)("&#x12;");
2523             testFail!(func, ct)("&#42;foo\nbar&#;");
2524             testFail!(func, ct)("&#42;foo\nbar&#x;");
2525             testFail!(func, ct)("&#42;foo\nbar&#AF;");
2526             testFail!(func, ct)("&#42;foo\nbar&#x");
2527             testFail!(func, ct)("&#42;foo\nbar&#42");
2528             testFail!(func, ct)("&#42;foo\nbar&#x42");
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)("&#42");
2593             test!(func, ct)("&#x42");
2594             test!(func, ct)("&#12;");
2595             test!(func, ct)("&#x12;");
2596             test!(func, ct)("&#42;foo\nbar&#;");
2597             test!(func, ct)("&#42;foo\nbar&#x;");
2598             test!(func, ct)("&#42;foo\nbar&#AF;");
2599             test!(func, ct)("&#42;foo\nbar&#x");
2600             test!(func, ct)("&#42;foo\nbar&#42");
2601             test!(func, ct)("&#42;foo\nbar&#x42");
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 }