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 }+/