1 module hunt.xml.Writer;
2 
3 import hunt.xml.Attribute;
4 import hunt.xml.Common;
5 import hunt.xml.Document;
6 import hunt.xml.Element;
7 import hunt.xml.Node;
8 
9 import hunt.logging.ConsoleLogger;
10 
11 private string ifCompiles(string code) {
12     return "static if (__traits(compiles, " ~ code ~ ")) " ~ code ~ ";\n";
13 }
14 
15 private string ifCompilesElse(string code, string fallback) {
16     return "static if (__traits(compiles, " ~ code ~ ")) " ~ code ~ "; else " ~ fallback ~ ";\n";
17 }
18 
19 private string ifAnyCompiles(string code, string[] codes...) {
20     if (codes.length == 0)
21         return "static if (__traits(compiles, " ~ code ~ ")) " ~ code ~ ";";
22     else
23         return "static if (__traits(compiles, " ~ code ~ ")) " ~ code ~ "; else "
24             ~ ifAnyCompiles(codes[0], codes[1 .. $]);
25 }
26 
27 import std.typecons : tuple;
28 
29 private auto xmlDeclarationAttributes(Args...)(Args args) {
30     static assert(Args.length <= 3, "Too many arguments for xml declaration");
31 
32     // version specification
33     static if (is(Args[0] == int)) {
34         assert(args[0] == 10 || args[0] == 11, "Invalid xml version specified");
35         string versionString = args[0] == 10 ? "1.0" : "1.1";
36         auto args1 = args[1 .. $];
37     } else static if (is(Args[0] == string)) {
38         string versionString = args[0];
39         auto args1 = args[1 .. $];
40     } else {
41         string versionString = [];
42         auto args1 = args;
43     }
44 
45     // encoding specification
46     static if (is(typeof(args1[0]) == string)) {
47         auto encodingString = args1[0];
48         auto args2 = args1[1 .. $];
49     } else {
50         string encodingString = [];
51         auto args2 = args1;
52     }
53 
54     // standalone specification
55     static if (is(typeof(args2[0]) == bool)) {
56         string standaloneString = args2[0] ? "yes" : "no";
57         auto args3 = args2[1 .. $];
58     } else {
59         string standaloneString = [];
60         auto args3 = args2;
61     }
62 
63     // catch other erroneous parameters
64     static assert(typeof(args3).length == 0,
65             "Unrecognized attribute type for xml declaration: " ~ typeof(args3[0]).stringof);
66 
67     return tuple(versionString, encodingString, standaloneString);
68 }
69 
70 /++
71 +   A collection of ready-to-use pretty-printers
72 +/
73 struct PrettyPrinters {
74     /++
75     +   The minimal pretty-printer. It just guarantees that the input satisfies
76     +   the xml grammar.
77     +/
78     struct Minimalizer {
79         // minimum requirements needed for correctness
80         enum string beforeAttributeName = " ";
81         enum string betweenPITargetData = " ";
82         bool isSuppressDeclaration = false;
83     }
84     /++
85     +   A pretty-printer that indents the nodes with a tabulation character
86     +   `'\t'` per level of nesting.
87     +/
88     struct Indenter {
89         // inherit minimum requirements
90         Minimalizer minimalizer;
91         alias minimalizer this;
92 
93         enum string afterNode = "\n";
94         enum string attributeDelimiter = "'";
95 
96         uint indentation;
97         enum string tab = "\t";
98         void decreaseLevel() {
99             indentation--;
100         }
101 
102         void increaseLevel() {
103             indentation++;
104         }
105 
106         void beforeNode(Out)(ref Out output) {
107             foreach (i; 0 .. indentation)
108                 output.put(tab);
109         }
110     }
111 }
112 
113 auto buildWriter(OutRange, PrettyPrinter)(ref OutRange output, PrettyPrinter pretty) {
114     return Writer!(OutRange, PrettyPrinter)(output, pretty);
115 }
116 
117 struct Writer(alias OutRange, alias PrettyPrinter = PrettyPrinters.Minimalizer) {
118     private PrettyPrinter prettyPrinter;
119     private OutRange* output;
120 
121     bool startingTag = false, insideDTD = false;
122 
123     this(ref OutRange output, PrettyPrinter pretty) {
124         this.output = &output;
125         prettyPrinter = pretty;
126     }
127 
128     private template expand(string methodName) {
129         import std.meta : AliasSeq;
130 
131         alias expand = AliasSeq!("prettyPrinter." ~ methodName ~ "(output)",
132                 "output.put(prettyPrinter." ~ methodName ~ ")");
133     }
134 
135     private template formatAttribute(string attribute) {
136         import std.meta : AliasSeq;
137 
138         alias formatAttribute = AliasSeq!("prettyPrinter.formatAttribute(output, " ~ attribute ~ ")",
139                 "output.put(prettyPrinter.formatAttribute(" ~ attribute ~ "))",
140                 "defaultFormatAttribute(" ~ attribute ~ ", prettyPrinter.attributeDelimiter)",
141                 "defaultFormatAttribute(" ~ attribute ~ ")");
142     }
143 
144     private void defaultFormatAttribute(string attribute, string delimiter = "'") {
145         // TODO: delimiter escaping
146         output.put(delimiter);
147         output.put(attribute);
148         output.put(delimiter);
149     }
150 
151     /++
152     +   Outputs an XML declaration.
153     +
154     +   Its arguments must be an `int` specifying the version
155     +   number (`10` or `11`), a string specifying the encoding (no check is performed on
156     +   this parameter) and a `bool` specifying the standalone property of the document.
157     +   Any argument can be skipped, but the specified arguments must respect the stated
158     +   ordering (which is also the ordering required by the XML specification).
159     +/
160     void writeXMLDeclaration(Args...)(Args args) {
161         auto attrs = xmlDeclarationAttributes(args);
162 
163         output.put("<?xml");
164 
165         if (attrs[0]) {
166             mixin(ifAnyCompiles(expand!"beforeAttributeName"));
167             output.put("version");
168             mixin(ifAnyCompiles(expand!"afterAttributeName"));
169             output.put("=");
170             mixin(ifAnyCompiles(expand!"beforeAttributeValue"));
171             mixin(ifAnyCompiles(formatAttribute!"attrs[0]"));
172         }
173         if (attrs[1]) {
174             mixin(ifAnyCompiles(expand!"beforeAttributeName"));
175             output.put("encoding");
176             mixin(ifAnyCompiles(expand!"afterAttributeName"));
177             output.put("=");
178             mixin(ifAnyCompiles(expand!"beforeAttributeValue"));
179             mixin(ifAnyCompiles(formatAttribute!"attrs[1]"));
180         }
181         if (attrs[2]) {
182             mixin(ifAnyCompiles(expand!"beforeAttributeName"));
183             output.put("standalone");
184             mixin(ifAnyCompiles(expand!"afterAttributeName"));
185             output.put("=");
186             mixin(ifAnyCompiles(expand!"beforeAttributeValue"));
187             mixin(ifAnyCompiles(formatAttribute!"attrs[2]"));
188         }
189 
190         mixin(ifAnyCompiles(expand!"beforePIEnd"));
191         output.put("?>");
192         mixin(ifAnyCompiles(expand!"afterNode"));
193     }
194 
195     void writeXMLDeclaration(string version_, string encoding, string standalone) {
196         output.put("<?xml");
197 
198         if (version_) {
199             mixin(ifAnyCompiles(expand!"beforeAttributeName"));
200             output.put("version");
201             mixin(ifAnyCompiles(expand!"afterAttributeName"));
202             output.put("=");
203             mixin(ifAnyCompiles(expand!"beforeAttributeValue"));
204             mixin(ifAnyCompiles(formatAttribute!"version_"));
205         }
206         if (encoding) {
207             mixin(ifAnyCompiles(expand!"beforeAttributeName"));
208             output.put("encoding");
209             mixin(ifAnyCompiles(expand!"afterAttributeName"));
210             output.put("=");
211             mixin(ifAnyCompiles(expand!"beforeAttributeValue"));
212             mixin(ifAnyCompiles(formatAttribute!"encoding"));
213         }
214         if (standalone) {
215             mixin(ifAnyCompiles(expand!"beforeAttributeName"));
216             output.put("standalone");
217             mixin(ifAnyCompiles(expand!"afterAttributeName"));
218             output.put("=");
219             mixin(ifAnyCompiles(expand!"beforeAttributeValue"));
220             mixin(ifAnyCompiles(formatAttribute!"standalone"));
221         }
222 
223         output.put("?>");
224         mixin(ifAnyCompiles(expand!"afterNode"));
225     }
226 
227     /++
228     +   Outputs a comment with the given content.
229     +/
230     void writeComment(string comment) {
231         closeOpenThings;
232 
233         mixin(ifAnyCompiles(expand!"beforeNode"));
234         output.put("<!--");
235         mixin(ifAnyCompiles(expand!"afterCommentStart"));
236 
237         mixin(ifCompilesElse("prettyPrinter.formatComment(output, comment)", "output.put(comment)"));
238 
239         mixin(ifAnyCompiles(expand!"beforeCommentEnd"));
240         output.put("-->");
241         mixin(ifAnyCompiles(expand!"afterNode"));
242     }
243     /++
244     +   Outputs a text node with the given content.
245     +/
246     void writeText(string text) {
247         //assert(!insideDTD);
248         closeOpenThingsSimplely();
249         mixin(ifCompilesElse("prettyPrinter.formatText(output, text)", "output.put(text)"));
250     }
251 
252 
253     /++
254     +   Outputs a CDATA section with the given content.
255     +/
256     void writeCDATA(string cdata) {
257         assert(!insideDTD);
258         closeOpenThings;
259 
260         mixin(ifAnyCompiles(expand!"beforeNode"));
261         output.put("<![CDATA[");
262         output.put(cdata);
263         output.put("]]>");
264         mixin(ifAnyCompiles(expand!"afterNode"));
265     }
266     /++
267     +   Outputs a processing instruction with the given target and data.
268     +/
269     void writeProcessingInstruction(string target, string data) {
270         closeOpenThings;
271 
272         mixin(ifAnyCompiles(expand!"beforeNode"));
273         output.put("<?");
274         output.put(target);
275         mixin(ifAnyCompiles(expand!"betweenPITargetData"));
276         output.put(data);
277 
278         mixin(ifAnyCompiles(expand!"beforePIEnd"));
279         output.put("?>");
280         mixin(ifAnyCompiles(expand!"afterNode"));
281     }
282 
283     private void closeOpenThings() {
284         if (startingTag) {
285             mixin(ifAnyCompiles(expand!"beforeElementEnd"));
286             output.put(">");
287             mixin(ifAnyCompiles(expand!"afterNode"));
288             startingTag = false;
289             mixin(ifCompiles("prettyPrinter.increaseLevel"));
290         }
291     }
292     
293     private void closeOpenThingsSimplely() {
294         if (startingTag) {
295             output.put(">");
296             startingTag = false;
297         }
298     }
299 
300     void startElement(string tagName) {
301         closeOpenThings();
302 
303         mixin(ifAnyCompiles(expand!"beforeNode"));
304         output.put("<");
305         output.put(tagName);
306         startingTag = true;
307     }
308 
309     void closeElement(string tagName) {
310         bool selfClose;
311         mixin(ifCompilesElse("selfClose = prettyPrinter.selfClosingElements", "selfClose = true"));
312 
313         if (selfClose && startingTag) {
314             mixin(ifAnyCompiles(expand!"beforeElementEnd"));
315             output.put("/>");
316             startingTag = false;
317         } else {
318             closeOpenThings;
319 
320             mixin(ifCompiles("prettyPrinter.decreaseLevel"));
321             mixin(ifAnyCompiles(expand!"beforeNode"));
322             output.put("</");
323             output.put(tagName);
324             mixin(ifAnyCompiles(expand!"beforeElementEnd"));
325             output.put(">");
326         }
327         mixin(ifAnyCompiles(expand!"afterNode"));
328     }
329 
330     void closeElementWithTextNode(string tagName) {
331         bool selfClose;
332         mixin(ifCompilesElse("selfClose = prettyPrinter.selfClosingElements", "selfClose = true"));
333 
334         if (selfClose && startingTag) {
335             mixin(ifAnyCompiles(expand!"beforeElementEnd"));
336             output.put("/>");
337             startingTag = false;
338         } else {
339             closeOpenThings;
340 
341             // mixin(ifCompiles("prettyPrinter.decreaseLevel"));
342             // mixin(ifAnyCompiles(expand!"beforeNode"));
343             output.put("</");
344             output.put(tagName);
345             mixin(ifAnyCompiles(expand!"beforeElementEnd"));
346             output.put(">");
347         }
348         mixin(ifAnyCompiles(expand!"afterNode"));
349     }    
350 
351     void writeAttribute(string name, string value) {
352         assert(startingTag, "Cannot write attribute outside element start");
353 
354         mixin(ifAnyCompiles(expand!"beforeAttributeName"));
355         output.put(name);
356         mixin(ifAnyCompiles(expand!"afterAttributeName"));
357         output.put("=");
358         mixin(ifAnyCompiles(expand!"beforeAttributeValue"));
359         mixin(ifAnyCompiles(formatAttribute!"value"));
360     }
361 
362     void startDoctype(string content) {
363         assert(!insideDTD && !startingTag);
364 
365         mixin(ifAnyCompiles(expand!"beforeNode"));
366         output.put("<!DOCTYPE");
367         output.put(content);
368         mixin(ifAnyCompiles(expand!"afterDoctypeId"));
369         output.put("[");
370         insideDTD = true;
371         mixin(ifAnyCompiles(expand!"afterNode"));
372         mixin(ifCompiles("prettyPrinter.increaseLevel"));
373     }
374 
375     void closeDoctype() {
376         assert(insideDTD);
377 
378         mixin(ifCompiles("prettyPrinter.decreaseLevel"));
379         insideDTD = false;
380         mixin(ifAnyCompiles(expand!"beforeDTDEnd"));
381         output.put("]>");
382         mixin(ifAnyCompiles(expand!"afterNode"));
383     }
384 
385     void writeDeclaration(string decl, string content) {
386         //assert(insideDTD);
387 
388         mixin(ifAnyCompiles(expand!"beforeNode"));
389         output.put("<!");
390         output.put(decl);
391         output.put(content);
392         output.put(">");
393         mixin(ifAnyCompiles(expand!"afterNode"));
394     }
395 
396     void write(Document doc) {
397         debug(HUNT_DEBUG_MORE) {
398             tracef("name: %s, text: %s, type: %s", doc.getName(), 
399                 doc.getText(), doc.getType());
400         }
401 
402         for (Element child = doc.firstNode(); child; child = child.nextSibling()) { 
403             debug(HUNT_DEBUG_MORE) {
404                 infof("name: %s, value: %s, type: %s", child.getName(),
405                         child.getText(), child.getType());
406             }
407             writeNode(child);
408         }
409 
410     }
411 
412     private void writeNode(Element node) {
413         // Print proper node type
414         switch (node.getType()) {
415             case NodeType.Document:
416                 writeChildren(node);
417                 break;
418 
419             case NodeType.Element:
420                 writeElement(node);
421                 break;
422 
423             case NodeType.Text:
424                 writeText(node.getText());
425                 break;
426 
427             case NodeType.Declaration:
428                 writeDeclaration(node);
429                 break;
430 
431             case NodeType.CDATA:
432                 writeCDATA(node.getText());
433                 break;
434 
435             default:
436                 warningf("Unhandled node, name: %s, type: %s", node.getName(), node.getType());
437                 break;
438         }
439     }
440     
441     /**
442      * <p>
443      * This will write the declaration to the given Writer. Assumes XML version
444      * 1.0 since we don't directly know.
445      * </p>
446      */
447     protected void writeDeclaration(Element node) {
448         // Only print of declaration is not suppressed
449         if (prettyPrinter.isSuppressDeclaration) 
450             return;
451         
452         output.put("<?xml");
453         for (Attribute attribute = node.firstAttribute(); attribute !is null; attribute = attribute.nextAttribute()) {
454             string value = attribute.getValue();
455             mixin(ifAnyCompiles(expand!"beforeAttributeName"));
456             output.put(attribute.getName());
457             mixin(ifAnyCompiles(expand!"afterAttributeName"));
458             output.put("=");
459             mixin(ifAnyCompiles(expand!"beforeAttributeValue"));
460             mixin(ifAnyCompiles(formatAttribute!"value"));            
461         }        
462         
463         mixin(ifAnyCompiles(expand!"beforePIEnd"));
464         output.put("?>");
465         mixin(ifAnyCompiles(expand!"afterNode"));
466     }
467 
468     /** 
469      * Print children of the node
470      * 
471      * Params:
472      *   node = 
473      */
474     private void writeChildren(Element node) {
475         debug(HUNT_DEBUG_MORE) tracef("type: %s, name: %s", node.getType(), node.getName());
476         for (Element child = node.firstNode(); child; child = child.nextSibling()) {
477             writeNode(child);
478         }
479     }
480 
481     private void writeAttributes(Element element) {
482         for (Attribute attribute = element.firstAttribute(); attribute !is null; attribute = attribute.nextAttribute()) {
483             writeAttribute(attribute.getName(), attribute.getValue());
484         }
485     }
486 
487     private void writeElement(Element element) {
488         Element child = element.firstNode();
489         debug(HUNT_DEBUG_MORE) {
490             tracef("type: %s, name: %s, text: %s", element.getType(), element.getName(), element.getText());
491         }
492         
493         startElement(element.getQualifiedName());
494         writeAttributes(element);
495 
496         if(child is null) {
497             closeElement(element.getQualifiedName());
498         } else if(child.getType() == NodeType.Text) {
499             debug(HUNT_DEBUG_MORE) {
500                 if(child !is null) {
501                     infof("type: %s, value %s", child.getType(), child.getText());
502                 }
503             }
504             writeText(child.getText());
505             closeElementWithTextNode(element.getQualifiedName());
506         } else {
507             debug(HUNT_DEBUG_MORE) infof("type: %s, name %s", child.getType(), child.getName());
508             writeChildren(element);
509             closeElement(element.getQualifiedName());
510         }
511     }
512 
513 }