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 }