1 /*
2 *             Copyright Lodovico Giaretta 2016 - .
3 *  Distributed under the Boost Software License, Version 1.0.
4 *      (See accompanying file LICENSE_1_0.txt or copy at
5 *            http://www.boost.org/LICENSE_1_0.txt)
6 */
7 
8 /++
9 +   This module implements components to put XML data in `OutputRange`s
10 +/
11 
12 module newxml.writer;
13 
14 import newxml.interfaces;
15 @safe:
16 private string ifCompiles(string code)
17 {
18     return "static if (__traits(compiles, " ~ code ~ ")) " ~ code ~ ";\n";
19 }
20 private string ifCompilesElse(string code, string fallback)
21 {
22     return "static if (__traits(compiles, " ~ code ~ ")) " ~ code ~ "; else " ~ fallback ~ ";\n";
23 }
24 private string ifAnyCompiles(string code, string[] codes...)
25 {
26     if (codes.length == 0)
27         return "static if (__traits(compiles, " ~ code ~ ")) " ~ code ~ ";";
28     else
29         return "static if (__traits(compiles, " ~ code ~ ")) " ~ code ~
30                 "; else " ~ ifAnyCompiles(codes[0], codes[1..$]);
31 }
32 
33 import std.typecons : tuple;
34 private auto xmlDeclarationAttributes(StringType, Args...)(Args args)
35 {
36     static assert(Args.length <= 3, "Too many arguments for xml declaration");
37 
38     // version specification
39     static if (is(Args[0] == int))
40     {
41         assert(args[0] == 10 || args[0] == 11, "Invalid xml version specified");
42         StringType versionString = args[0] == 10 ? "1.0" : "1.1";
43         auto args1 = args[1..$];
44     }
45     else static if (is(Args[0] == StringType))
46     {
47         StringType versionString = args[0];
48         auto args1 = args[1..$];
49     }
50     else
51     {
52         StringType versionString = [];
53         auto args1 = args;
54     }
55 
56     // encoding specification
57     static if (is(typeof(args1[0]) == StringType))
58     {
59         auto encodingString = args1[0];
60         auto args2 = args1[1..$];
61     }
62     else
63     {
64         StringType encodingString = [];
65         auto args2 = args1;
66     }
67 
68     // standalone specification
69     static if (is(typeof(args2[0]) == bool))
70     {
71         StringType standaloneString = args2[0] ? "yes" : "no";
72         auto args3 = args2[1..$];
73     }
74     else
75     {
76         StringType standaloneString = [];
77         auto args3 = args2;
78     }
79 
80     // catch other erroneous parameters
81     static assert(typeof(args3).length == 0,
82                   "Unrecognized attribute type for xml declaration: " ~ typeof(args3[0]).stringof);
83 
84     return tuple(versionString, encodingString, standaloneString);
85 }
86 
87 /++
88 +   A collection of ready-to-use pretty-printers
89 +/
90 struct PrettyPrinters
91 {
92     /++
93     +   The minimal pretty-printer. It just guarantees that the input satisfies
94     +   the xml grammar.
95     +/
96     struct Minimalizer(StringType)
97     {
98         // minimum requirements needed for correctness
99         enum StringType beforeAttributeName = " ";
100         enum StringType betweenPITargetData = " ";
101     }
102     /++
103     +   A pretty-printer that indents the nodes with a tabulation character
104     +   `'\t'` per level of nesting.
105     +/
106     struct Indenter(StringType)
107     {
108         // inherit minimum requirements
109         Minimalizer!StringType minimalizer;
110         alias minimalizer this;
111 
112         enum StringType afterNode = "\n";
113         enum StringType attributeDelimiter = "'";
114 
115         uint indentation;
116         enum StringType tab = "\t";
117         void decreaseLevel() { indentation--; }
118         void increaseLevel() { indentation++; }
119 
120         void beforeNode(Out)(ref Out output)
121         {
122             foreach (i; 0..indentation)
123                 output ~= tab;
124         }
125     }
126 }
127 
128 /++
129 +   Component that outputs XML data to an `OutputRange`.
130 +
131 +   To format the XML data, it calls specific methods of the `PrettyPrinter`, if
132 +   they are defined. Otherwise, it just prints the data with the minimal markup required.
133 +   The currently available format callbacks are:
134 +   $(UL
135 +       $(LI `beforeNode`, called as the first operation of outputting every XML node;
136 +                          expected to return a string to be printed before the node)
137 +       $(LI `afterNode`, called as the last operation of outputting every XML node;
138 +                         expected to return a string to be printed after the node)
139 +       $(LI `increaseLevel`, called after the start of a node that may have children
140 +                             (like a start tag or a doctype with an internal subset))
141 +       $(LI `decreaseLevel`, called before the end of a node that had some children
142 +                             (i.e. before writing a closing tag or the end of a doctype
143 +                              with an internal subset))
144 +       $(LI `beforeAttributeName`, called to obtain a string to be used as spacing
145 +                                   between the tag name and the first attribute name
146 +                                   and between the attribute value and the name of the
147 +                                   next attribute; it is not used between the value
148 +                                   of the last attribute and the closing `>`, nor between
149 +                                   the tag name and the closing `>` if the element
150 +                                   has no attributes)
151 +       $(LI `beforeElementEnd`, called to obtain a string to be used as spacing
152 +                                before the closing `>` of a tag, that is after the
153 +                                last attribute name or after the tag name if the
154 +                                element has no attributes)
155 +       $(LI `afterAttributeName`, called to obtain a string to be used as spacing
156 +                                  between the name of an attribute and the `=` sign)
157 +       $(LI `beforeAttributeValue`, called to obtain a string to be used as spacing
158 +                                  between the `=` sign and the value of an attribute)
159 +       $(LI `formatAttribute(outputRange, attibuteValue)`, called to write out the value
160 +                                                           of an attribute)
161 +       $(LI `formatAttribute(attributeValue)`, called to obtain a string that represents
162 +                                               the formatted attribute to be printed; used
163 +                                               when the previous method is not defined)
164 +       $(LI `attributeDelimiter`, called to obtain a string that represents the delimiter
165 +                                  to be used when writing attributes; used when the previous
166 +                                  two methods are not defined; in this case the attribute
167 +                                  is not subject to any formatting, except prepending and
168 +                                  appending the string returned by this method)
169 +       $(LI `afterCommentStart`, called to obtain a string that represents the spacing
170 +                                 to be used between the `<!--` opening and the comment contents)
171 +       $(LI `beforeCommentEnd`, called to obtain a string that represents the spacing
172 +                                to be used between the comment contents and the closing `-->`)
173 +       $(LI `betweenPITargetData`, called to obtain a string to be used as spacing
174 +                                   between the target and data of a processing instruction)
175 +       $(LI `beforePIEnd`, called to obtain a string to be used as spacing between
176 +                           the processing instruction data and the closing `?>`)
177 +   )
178 +   Template arguments:
179 +       _StringType = The type of string to be targeted. The function `writeDOM` will take care of all UTF conversion
180 +   if necessary.
181 +       PrettyPrinter = A struct, that will handle any and all formatting.
182 +       validateTagOrder = If set to `Yes`, then tag order will be validated during writing.
183 +/
184 struct Writer(_StringType, alias PrettyPrinter = PrettyPrinters.Minimalizer)
185     if(is(_StringType == string) || is(_StringType == wstring) || is(_StringType == dstring))
186 {
187     alias StringType = _StringType;
188 
189     static if (is(PrettyPrinter))
190         private PrettyPrinter prettyPrinter;
191     else static if (is(PrettyPrinter!StringType))
192         private PrettyPrinter!StringType prettyPrinter;
193     else
194         static assert(0, "Invalid pretty printer type for string type " ~ StringType.stringof);
195 
196     StringType output;
197     
198     bool startingTag = false, insideDTD = false;
199 
200     this(typeof(prettyPrinter) pretty)
201     {
202         prettyPrinter = pretty;
203     }
204 
205     private template expand(string methodName)
206     {
207         import std.meta : AliasSeq;
208         alias expand = AliasSeq!(
209             "prettyPrinter." ~ methodName ~ "(output)",
210             "output ~= prettyPrinter." ~ methodName
211         );
212     }
213     private template formatAttribute(string attribute)
214     {
215         import std.meta : AliasSeq;
216         alias formatAttribute = AliasSeq!(
217             "prettyPrinter.formatAttribute(output, " ~ attribute ~ ")",
218             "output ~= prettyPrinter.formatAttribute(" ~ attribute ~ ")",
219             "defaultFormatAttribute(" ~ attribute ~ ", prettyPrinter.attributeDelimiter)",
220             "defaultFormatAttribute(" ~ attribute ~ ")"
221         );
222     }
223 
224     private void defaultFormatAttribute(StringType attribute, StringType delimiter = "'")
225     {
226         // TODO: delimiter escaping
227         output ~= delimiter;
228         output ~= attribute;
229         output ~= delimiter;
230     }
231 
232     /++
233     +   Outputs an XML declaration.
234     +
235     +   Its arguments must be an `int` specifying the version
236     +   number (`10` or `11`), a string specifying the encoding (no check is performed on
237     +   this parameter) and a `bool` specifying the standalone property of the document.
238     +   Any argument can be skipped, but the specified arguments must respect the stated
239     +   ordering (which is also the ordering required by the XML specification).
240     +/
241     void writeXMLDeclaration(Args...)(Args args)
242     {
243         auto attrs = xmlDeclarationAttributes!StringType(args);
244 
245         output ~= "<?xml";
246 
247         if (attrs[0])
248         {
249             mixin(ifAnyCompiles(expand!"beforeAttributeName"));
250             output ~= "version";
251             mixin(ifAnyCompiles(expand!"afterAttributeName"));
252             output ~= "=";
253             mixin(ifAnyCompiles(expand!"beforeAttributeValue"));
254             mixin(ifAnyCompiles(formatAttribute!"attrs[0]"));
255         }
256         if (attrs[1])
257         {
258             mixin(ifAnyCompiles(expand!"beforeAttributeName"));
259             output ~= "encoding";
260             mixin(ifAnyCompiles(expand!"afterAttributeName"));
261             output ~= "=";
262             mixin(ifAnyCompiles(expand!"beforeAttributeValue"));
263             mixin(ifAnyCompiles(formatAttribute!"attrs[1]"));
264         }
265         if (attrs[2])
266         {
267             mixin(ifAnyCompiles(expand!"beforeAttributeName"));
268             output ~= "standalone";
269             mixin(ifAnyCompiles(expand!"afterAttributeName"));
270             output ~= "=";
271             mixin(ifAnyCompiles(expand!"beforeAttributeValue"));
272             mixin(ifAnyCompiles(formatAttribute!"attrs[2]"));
273         }
274 
275         mixin(ifAnyCompiles(expand!"beforePIEnd"));
276         output ~= "?>";
277         mixin(ifAnyCompiles(expand!"afterNode"));
278     }
279     void writeXMLDeclaration(StringType version_, StringType encoding, StringType standalone)
280     {
281         output ~= "<?xml";
282 
283         if (version_)
284         {
285             mixin(ifAnyCompiles(expand!"beforeAttributeName"));
286             output ~= "version";
287             mixin(ifAnyCompiles(expand!"afterAttributeName"));
288             output ~= "=";
289             mixin(ifAnyCompiles(expand!"beforeAttributeValue"));
290             mixin(ifAnyCompiles(formatAttribute!"version_"));
291         }
292         if (encoding)
293         {
294             mixin(ifAnyCompiles(expand!"beforeAttributeName"));
295             output ~= "encoding";
296             mixin(ifAnyCompiles(expand!"afterAttributeName"));
297             output ~= "=";
298             mixin(ifAnyCompiles(expand!"beforeAttributeValue"));
299             mixin(ifAnyCompiles(formatAttribute!"encoding"));
300         }
301         if (standalone)
302         {
303             mixin(ifAnyCompiles(expand!"beforeAttributeName"));
304             output ~= "standalone";
305             mixin(ifAnyCompiles(expand!"afterAttributeName"));
306             output ~= "=";
307             mixin(ifAnyCompiles(expand!"beforeAttributeValue"));
308             mixin(ifAnyCompiles(formatAttribute!"standalone"));
309         }
310 
311         output ~= "?>";
312         mixin(ifAnyCompiles(expand!"afterNode"));
313     }
314 
315     /++
316     +   Outputs a comment with the given content.
317     +/
318     void writeComment(StringType comment)
319     {
320         closeOpenThings;
321 
322         mixin(ifAnyCompiles(expand!"beforeNode"));
323         output ~= "<!--";
324         mixin(ifAnyCompiles(expand!"afterCommentStart"));
325 
326         mixin(ifCompilesElse(
327             "prettyPrinter.formatComment(output, comment)",
328             "output ~= comment"
329         ));
330 
331         mixin(ifAnyCompiles(expand!"beforeCommentEnd"));
332         output ~= "-->";
333         mixin(ifAnyCompiles(expand!"afterNode"));
334     }
335     /++
336     +   Outputs a text node with the given content.
337     +/
338     void writeText(StringType text)
339     {
340         //assert(!insideDTD);
341         closeOpenThings;
342 
343         mixin(ifAnyCompiles(expand!"beforeNode"));
344         mixin(ifCompilesElse(
345             "prettyPrinter.formatText(output, comment)",
346             "output ~= text"
347         ));
348         mixin(ifAnyCompiles(expand!"afterNode"));
349     }
350     /++
351     +   Outputs a CDATA section with the given content.
352     +/
353     void writeCDATA(StringType cdata)
354     {
355         assert(!insideDTD);
356         closeOpenThings;
357 
358         mixin(ifAnyCompiles(expand!"beforeNode"));
359         output ~= "<![CDATA[";
360         output ~= cdata;
361         output ~= "]]>";
362         mixin(ifAnyCompiles(expand!"afterNode"));
363     }
364     /++
365     +   Outputs a processing instruction with the given target and data.
366     +/
367     void writeProcessingInstruction(StringType target, StringType data)
368     {
369         closeOpenThings;
370 
371         mixin(ifAnyCompiles(expand!"beforeNode"));
372         output ~= "<?";
373         output ~= target;
374         mixin(ifAnyCompiles(expand!"betweenPITargetData"));
375         output ~= data;
376 
377         mixin(ifAnyCompiles(expand!"beforePIEnd"));
378         output ~= "?>";
379         mixin(ifAnyCompiles(expand!"afterNode"));
380     }
381 
382     private void closeOpenThings()
383     {
384         if (startingTag)
385         {
386             mixin(ifAnyCompiles(expand!"beforeElementEnd"));
387             output ~= ">";
388             mixin(ifAnyCompiles(expand!"afterNode"));
389             startingTag = false;
390             mixin(ifCompiles("prettyPrinter.increaseLevel"));
391         }
392     }
393 
394     void startElement(StringType tagName)
395     {
396         closeOpenThings();
397 
398         mixin(ifAnyCompiles(expand!"beforeNode"));
399         output ~= "<";
400         output ~= tagName;
401         startingTag = true;
402     }
403     void closeElement(StringType tagName)
404     {
405         bool selfClose;
406         mixin(ifCompilesElse(
407             "selfClose = prettyPrinter.selfClosingElements",
408             "selfClose = true"
409         ));
410 
411         if (selfClose && startingTag)
412         {
413             mixin(ifAnyCompiles(expand!"beforeElementEnd"));
414             output ~= "/>";
415             startingTag = false;
416         }
417         else
418         {
419             closeOpenThings;
420 
421             mixin(ifCompiles("prettyPrinter.decreaseLevel"));
422             mixin(ifAnyCompiles(expand!"beforeNode"));
423             output ~= "</";
424             output ~= tagName;
425             mixin(ifAnyCompiles(expand!"beforeElementEnd"));
426             output ~= ">";
427         }
428         mixin(ifAnyCompiles(expand!"afterNode"));
429     }
430     void writeAttribute(StringType name, StringType value)
431     {
432         assert(startingTag, "Cannot write attribute outside element start");
433 
434         mixin(ifAnyCompiles(expand!"beforeAttributeName"));
435         output ~= name;
436         mixin(ifAnyCompiles(expand!"afterAttributeName"));
437         output ~= "=";
438         mixin(ifAnyCompiles(expand!"beforeAttributeValue"));
439         mixin(ifAnyCompiles(formatAttribute!"value"));
440     }
441 
442     void startDoctype(StringType content)
443     {
444         assert(!insideDTD && !startingTag);
445 
446         mixin(ifAnyCompiles(expand!"beforeNode"));
447         output ~= "<!DOCTYPE";
448         output ~= content;
449         mixin(ifAnyCompiles(expand!"afterDoctypeId"));
450         output ~= "[";
451         insideDTD = true;
452         mixin(ifAnyCompiles(expand!"afterNode"));
453         mixin(ifCompiles("prettyPrinter.increaseLevel"));
454     }
455     void closeDoctype()
456     {
457         assert(insideDTD);
458 
459         mixin(ifCompiles("prettyPrinter.decreaseLevel"));
460         insideDTD = false;
461         mixin(ifAnyCompiles(expand!"beforeDTDEnd"));
462         output ~= "]>";
463         mixin(ifAnyCompiles(expand!"afterNode"));
464     }
465     void writeDeclaration(StringType decl, StringType content)
466     {
467         //assert(insideDTD);
468 
469         mixin(ifAnyCompiles(expand!"beforeNode"));
470         output ~= "<!";
471         output ~= decl;
472         output ~= content;
473         output ~= ">";
474         mixin(ifAnyCompiles(expand!"afterNode"));
475     }
476 }
477 
478 unittest
479 {
480     import std.array : Appender;
481     import std.typecons : refCounted;
482 
483     //string app;
484     auto writer = Writer!(string)();
485     //writer.setSink(app);
486 
487     writer.writeXMLDeclaration(10, "utf-8", false);
488     assert(writer.output == "<?xml version='1.0' encoding='utf-8' standalone='no'?>", writer.output);
489 
490     //static assert(isWriter!(typeof(writer)));
491 }
492 
493 unittest
494 {
495     import std.array : Appender;
496     import std.typecons : refCounted;
497     
498     auto writer = Writer!(string, PrettyPrinters.Indenter)();
499 
500     writer.startElement("elem");
501     writer.writeAttribute("attr1", "val1");
502     writer.writeAttribute("attr2", "val2");
503     writer.writeComment("Wonderful comment");
504     writer.startElement("self-closing");
505     writer.closeElement("self-closing");
506     writer.writeText("Wonderful text");
507     writer.writeCDATA("Wonderful cdata");
508     writer.writeProcessingInstruction("pi", "it works");
509     writer.closeElement("elem");
510 
511     import std.string : lineSplitter;
512     auto splitter = writer.output.lineSplitter;
513 
514     assert(splitter.front == "<elem attr1='val1' attr2='val2'>", splitter.front);
515     splitter.popFront;
516     assert(splitter.front == "\t<!--Wonderful comment-->");
517     splitter.popFront;
518     assert(splitter.front == "\t<self-closing/>");
519     splitter.popFront;
520     assert(splitter.front == "\tWonderful text");
521     splitter.popFront;
522     assert(splitter.front == "\t<![CDATA[Wonderful cdata]]>");
523     splitter.popFront;
524     assert(splitter.front == "\t<?pi it works?>");
525     splitter.popFront;
526     assert(splitter.front == "</elem>");
527     splitter.popFront;
528     assert(splitter.empty);
529 }
530 
531 import dom = newxml.dom;
532 import newxml.domstring;
533 
534 /++
535 +   Outputs the entire DOM tree rooted at `node` using the given `writer`.
536 +/
537 void writeDOM(WriterType)(auto ref WriterType writer, dom.Node node)
538 {
539     import std.traits : ReturnType;
540     import newxml.faststrings;
541     alias Document = typeof(node.ownerDocument);
542     alias Element = ReturnType!(Document.documentElement);
543     alias StringType = writer.StringType;
544 
545     switch (node.nodeType) with (dom.NodeType)
546     {
547         case document:
548             auto doc = cast(Document)node;
549             DOMString xmlVersion = doc.xmlVersion, xmlEncoding = doc.xmlEncoding;
550             writer.writeXMLDeclaration(xmlVersion ? xmlVersion.transcodeTo!StringType() : null, 
551                     xmlEncoding ? xmlEncoding.transcodeTo!StringType() : null, doc.xmlStandalone);
552             foreach (child; doc.childNodes)
553                 writer.writeDOM(child);
554             break;
555         case element:
556             auto elem = cast(Element)node;
557             writer.startElement(elem.tagName.transcodeTo!StringType);
558             if (elem.hasAttributes)
559                 foreach (attr; elem.attributes)
560                     writer.writeAttribute(attr.nodeName.transcodeTo!StringType, 
561                             xmlEscape(attr.nodeValue.transcodeTo!StringType));
562             foreach (child; elem.childNodes)
563                 writer.writeDOM(child);
564             writer.closeElement(elem.tagName.transcodeTo!StringType);
565             break;
566         case text:
567             writer.writeText(xmlEscape(node.nodeValue.transcodeTo!StringType));
568             break;
569         case cdataSection:
570             writer.writeCDATA(xmlEscape(node.nodeValue.transcodeTo!StringType));
571             break;
572         case comment:
573             writer.writeComment(node.nodeValue.transcodeTo!StringType);
574             break;
575         default:
576             break;
577     }
578 }
579 
580 unittest
581 {
582     import newxml.domimpl;
583     Writer!(string, PrettyPrinters.Minimalizer) wrt = Writer!(string)(PrettyPrinters.Minimalizer!string());
584 
585     dom.DOMImplementation domimpl = new DOMImplementation;
586     dom.Document doc = domimpl.createDocument(null, new DOMString("doc"), null);
587     dom.Element e0 = doc.createElement(new DOMString("text"));
588     doc.firstChild.appendChild(e0);
589     e0.setAttribute(new DOMString("something"), new DOMString("other thing"));
590     e0.appendChild(doc.createTextNode(new DOMString("Some text ")));
591     dom.Element e1 = doc.createElement(new DOMString("b"));
592     e1.appendChild(doc.createTextNode(new DOMString("with")));
593     e0.appendChild(e1);
594     e0.appendChild(doc.createTextNode(new DOMString(" markup.")));
595     
596     wrt.writeDOM(doc);
597 
598     assert(wrt.output == "<?xml version='1.0' standalone='no'?><doc><text something='other thing'>Some text <b>with</b> markup.</text></doc>", wrt.output);
599 }
600 
601 import std.typecons : Flag, No, Yes;
602 
603 /++
604 +   Writes the contents of a cursor to a writer.
605 +
606 +   This method advances the cursor till the end of the document, outputting all
607 +   nodes using the given writer. The actual work is done inside a fiber, which is
608 +   then returned. This means that if the methods of the cursor call `Fiber.yield`,
609 +   this method will not complete its work, but will return a fiber in `HOLD` status,
610 +   which the user can `call` to advance the work. This is useful if the cursor
611 +   has to wait for other nodes to be ready (e.g. if the cursor input is generated
612 +   programmatically).
613 +/
614 auto writeCursor(Flag!"useFiber" useFiber = No.useFiber, WriterType, CursorType)
615                 (auto ref WriterType writer, auto ref CursorType cursor)
616 {
617     alias StringType = WriterType.StringType;
618     void inspectOneLevel() @safe
619     {
620         do
621         {
622             switch (cursor.kind) with (XMLKind)
623             {
624                 case document:
625                     StringType version_, encoding, standalone;
626                     foreach (attr; cursor.attributes)
627                         if (attr.name == "version")
628                             version_ = attr.value;
629                         else if (attr.name == "encoding")
630                             encoding = attr.value;
631                         else if (attr.name == "standalone")
632                             standalone = attr.value;
633                     writer.writeXMLDeclaration(version_, encoding, standalone);
634                     if (cursor.enter)
635                     {
636                         inspectOneLevel();
637                         cursor.exit;
638                     }
639                     break;
640                 case dtdEmpty:
641                 case dtdStart:
642                     writer.startDoctype(cursor.wholeContent);
643                     if (cursor.enter)
644                     {
645                         inspectOneLevel();
646                         cursor.exit;
647                     }
648                     writer.closeDoctype();
649                     break;
650                 case attlistDecl:
651                     writer.writeDeclaration("ATTLIST", cursor.wholeContent);
652                     break;
653                 case elementDecl:
654                     writer.writeDeclaration("ELEMENT", cursor.wholeContent);
655                     break;
656                 case entityDecl:
657                     writer.writeDeclaration("ENTITY", cursor.wholeContent);
658                     break;
659                 case notationDecl:
660                     writer.writeDeclaration("NOTATION", cursor.wholeContent);
661                     break;
662                 case declaration:
663                     writer.writeDeclaration(cursor.name, cursor.content);
664                     break;
665                 case text:
666                     writer.writeText(cursor.content);
667                     break;
668                 case cdata:
669                     writer.writeCDATA(cursor.content);
670                     break;
671                 case comment:
672                     writer.writeComment(cursor.content);
673                     break;
674                 case processingInstruction:
675                     writer.writeProcessingInstruction(cursor.name, cursor.content);
676                     break;
677                 case elementStart:
678                 case elementEmpty:
679                     writer.startElement(cursor.name);
680                     for (auto attrs = cursor.attributes; !attrs.empty; attrs.popFront)
681                     {
682                         auto attr = attrs.front;
683                         writer.writeAttribute(attr.name, attr.value);
684                     }
685                     if (cursor.enter)
686                     {
687                         inspectOneLevel();
688                         cursor.exit;
689                     }
690                     writer.closeElement(cursor.name);
691                     break;
692                 default:
693                     break;
694                     //assert(0);
695             }
696         }
697         while (cursor.next);
698     }
699 
700     static if (useFiber)
701     {
702         import core.thread: Fiber;
703         auto fiber = new Fiber(&inspectOneLevel);
704         fiber.call;
705         return fiber;
706     }
707     else
708         inspectOneLevel();
709 }
710 
711 unittest
712 {
713     import std.array : Appender;
714     import newxml.parser;
715     import newxml.cursor;
716     import newxml.lexers;
717     import std.typecons : refCounted;
718 
719     string xml =
720     "<?xml?>\n" ~
721     "<!DOCTYPE ciaone [\n" ~
722     "\t<!ELEMENT anything here>\n" ~
723     "\t<!ATTLIST no check at all...>\n" ~
724     "\t<!NOTATION dunno what to write>\n" ~
725     "\t<!ENTITY .....>\n" ~
726     "\t<!I_SAID_NO_CHECKS_AT_ALL_BY_DEFAULT>\n" ~
727     "]>\n";
728 
729     auto cursor = xml.lexer.parser.cursor;
730     cursor.setSource(xml);
731 
732     auto writer = Writer!(string, PrettyPrinters.Indenter)();
733 
734     writer.writeCursor(cursor);
735 
736     assert(writer.output == xml);
737 }
738 
739 /++
740 +   A wrapper around a writer that, before forwarding every write operation, validates
741 +   the input given by the user using a chain of validating cursors.
742 +
743 +   This type should not be instantiated directly, but with the helper function
744 +   `withValidation`.
745 +/
746 struct CheckedWriter(WriterType, CursorType = void)
747     if (isWriter!(WriterType) && (is(CursorType == void) ||
748        (isCursor!CursorType && is(WriterType.StringType == CursorType.StringType))))
749 {
750     import core.thread : Fiber;
751     private Fiber fiber;
752     private bool startingTag = false;
753 
754     WriterType writer;
755     alias writer this;
756 
757     alias StringType = WriterType.StringType;
758 
759     static if (is(CursorType == void))
760     {
761         struct Cursor
762         {
763             import newxml.cursor: Attribute;
764             import std.container.array;
765 
766             alias StringType = WriterType.StringType;
767 
768             private StringType _name, _content;
769             private Array!(Attribute!StringType) attrs;
770             private XMLKind _kind;
771             private size_t colon;
772             private bool initialized;
773 
774             void _setName(StringType name)
775             {
776                 import newxml.faststrings;
777                 _name = name;
778                 auto i = name.fastIndexOf(':');
779                 if (i > 0)
780                     colon = i;
781                 else
782                     colon = 0;
783             }
784             void _addAttribute(StringType name, StringType value)
785             {
786                 attrs.insertBack(Attribute!StringType(name, value));
787             }
788             void _setKind(XMLKind kind)
789             {
790                 _kind = kind;
791                 initialized = true;
792                 attrs.clear;
793             }
794             void _setContent(StringType content) { _content = content; }
795 
796             auto kind()
797             {
798                 if (!initialized)
799                     Fiber.yield;
800 
801                 return _kind;
802             }
803             auto name() { return _name; }
804             auto prefix() { return _name[0..colon]; }
805             auto content() { return _content; }
806             auto attributes() { return attrs[]; }
807             StringType localName()
808             {
809                 if (colon)
810                     return _name[colon+1..$];
811                 else
812                     return [];
813             }
814 
815             bool enter()
816             {
817                 if (_kind == XMLKind.document)
818                 {
819                     Fiber.yield;
820                     return true;
821                 }
822                 if (_kind != XMLKind.elementStart)
823                     return false;
824 
825                 Fiber.yield;
826                 return _kind != XMLKind.elementEnd;
827             }
828             bool next()
829             {
830                 Fiber.yield;
831                 return _kind != XMLKind.elementEnd;
832             }
833             void exit() {}
834             bool atBeginning()
835             {
836                 return !initialized || _kind == XMLKind.document;
837             }
838             bool documentEnd() { return false; }
839 
840             alias InputType = void*;
841             StringType wholeContent()
842             {
843                 assert(0, "Cannot call wholeContent on this type of cursor");
844             }
845             void setSource(InputType)
846             {
847                 assert(0, "Cannot set the source of this type of cursor");
848             }
849         }
850         Cursor cursor;
851     }
852     else
853     {
854         CursorType cursor;
855     }
856 
857     void writeXMLDeclaration(Args...)(Args args)
858     {
859         auto attrs = xmlDeclarationAttributes!StringType(args);
860         cursor._setKind(XMLKind.document);
861         if (attrs[0])
862             cursor._addAttribute("version", attrs[0]);
863         if (attrs[1])
864             cursor._addAttribute("encoding", attrs[1]);
865         if (attrs[2])
866             cursor._addAttribute("standalone", attrs[2]);
867         fiber.call;
868     }
869     void writeXMLDeclaration(StringType version_, StringType encoding, StringType standalone)
870     {
871         cursor._setKind(XMLKind.document);
872         if (version_)
873             cursor._addAttribute("version", version_);
874         if (encoding)
875             cursor._addAttribute("encoding", encoding);
876         if (standalone)
877             cursor._addAttribute("standalone", standalone);
878         fiber.call;
879     }
880     void writeComment(StringType text)
881     {
882         if (startingTag)
883         {
884             fiber.call;
885             startingTag = false;
886         }
887         cursor._setKind(XMLKind.comment);
888         cursor._setContent(text);
889         fiber.call;
890     }
891     void writeText(StringType text)
892     {
893         if (startingTag)
894         {
895             fiber.call;
896             startingTag = false;
897         }
898         cursor._setKind(XMLKind.text);
899         cursor._setContent(text);
900         fiber.call;
901     }
902     void writeCDATA(StringType text)
903     {
904         if (startingTag)
905         {
906             fiber.call;
907             startingTag = false;
908         }
909         cursor._setKind(XMLKind.cdata);
910         cursor._setContent(text);
911         fiber.call;
912     }
913     void writeProcessingInstruction(StringType target, StringType data)
914     {
915         if (startingTag)
916         {
917             fiber.call;
918             startingTag = false;
919         }
920         cursor._setKind(XMLKind.comment);
921         cursor._setName(target);
922         cursor._setContent(data);
923         fiber.call;
924     }
925     void startElement(StringType tag)
926     {
927         if (startingTag)
928             fiber.call;
929 
930         startingTag = true;
931         cursor._setKind(XMLKind.elementStart);
932         cursor._setName(tag);
933     }
934     void closeElement(StringType tag)
935     {
936         if (startingTag)
937         {
938             fiber.call;
939             startingTag = false;
940         }
941         cursor._setKind(XMLKind.elementEnd);
942         cursor._setName(tag);
943         fiber.call;
944     }
945     void writeAttribute(StringType name, StringType value)
946     {
947         assert(startingTag);
948         cursor._addAttribute(name, value);
949     }
950 }
951 
952 /+unittest
953 {
954     import newxml.validation;
955     import std.typecons : refCounted;
956 
957     string app;
958 
959     auto writer =
960          Writer!(string, PrettyPrinters.Indenter)();
961     writer.setSink(app);
962 
963     writer.writeXMLDeclaration(10, "utf-8", false);
964     assert(app.data == "<?xml version='1.0' encoding='utf-8' standalone='no'?>\n");
965 
966     writer.writeComment("a nice comment");
967     writer.startElement("aa;bb");
968     writer.writeAttribute(";eh", "foo");
969     writer.writeText("a nice text");
970     writer.writeCDATA("a nice cdata");
971     writer.closeElement("aabb");
972 }+/