1 // Written in the D programming language
2 
3 /++
4     This module provides functionality for creating XML 1.0 documents.
5 
6     $(H3 Primary Symbols)
7     $(TABLE
8         $(TR $(TH Symbol) $(TH Description))
9         $(TR $(TD $(LREF XMLWriter))
10              $(TD Type used for writing XML documents.))
11         $(TR $(TD $(LREF xmlWriter))
12              $(TD Function used to create an $(LREF XMLWriter).))
13     )
14 
15     $(H3 Helper Types)
16     $(TABLE
17         $(TR $(TD $(LREF XMLWritingException))
18              $(TD Thrown by $(LREF XMLWriter) when it's given data that would
19                   result in invalid XML.))
20     )
21 
22     $(H3 Helper Functions)
23     $(TABLE
24         $(TR $(TH Symbol) $(TH Description))
25         $(TR $(TD $(LREF writeTaggedText))
26              $(TD Shortcut for writing text enclosed by tags. e.g.
27                   $(D_CODE_STRING $(LT)tag>text$(LT)/tag>).))
28         $(TR $(TD $(LREF writeXMLDecl))
29              $(TD Writes the optional XML declaration that goes at the top of
30                   an XML document to an ouput range.))
31     )
32 
33     Copyright: Copyright 2018 - 2020
34     License:   $(HTTP www.boost.org/LICENSE_1_0.txt, Boost License 1.0).
35     Authors:   $(HTTPS jmdavisprog.com, Jonathan M Davis)
36     Source:    $(LINK_TO_SRC dxml/_writer.d)
37 
38     See_Also: $(LINK2 http://www.w3.org/TR/REC-xml/, Official Specification for XML 1.0)
39   +/
40 module dxml.writer;
41 
42 import std.range.primitives;
43 import std.traits;
44 import std.typecons : Flag;
45 
46 
47 /++
48     Exception thrown when the writer is given data that would result in invalid
49     XML.
50   +/
51 class XMLWritingException : Exception
52 {
53 private:
54 
55     this(string msg, string file = __FILE__, size_t line = __LINE__) @safe pure nothrow @nogc
56     {
57         super(msg, file, line);
58     }
59 }
60 
61 
62 /++
63     $(PHOBOS_REF Flag, std, typecons) indicating whether
64     $(LREF2 closeStartTag, XMLWriter) or $(LREF2 writeStartTag, XMLWriter) or
65     will write an empty element tag (which then does not require a corresponding
66     end tag).
67   +/
68 alias EmptyTag = Flag!"EmptyTag";
69 
70 
71 /++
72     $(PHOBOS_REF Flag, std, typecons) indicating whether a write function of
73     $(LREF XMLWriter) will write a newline followed by an indent before the
74     entity being written.
75   +/
76 alias Newline = Flag!"Newline";
77 
78 
79 /++
80     $(PHOBOS_REF Flag, std, typecons) indicating whether a write function of
81     $(LREF XMLWriter) which accepts text which may include newlines will write
82     an indent after each newline is written.
83   +/
84 alias InsertIndent = Flag!"InsertIndent";
85 
86 
87 /++
88     Writes XML to an output range of characters.
89 
90     Note that default initialization, copying, and assignment are disabled for
91     XMLWriter. This is because XMLWriter is essentially a reference type, but
92     in many cases, it doesn't need to be passed around, and forcing it to be
93     allocated on the heap in order to be a reference type seemed like an
94     unnecessary heap allocation. So, it's a struct with default initialization,
95     copying, and assignment disabled so that like a reference type, it will not
96     be copied or overwritten. Code that needs to pass it around can pass it by
97     $(K_REF) or use the $(LREF_ALTTEXT constructor, _XMLWriter.this) to
98     explicitly allocate it on the heap and then pass around the resulting
99     pointer.
100 
101     The optional $(LREF Newline) and $(LREF InsertIndent) parameters to the
102     various write functions are used to control the formatting of the XML, and
103     $(LREF2 writeIndent, _XMLWriter) and $(LREF2 _output, _XMLWriter) can be
104     used for additional control over the formatting.
105 
106     The indent provided to the XMLWriter is the base indent that will be used
107     whenever $(LREF2 writeIndent, _XMLWriter) and any write functions using
108     $(D Newline.yes) or $(D InsertIndent.yes) are called - e.g.
109     if the base indent is 4 spaces, $(D $(LREF2 tagDepth, _XMLWriter) == 3), and
110     $(D Newline.yes) is passed to $(LREF2 writeComment, _XMLWriter), then a
111     newline followed by 12 spaces will be written to the output range after the
112     comment.
113 
114     $(LREF writeXMLDecl) can be used to write the $(D <?xml...?>) declaration
115     to the output range before constructing an XML writer, but if an application
116     wishes to do anything with a DTD section, it will have to write that to the
117     output range on its own before constructing the XMLWriter. XMLWriter expects
118     to start writing XML after any $(D <?xml...?>) or $(D <!DOCTYPE...>)
119     declarations.
120 
121     The write functions check the arguments prior to writing anything to the
122     output range, so the XMLWriter is not in an invalid state after an
123     $(LREF XMLWritingException) is thrown, but it $(I is) in an invalid state
124     if any other exception is thrown (which will only occur if an input range
125     that is passed to a write function throws or if the ouput range throws when
126     XMLWriter calls $(PHOBOS_REF_ALTTEXT put, put, std, range, primitives) on
127     it).
128 
129     Params:
130         output = The _output range that the XML will be written to.
131         baseIndent = Optional argument indicating the base indent to be used
132                      when an indent is inserted after a newline in the XML (with
133                      the actual indent being the base indent inserted once for
134                      each level of the $(LREF2 tagDepth, _XMLWriter)).
135                      The default is four spaces. baseIndent may only contain
136                      spaces and/or tabs.
137 
138     See_Also: $(LREF writeXMLDecl)$(BR)
139               $(REF encodeAttr, dxml, util)$(BR)
140               $(REF encodeText, dxml, util)$(BR)
141               $(REF StdEntityRef, dxml, util)$(BR)
142               $(REF toCharRef, dxml, util)$(BR)
143   +/
144 struct XMLWriter(OR)
145     if(isOutputRange!(OR, char))
146 {
147     import std.range.primitives;
148     import std.traits;
149 
150     enum compileInTests = is(OR == XMLWriterCompileTests);
151 
152 public:
153 
154 
155     /++
156         Writes the first portion of a start tag to the given output range.
157 
158         Once openStartTag has been called,
159         $(LREF2 writeAttr, XMLWriter) can be called to add attributes
160         to the start tag. $(LREF2 closeStartTag, XMLWriter) writes the closing
161         portion of the start tag.
162 
163         Once openStartTag has been called, it is an error to call any
164         function on XMLWriter other than $(LREF2 closeStartTag, XMLWriter),
165         $(LREF2 writeAttr, XMLWriter), $(LREF2 writeIndent, XMLWriter),
166         $(LREF2 tagDepth, XMLWriter), $(LREF2 baseIndent, XMLWriter), or
167         $(LREF2 output, XMLWriter) until $(LREF2 closeStartTag, XMLWriter) has
168         been called (basically, any function that involves writing XML that is
169         not legal in a start tag can't be called until the start tag has been
170         properly closed).
171 
172         It is also an error to call openStartTag after the end tag for the root
173         element has been written.
174 
175         Params:
176             name = The name of the start tag.
177             newline = Whether a _newline followed by an indent will be written
178                       to the output range before the start tag.
179 
180         Throws: $(LREF XMLWritingException) if the given _name is not a valid
181                 XML tag _name.
182 
183         See_Also: $(LREF2 writeStartTag, XMLWriter)$(BR)
184                   $(LREF2 writeAttr, XMLWriter)$(BR)
185                   $(LREF2 closeStartTag, XMLWriter)$(BR)
186                   $(LREF2 writeEndTag, XMLWriter)$(BR)
187                   $(LINK http://www.w3.org/TR/REC-xml/#NT-STag)
188       +/
189     void openStartTag(string name, Newline newline = Newline.yes)
190     {
191         _validateStartTag!"openStartTag"(name);
192         if(newline == Newline.yes)
193             put(_output, _getIndent(tagDepth));
194         _startTagOpen = true;
195         _incLevel(name);
196         put(_output, '<');
197         put(_output, name);
198     }
199 
200     // This is so that openStartTag, writeStartTag, and writeTaggedText can
201     // share this code.
202     private void _validateStartTag(string funcName)(string name)
203     {
204         assert(!_startTagOpen, funcName ~ " cannot be called when a start tag is already open");
205         // FIXME It seems like a bug that version(assert) would be required to
206         // reference a symbol declared with version(assert) when it's being
207         // referenced inside an assertion.
208         version(assert)
209             assert(!_writtenRootEnd, funcName ~ " cannot be called after the root element's end tag has been written.");
210         checkName(name);
211     }
212 
213     ///
214     static if(compileInTests) unittest
215     {
216         import std.array : appender;
217         import std.exception : assertThrown;
218 
219         auto writer = xmlWriter(appender!string());
220 
221         writer.openStartTag("root", Newline.no);
222         assert(writer.output.data == "<root");
223 
224         writer.closeStartTag();
225         assert(writer.output.data == "<root>");
226 
227         // Neither < nor > is allowed in a tag name.
228         assertThrown!XMLWritingException(writer.openStartTag("<tag>"));
229 
230         // Unchanged after an XMLWritingException is thrown.
231         assert(writer.output.data == "<root>");
232 
233         writer.openStartTag("foo");
234         assert(writer.output.data ==
235                "<root>\n" ~
236                "    <foo");
237 
238         writer.writeAttr("answer", "42");
239         assert(writer.output.data ==
240                "<root>\n" ~
241                `    <foo answer="42"`);
242 
243         writer.closeStartTag(EmptyTag.yes);
244         assert(writer.output.data ==
245                "<root>\n" ~
246                `    <foo answer="42"/>`);
247 
248         writer.writeEndTag();
249         assert(writer.output.data ==
250                "<root>\n" ~
251                `    <foo answer="42"/>` ~ "\n" ~
252                "</root>");
253     }
254 
255     static if(compileInTests) @safe pure unittest
256     {
257         import dxml.internal : TestAttrOR;
258         auto writer = xmlWriter(TestAttrOR.init);
259         writer.openStartTag("root");
260     }
261 
262 
263     /++
264         Writes an attribute for a start tag to the output range.
265 
266         It is an error to call writeAttr except between calls to
267         $(LREF2 openStartTag, XMLWriter) and $(LREF2 closeStartTag, XMLWriter).
268 
269         Params:
270             quote = The quote character to use for the attribute value's
271                     delimiter.
272             name = The name of the attribute.
273             value = The value of the attribute.
274             newline = Whether a _newline followed by an indent will be written
275                       to the output range before the attribute. Note that unlike
276                       most write functions, the default is $(D Newline.no)
277                       (since it's more common to not want newlines between
278                       attributes).
279 
280         Throws: $(LREF XMLWritingException) if the given _name is not a valid
281                 XML attribute _name, if the given _value is not a valid XML
282                 attribute _value, or if the given _name has already been written
283                 to the current start tag. $(REF encodeAttr, dxml, util) can be
284                 used to encode any characters that are not legal in their
285                 literal form in an attribute _value but are legal as entity
286                 references.
287 
288         See_Also: $(REF encodeAttr, dxml, util)$(BR)
289                   $(REF StdEntityRef, dxml, util)$(BR)
290                   $(REF toCharRef, dxml, util)$(BR)
291                   $(LINK http://www.w3.org/TR/REC-xml/#NT-Attribute)
292       +/
293     void writeAttr(char quote = '"', R)(string name, R value, Newline newline = Newline.no)
294         if((quote == '"' || quote == '\'') &&
295            isForwardRange!R && isSomeChar!(ElementType!R))
296     {
297         assert(_startTagOpen, "writeAttr cannot be called except when a start tag is open");
298 
299         checkName(name);
300         static if(quote == '"')
301             checkText!(CheckText.attValueQuot)(value.save);
302         else
303             checkText!(CheckText.attValueApos)(value.save);
304 
305         import std.algorithm.searching : canFind;
306         if(_attributes.canFind(name))
307             throw new XMLWritingException("Duplicate attribute name: " ~ name);
308         _attributes ~= name;
309 
310         if(newline == Newline.yes)
311             put(_output, _getIndent(tagDepth));
312         else
313             put(_output, ' ');
314         put(_output, name);
315         put(_output, "=" ~ quote);
316         put(_output, value);
317         put(_output, quote);
318     }
319 
320     ///
321     static if(compileInTests) unittest
322     {
323         import std.array : appender;
324         import std.exception : assertThrown;
325         import dxml.util : encodeAttr;
326 
327         auto writer = xmlWriter(appender!string());
328 
329         writer.openStartTag("root", Newline.no);
330         assert(writer.output.data == "<root");
331 
332         writer.writeAttr("a", "one");
333         assert(writer.output.data == `<root a="one"`);
334 
335         writer.writeAttr("b", "two");
336         assert(writer.output.data == `<root a="one" b="two"`);
337 
338         // It's illegal for two attributes on the same start tag
339         // to have the same name.
340         assertThrown!XMLWritingException(writer.writeAttr("a", "three"));
341 
342         // Invalid name.
343         assertThrown!XMLWritingException(writer.writeAttr("=", "value"));
344 
345         // Can't have a quote that matches the enclosing quote.
346         assertThrown!XMLWritingException(writer.writeAttr("c", `foo"bar`));
347         assertThrown!XMLWritingException(writer.writeAttr!'\''("c", "foo'bar"));
348 
349         // Unchanged after an XMLWritingException is thrown.
350         assert(writer.output.data == `<root a="one" b="two"`);
351 
352         writer.closeStartTag();
353         assert(writer.output.data == `<root a="one" b="two">`);
354 
355         writer.openStartTag("foobar");
356         assert(writer.output.data ==
357                `<root a="one" b="two">` ~ "\n" ~
358                "    <foobar");
359 
360         // " is the default for the quote character, but ' can be specified.
361         writer.writeAttr!'\''("answer", "42");
362         assert(writer.output.data ==
363                `<root a="one" b="two">` ~ "\n" ~
364                "    <foobar answer='42'");
365 
366         writer.writeAttr("base", "13", Newline.yes);
367         assert(writer.output.data ==
368                `<root a="one" b="two">` ~ "\n" ~
369                "    <foobar answer='42'\n" ~
370                `        base="13"`);
371 
372         writer.closeStartTag();
373         assert(writer.output.data ==
374                `<root a="one" b="two">` ~ "\n" ~
375                "    <foobar answer='42'\n" ~
376                `        base="13">`);
377 
378         writer.openStartTag("tag");
379         assert(writer.output.data ==
380                `<root a="one" b="two">` ~ "\n" ~
381                "    <foobar answer='42'\n" ~
382                `        base="13">` ~ "\n" ~
383                "        <tag");
384 
385         // &, <, and > are not legal in an attribute value.
386         assertThrown!XMLWritingException(writer.writeAttr("foo", "&"));
387 
388         // Unchanged after an XMLWritingException is thrown.
389         assert(writer.output.data ==
390                `<root a="one" b="two">` ~ "\n" ~
391                "    <foobar answer='42'\n" ~
392                `        base="13">` ~ "\n" ~
393                "        <tag");
394 
395         // Use dxml.util.encodeAttr to encode characters that aren't
396         // legal in an attribute value but can legally be encoded.
397         writer.writeAttr("foo", encodeAttr("&"));
398         assert(writer.output.data ==
399                `<root a="one" b="two">` ~ "\n" ~
400                "    <foobar answer='42'\n" ~
401                `        base="13">` ~ "\n" ~
402                `        <tag foo="&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 unittest
1905 {
1906     import std.array : appender;
1907     {
1908         auto writer = xmlWriter(appender!string());
1909         writer.writeStartTag("root");
1910 
1911         writer.openStartTag("foo");
1912         writer.writeAttr("a", "42");
1913         writer.closeStartTag();
1914 
1915         writer.writeText("bar");
1916 
1917         writer.writeEndTag("foo");
1918 
1919         writer.writeEndTag("root");
1920 
1921         assert(writer.output.data ==
1922                "\n" ~
1923                "<root>\n" ~
1924                `    <foo a="42">` ~ "\n" ~
1925                "        bar\n" ~
1926                "    </foo>\n" ~
1927                "</root>");
1928     }
1929 
1930     // Newline.no can be used to avoid inserting newlines.
1931     {
1932         auto writer = xmlWriter(appender!string());
1933 
1934         // Unless writeXMLDecl was used, Newline.no is needed on the first
1935         // entity to avoid having the document start with a newline.
1936         writer.writeStartTag("root", Newline.no);
1937 
1938         writer.openStartTag("foo");
1939         writer.writeAttr("a", "42");
1940         writer.closeStartTag();
1941 
1942         writer.writeText("bar", Newline.no);
1943 
1944         writer.writeEndTag("foo", Newline.no);
1945 
1946         writer.writeEndTag("root");
1947 
1948         assert(writer.output.data ==
1949                "<root>\n" ~
1950                `    <foo a="42">bar</foo>` ~ "\n" ~
1951                "</root>");
1952     }
1953 }
1954 
1955 @safe pure nothrow unittest
1956 {
1957     import dxml.internal : TestAttrOR;
1958     auto writer = xmlWriter(TestAttrOR.init);
1959 }
1960 
1961 // This is purely to provide a way to trigger the unittest blocks in XMLWriter
1962 // without compiling them in normally.
1963 private struct XMLWriterCompileTests
1964 {
1965     void put(char c) @safe pure nothrow @nogc { assert(0); }
1966 }
1967 
1968 unittest
1969 {
1970     auto _xmlWriterTests = XMLWriter!(XMLWriterCompileTests).init;
1971 }
1972 
1973 
1974 /++
1975     Writes the $(D <?xml...?>) declaration to the given output range. If it's
1976     going to be used in conjunction with $(LREF XMLWriter), then either
1977     writeXMLDecl will need to be called before constructing the
1978     $(LREF XMLWriter), or $(LREF XMLWriter._output) will need to be used to
1979     write to the output range before writing anything else using the
1980     $(LREF XMLWriter). $(LREF XMLWriter) expects to be writing XML after the
1981     $(D <?xml...?>) and $(D <!DOCTYPE...>) declarations (assuming they're
1982     present at all), and it is invalid to put a $(D <?xml...?>) declaration
1983     anywhere but at the very beginning of an XML document.
1984 
1985     Params:
1986         S = The string type used to infer the encoding type. Ideally, it would
1987             be inferred from the type of the _output range, but unfortunately,
1988             the _output range API does not provide that functionality. If S
1989             does not match the encoding of the _output range, then the result
1990             will be invalid XML.
1991         output = The _output range to write to.
1992   +/
1993 void writeXMLDecl(S, OR)(ref OR output)
1994     if(isOutputRange!(OR, char) && isSomeString!S)
1995 {
1996     put(output, `<?xml version="1.0"`);
1997     static if(is(Unqual!(ElementEncodingType!S) == char))
1998         put(output, ` encoding="UTF-8"?>`);
1999     else static if(is(Unqual!(ElementEncodingType!S) == wchar))
2000         put(output, ` encoding="UTF-16"?>`);
2001     else
2002         put(output, ` encoding="UTF-32"?>`);
2003 }
2004 
2005 ///
2006 unittest
2007 {
2008     import std.array : appender;
2009 
2010     {
2011         auto app = appender!string();
2012         app.writeXMLDecl!string();
2013         assert(app.data == `<?xml version="1.0" encoding="UTF-8"?>`);
2014     }
2015     {
2016         auto app = appender!wstring();
2017         app.writeXMLDecl!wstring();
2018         assert(app.data == `<?xml version="1.0" encoding="UTF-16"?>`w);
2019     }
2020     {
2021         auto app = appender!dstring();
2022         app.writeXMLDecl!dstring();
2023         assert(app.data == `<?xml version="1.0" encoding="UTF-32"?>`d);
2024     }
2025 
2026     // This would be invalid XML, because the output range contains UTF-8, but
2027     // writeXMLDecl is told to write that the encoding is UTF-32.
2028     {
2029         auto app = appender!string();
2030         app.writeXMLDecl!dstring();
2031         assert(app.data == `<?xml version="1.0" encoding="UTF-32"?>`);
2032     }
2033 }
2034 
2035 
2036 /++
2037     Helper function for writing _text which has a start tag and end tag on each
2038     side and no attributes so that it can be done with one function call instead
2039     of three.
2040 
2041     writeTaggedText is essentially equivalent to calling
2042 
2043     ---
2044     writer.writeStartTag(name, newline);
2045     writer.writeText(text, insertIndent, Newline.no);
2046     writer.writeEndTag(Newline.no);
2047     ---
2048 
2049     with the difference being that both the name and text are validated before
2050     any data is written. So, if the text is invalid XML, then nothing will have
2051     been written to the output range when the exception is thrown (whereas if
2052     each function were called individually, then the start tag would have been
2053     written before the exception was thrown from $(LREF2 writeText, XMLWriter)).
2054 
2055     If more control is needed over the formatting, or if attributes are needed
2056     on the start tag, then the functions will have to be called separately
2057     instead of calling writeTaggedText.
2058 
2059     Params:
2060             writer = The $(LREF XMLWriter) to write to.
2061             name = The _name of the start tag.
2062             text = The _text to write between the start and end tags.
2063             newline = Whether a _newline followed by an indent will be written
2064                       to the output range before the start tag.
2065             insertIndent = Whether an indent will be inserted after each
2066                            _newline within the _text.
2067 
2068     Throws: $(LREF XMLWritingException) if the given _name is an invalid XML
2069             tag _name or if the given _text contains any characters or sequence
2070             of characters which are not legal in the _text portion of an XML
2071             document. $(REF encodeText, dxml, util) can be used to encode any
2072             characters that are not legal in their literal form in the _text but
2073             are legal as entity references.
2074 
2075     See_Also: $(LREF2 writeStartTag, XMLWriter)$(BR)
2076               $(LREF2 writeText, XMLWriter)$(BR)
2077               $(LREF2 writeEndTag, XMLWriter)
2078   +/
2079 void writeTaggedText(XW, R)(ref XW writer, string name, R text, Newline newline = Newline.yes,
2080                             InsertIndent insertIndent = InsertIndent.yes)
2081     if(isInstanceOf!(XMLWriter, XW) &&
2082        isForwardRange!R && isSomeChar!(ElementType!R))
2083 {
2084     writer._validateStartTag!"writeTaggedText"(name);
2085     writer._validateText!"writeTaggedText"(text.save);
2086 
2087     writer._writeStartTag(name, EmptyTag.no, newline);
2088     writer._writeText(text, Newline.no, insertIndent);
2089     writer.writeEndTag(Newline.no);
2090 }
2091 
2092 /// Ditto
2093 void writeTaggedText(XW, R)(ref XW writer, string name, R text, InsertIndent insertIndent,
2094                             Newline newline = Newline.yes)
2095     if(isInstanceOf!(XMLWriter, XW) &&
2096        isForwardRange!R && isSomeChar!(ElementType!R))
2097 {
2098     writeTaggedText(writer, name, text, newline, insertIndent);
2099 }
2100 
2101 ///
2102 unittest
2103 {
2104     import std.array : appender;
2105 
2106     {
2107         auto writer = xmlWriter(appender!string());
2108         writer.writeStartTag("root", Newline.no);
2109         writer.writeTaggedText("foo", "Some text between foos");
2110         writer.writeEndTag("root");
2111 
2112         assert(writer.output.data ==
2113                "<root>\n" ~
2114                "    <foo>Some text between foos</foo>\n" ~
2115                "</root>");
2116     }
2117 
2118     // With Newline.no
2119     {
2120         auto writer = xmlWriter(appender!string());
2121         writer.writeStartTag("root", Newline.no);
2122         writer.writeTaggedText("foo", "Some text between foos", Newline.no);
2123         writer.writeEndTag("root");
2124 
2125         assert(writer.output.data ==
2126                "<root><foo>Some text between foos</foo>\n" ~
2127                "</root>");
2128     }
2129 
2130     // With InsertIndent.yes
2131     {
2132         auto writer = xmlWriter(appender!string());
2133         writer.writeStartTag("root", Newline.no);
2134         writer.writeTaggedText("foo", "Some text\nNext line");
2135         writer.writeEndTag("root");
2136 
2137         assert(writer.output.data ==
2138                "<root>\n" ~
2139                "    <foo>Some text\n" ~
2140                "        Next line</foo>\n" ~
2141                "</root>");
2142     }
2143 
2144     // With InsertIndent.no
2145     {
2146         auto writer = xmlWriter(appender!string());
2147         writer.writeStartTag("root", Newline.no);
2148         writer.writeTaggedText("foo", "Some text\nNext line", InsertIndent.no);
2149         writer.writeEndTag("root");
2150 
2151         assert(writer.output.data ==
2152                "<root>\n" ~
2153                "    <foo>Some text\n" ~
2154                "Next line</foo>\n" ~
2155                "</root>");
2156     }
2157 }
2158 
2159     unittest
2160     {
2161         import std.array : appender;
2162         import std.exception : assertThrown;
2163         import dxml.internal : testRangeFuncs;
2164 
2165         foreach(func; testRangeFuncs)
2166         {
2167             auto writer = xmlWriter(appender!string);
2168             writer.writeStartTag("root", Newline.no);
2169             writer.writeTaggedText("foo", func("hello sally"));
2170             assertThrown!XMLWritingException(writer.writeTaggedText("foo", func("\v")));
2171             assertThrown!XMLWritingException(writer.writeTaggedText("foo", func("&bar")));
2172             assertThrown!XMLWritingException(writer.writeTaggedText("foo", func("--<--")));
2173             assertThrown!XMLWritingException(writer.writeTaggedText("foo", func("--&--")));
2174             assertThrown!XMLWritingException(writer.writeTaggedText(".f", func("bar")));
2175             writer.writeTaggedText("f.", func("--"));
2176             writer.writeTaggedText("a", func("&foo; &bar; &baz;"), Newline.no);
2177             writer.writeTaggedText("a", func("&foo; \n &bar;\n&baz;"));
2178             writer.writeTaggedText("a", func("&foo; \n &bar;\n&baz;"), InsertIndent.no);
2179             assert(writer.output.data ==
2180                    "<root>\n" ~
2181                    "    <foo>hello sally</foo>\n" ~
2182                    "    <f.>--</f.><a>&foo; &bar; &baz;</a>\n" ~
2183                    "    <a>&foo; \n" ~
2184                    "         &bar;\n" ~
2185                    "        &baz;</a>\n" ~
2186                    "    <a>&foo; \n" ~
2187                    " &bar;\n" ~
2188                    "&baz;</a>");
2189         }
2190     }
2191 
2192 // _decLevel cannot currently be pure.
2193 @safe /+pure+/ unittest
2194 {
2195     import dxml.internal : TestAttrOR;
2196     auto writer = xmlWriter(TestAttrOR.init);
2197     writer.writeTaggedText("root", "text");
2198 }
2199 
2200 
2201 private:
2202 
2203 void checkName(R)(R range)
2204 {
2205     import std.format : format;
2206     import std.range : takeExactly;
2207     import std.utf : byCodeUnit, decodeFront, UseReplacementDchar;
2208     import dxml.internal : isNameStartChar, isNameChar;
2209 
2210     auto text = range.byCodeUnit();
2211 
2212     size_t takeLen;
2213     {
2214         immutable decodedC = text.decodeFront!(UseReplacementDchar.yes)(takeLen);
2215         if(!isNameStartChar(decodedC))
2216             throw new XMLWritingException(format!"Name contains invalid character: 0x%0x"(decodedC));
2217     }
2218 
2219     while(!text.empty)
2220     {
2221         size_t numCodeUnits;
2222         immutable decodedC = text.decodeFront!(UseReplacementDchar.yes)(numCodeUnits);
2223         if(!isNameChar(decodedC))
2224             throw new XMLWritingException(format!"Name contains invalid character: 0x%0x"(decodedC));
2225     }
2226 }
2227 
2228 @safe pure unittest
2229 {
2230     import std.exception : assertNotThrown, assertThrown;
2231     import std.range : only;
2232     import dxml.internal : testRangeFuncs;
2233 
2234     static foreach(func; testRangeFuncs)
2235     {
2236         foreach(str; only("hello", "プログラミング", "h_:llo-.42", "_.", "_-", "_42", "プログラミング"))
2237             assertNotThrown!XMLWritingException(checkName(func(str)));
2238 
2239         foreach(str; only(".", ".foo", "-foo", "&foo;", "foo\vbar"))
2240             assertThrown!XMLWritingException(checkName(func(str)));
2241     }
2242 }
2243 
2244 void checkPIName(R)(R range)
2245 {
2246     import std.range : walkLength;
2247     import std.uni : icmp;
2248     import std.utf : byCodeUnit;
2249 
2250     if(icmp(range.save.byCodeUnit(), "xml") == 0)
2251         throw new XMLWritingException("Processing instructions cannot be named xml");
2252     checkName(range);
2253 }
2254 
2255 @safe pure unittest
2256 {
2257     import std.exception : assertNotThrown, assertThrown;
2258     import std.range : only;
2259     import dxml.internal : testRangeFuncs;
2260 
2261     static foreach(func; testRangeFuncs)
2262     {
2263         foreach(str; only("hello", "プログラミング", "h_:llo-.42", "_.", "_-", "_42", "プログラミング", "xmlx"))
2264             assertNotThrown!XMLWritingException(checkPIName(func(str)));
2265 
2266         foreach(str; only(".", ".foo", "-foo", "&foo;", "foo\vbar", "xml", "XML", "xMl"))
2267             assertThrown!XMLWritingException(checkPIName(func(str)));
2268     }
2269 }
2270 
2271 
2272 enum CheckText
2273 {
2274     attValueApos,
2275     attValueQuot,
2276     cdata,
2277     comment,
2278     pi,
2279     text
2280 }
2281 
2282 void checkText(CheckText ct, R)(R range)
2283 {
2284     import std.format : format;
2285     import std.utf : byCodeUnit, decodeFront, UseReplacementDchar;
2286 
2287     auto text = range.byCodeUnit();
2288 
2289     loop: while(!text.empty)
2290     {
2291         switch(text.front)
2292         {
2293             static if(ct == CheckText.attValueApos || ct == CheckText.attValueQuot || ct == CheckText.text)
2294             {
2295                 case '&':
2296                 {
2297                     import dxml.util : parseCharRef;
2298 
2299                     {
2300                         auto temp = text.save;
2301                         auto charRef = parseCharRef(temp);
2302                         if(!charRef.isNull)
2303                         {
2304                             static if(hasLength!(typeof(text)))
2305                                 text = temp;
2306                             else
2307                             {
2308                                 while(text.front != ';')
2309                                     text.popFront();
2310                                 text.popFront();
2311                             }
2312                             continue;
2313                         }
2314                     }
2315 
2316                     text.popFront();
2317 
2318                     import dxml.internal : isNameStartChar, isNameChar;
2319 
2320                     if(text.empty)
2321                         goto failedEntityRef;
2322 
2323                     {
2324                         size_t numCodeUnits;
2325                         immutable decodedC = text.decodeFront!(UseReplacementDchar.yes)(numCodeUnits);
2326                         if(!isNameStartChar(decodedC))
2327                             goto failedEntityRef;
2328                     }
2329 
2330                     while(true)
2331                     {
2332                         if(text.empty)
2333                             goto failedEntityRef;
2334                         immutable c = text.front;
2335                         if(c == ';')
2336                         {
2337                             text.popFront();
2338                             break;
2339                         }
2340                         size_t numCodeUnits;
2341                         immutable decodedC = text.decodeFront!(UseReplacementDchar.yes)(numCodeUnits);
2342                         if(!isNameChar(decodedC))
2343                             goto failedEntityRef;
2344                     }
2345                     break;
2346 
2347                     failedEntityRef:
2348                     throw new XMLWritingException("& is only legal in an attribute value as part of a " ~
2349                                                   "character or entity reference, and this is not a valid " ~
2350                                                   "character or entity reference.");
2351                 }
2352                 case '<': throw new XMLWritingException("< is not legal in EntityType.text");
2353             }
2354             static if(ct == CheckText.comment)
2355             {
2356                 case '-':
2357                 {
2358                     text.popFront();
2359                     if(text.empty)
2360                         throw new XMLWritingException("- is not legal at the end of an EntityType.comment");
2361                     if(text.front == '-')
2362                         throw new XMLWritingException("-- is not legal in EntityType.comment");
2363                     break;
2364                 }
2365             }
2366             else static if(ct == CheckText.pi)
2367             {
2368                 case '?':
2369                 {
2370                     text.popFront();
2371                     if(!text.empty && text.front == '>')
2372                         throw new XMLWritingException("A EntityType.pi cannot contain ?>");
2373                     break;
2374                 }
2375             }
2376             else static if(ct == CheckText.cdata || ct == CheckText.text)
2377             {
2378                 case ']':
2379                 {
2380                     import std.algorithm.searching : startsWith;
2381                     text.popFront();
2382                     if(text.save.startsWith("]>"))
2383                     {
2384                         static if(ct == CheckText.cdata)
2385                             throw new XMLWritingException("]]> is not legal in EntityType.cdata");
2386                         else
2387                             throw new XMLWritingException("]]> is not legal in EntityType.text");
2388                     }
2389                     break;
2390                 }
2391             }
2392             else static if(ct == CheckText.attValueApos)
2393             {
2394                 case '\'':
2395                 {
2396                     throw new XMLWritingException("If a single quote is the attrbute value's delimiter, then it's " ~
2397                                                   "illegal for the attribute value to contain a single quote. Either " ~
2398                                                   "instantiate writeAttr with a double quote instead or use " ~
2399                                                   "&apos; in the attribute value instead of a single quote.");
2400                 }
2401             }
2402             else static if(ct == CheckText.attValueQuot)
2403             {
2404                 case '"':
2405                 {
2406                     throw new XMLWritingException("If a double quote is the attrbute value's delimiter, then it's " ~
2407                                                   "illegal for the attribute value to contain a double quote. Either " ~
2408                                                   "instantiate writeAttr with a single quote instead or use " ~
2409                                                   "&quot; in the attribute value instead of a double quote.");
2410                 }
2411             }
2412             case '\n':
2413             {
2414                 text.popFront();
2415                 break;
2416             }
2417             default:
2418             {
2419                 import std.ascii : isASCII;
2420                 import dxml.internal : isXMLChar;
2421                 immutable c = text.front;
2422                 if(isASCII(c))
2423                 {
2424                     if(!isXMLChar(c))
2425                         throw new XMLWritingException(format!"Character is not legal in an XML File: 0x%0x"(c));
2426                     text.popFront();
2427                 }
2428                 else
2429                 {
2430                     import std.utf : UTFException;
2431                     // Annoyngly, letting decodeFront throw is the easier way to handle this, since the
2432                     // replacement character is considered valid XML, and if we decoded using it, then
2433                     // all of the invalid Unicode characters would come out as the replacement character
2434                     // and then be treated as valid instead of being caught, which we could do, but then
2435                     // the resulting XML document would contain the replacement character without the
2436                     // caller knowing it, which almost certainly means that a bug would go unnoticed.
2437                     try
2438                     {
2439                         size_t numCodeUnits;
2440                         immutable decodedC = text.decodeFront!(UseReplacementDchar.no)(numCodeUnits);
2441                         if(!isXMLChar(decodedC))
2442                         {
2443                             enum fmt = "Character is not legal in an XML File: 0x%0x";
2444                             throw new XMLWritingException(format!fmt(decodedC));
2445                         }
2446                     }
2447                     catch(UTFException)
2448                         throw new XMLWritingException("Text contains invalid Unicode character");
2449                 }
2450                 break;
2451             }
2452         }
2453     }
2454 }
2455 
2456 unittest
2457 {
2458     import std.exception : assertNotThrown, assertThrown;
2459     import dxml.internal : testRangeFuncs;
2460 
2461     static void test(alias func, CheckText ct)(string text, size_t line = __LINE__)
2462     {
2463         assertNotThrown(checkText!ct(func(text)), "unittest failure", __FILE__, line);
2464     }
2465 
2466     static void testFail(alias func, CheckText ct)(string text, size_t line = __LINE__)
2467     {
2468         assertThrown!XMLWritingException(checkText!ct(func(text)), "unittest failure", __FILE__, line);
2469     }
2470 
2471     static foreach(func; testRangeFuncs)
2472     {
2473         static foreach(ct; EnumMembers!CheckText)
2474         {
2475             test!(func, ct)("");
2476             test!(func, ct)("J",);
2477             test!(func, ct)("foo");
2478             test!(func, ct)("プログラミング");
2479 
2480             test!(func, ct)("&amp;&gt;&lt;");
2481             test!(func, ct)("hello&amp;&gt;&lt;world");
2482             test!(func, ct)(".....&apos;&quot;&amp;.....");
2483             test!(func, ct)("&#12487;&#12451;&#12521;&#12531;");
2484             test!(func, ct)("-hello&#xAF;&#42;&quot;-world");
2485             test!(func, ct)("&foo;&bar;&baz;");
2486 
2487             test!(func, ct)("]]");
2488             test!(func, ct)("]>");
2489             test!(func, ct)("foo]]bar");
2490             test!(func, ct)("foo]>bar");
2491             test!(func, ct)("]] >");
2492             test!(func, ct)("? >");
2493 
2494             testFail!(func, ct)("\v");
2495             testFail!(func, ct)("\uFFFE");
2496             testFail!(func, ct)("hello\vworld");
2497             testFail!(func, ct)("he\nllo\vwo\nrld");
2498         }
2499 
2500         static foreach(ct; [CheckText.attValueApos, CheckText.attValueQuot, CheckText.text])
2501         {
2502             testFail!(func, ct)("<");
2503             testFail!(func, ct)("&");
2504             testFail!(func, ct)("&");
2505             testFail!(func, ct)("&x");
2506             testFail!(func, ct)("&&;");
2507             testFail!(func, ct)("&a");
2508             testFail!(func, ct)("hello&;");
2509             testFail!(func, ct)("hello&.f;");
2510             testFail!(func, ct)("hello&f?;");
2511             testFail!(func, ct)("hello&;world");
2512             testFail!(func, ct)("hello&<;world");
2513             testFail!(func, ct)("hello&world");
2514             testFail!(func, ct)("hello world&");
2515             testFail!(func, ct)("hello world&;");
2516             testFail!(func, ct)("hello world&foo");
2517             testFail!(func, ct)("&#;");
2518             testFail!(func, ct)("&#x;");
2519             testFail!(func, ct)("&#AF;");
2520             testFail!(func, ct)("&#x");
2521             testFail!(func, ct)("&#42");
2522             testFail!(func, ct)("&#x42");
2523             testFail!(func, ct)("&#12;");
2524             testFail!(func, ct)("&#x12;");
2525             testFail!(func, ct)("&#42;foo\nbar&#;");
2526             testFail!(func, ct)("&#42;foo\nbar&#x;");
2527             testFail!(func, ct)("&#42;foo\nbar&#AF;");
2528             testFail!(func, ct)("&#42;foo\nbar&#x");
2529             testFail!(func, ct)("&#42;foo\nbar&#42");
2530             testFail!(func, ct)("&#42;foo\nbar&#x42");
2531             testFail!(func, ct)("プログラミング&");
2532         }
2533 
2534         static foreach(ct; EnumMembers!CheckText)
2535         {
2536             static if(ct == CheckText.attValueApos)
2537                 testFail!(func, ct)(`foo'bar`);
2538             else
2539                 test!(func, ct)(`foo'bar`);
2540 
2541             static if(ct == CheckText.attValueQuot)
2542                 testFail!(func, ct)(`foo"bar`);
2543             else
2544                 test!(func, ct)(`foo"bar`);
2545 
2546             static if(ct == CheckText.comment)
2547             {
2548                 testFail!(func, ct)("-");
2549                 testFail!(func, ct)("--");
2550                 testFail!(func, ct)("--*");
2551             }
2552             else
2553             {
2554                 test!(func, ct)("-");
2555                 test!(func, ct)("--");
2556                 test!(func, ct)("--*");
2557             }
2558 
2559             static if(ct == CheckText.pi)
2560                 testFail!(func, ct)("?>");
2561             else
2562                 test!(func, ct)("?>");
2563         }
2564 
2565         static foreach(ct; [CheckText.attValueApos, CheckText.attValueQuot, CheckText.pi])
2566         {
2567             test!(func, ct)("]]>");
2568             test!(func, ct)("foo]]>bar");
2569         }
2570         static foreach(ct; [CheckText.cdata, CheckText.text])
2571         {
2572             testFail!(func, ct)("]]>");
2573             testFail!(func, ct)("foo]]>bar");
2574         }
2575 
2576         static foreach(ct; [CheckText.cdata, CheckText.comment, CheckText.pi])
2577         {
2578             test!(func, ct)("<");
2579             test!(func, ct)("&");
2580             test!(func, ct)("&x");
2581             test!(func, ct)("&&;");
2582             test!(func, ct)("&a");
2583             test!(func, ct)("hello&;");
2584             test!(func, ct)("hello&;world");
2585             test!(func, ct)("hello&<;world");
2586             test!(func, ct)("hello&world");
2587             test!(func, ct)("hello world&");
2588             test!(func, ct)("hello world&;");
2589             test!(func, ct)("hello world&foo");
2590             test!(func, ct)("&#;");
2591             test!(func, ct)("&#x;");
2592             test!(func, ct)("&#AF;");
2593             test!(func, ct)("&#x");
2594             test!(func, ct)("&#42");
2595             test!(func, ct)("&#x42");
2596             test!(func, ct)("&#12;");
2597             test!(func, ct)("&#x12;");
2598             test!(func, ct)("&#42;foo\nbar&#;");
2599             test!(func, ct)("&#42;foo\nbar&#x;");
2600             test!(func, ct)("&#42;foo\nbar&#AF;");
2601             test!(func, ct)("&#42;foo\nbar&#x");
2602             test!(func, ct)("&#42;foo\nbar&#42");
2603             test!(func, ct)("&#42;foo\nbar&#x42");
2604             test!(func, ct)("プログラミング&");
2605         }
2606     }
2607 
2608     // These can't be tested with testFail, because attempting to convert
2609     // invalid Unicode results in UnicodeExceptions before checkText even
2610     // gets called.
2611     import std.meta : AliasSeq;
2612     static foreach(str; AliasSeq!(cast(string)[255], cast(wstring)[0xD800], cast(dstring)[0xD800]))
2613     {
2614         static foreach(ct; EnumMembers!CheckText)
2615         {
2616             assertThrown!XMLWritingException(checkText!ct(str));
2617             assertThrown!XMLWritingException(checkText!ct(str));
2618         }
2619     }
2620 }
2621 
2622 @safe pure unittest
2623 {
2624     static foreach(ct; EnumMembers!CheckText)
2625         checkText!ct("foo");
2626 }