1 /** 2 * An XML writer written for speed 3 * 4 * License: 5 * This Source Code Form is subject to the terms of 6 * the Mozilla Public License, v. 2.0. If a copy of 7 * the MPL was not distributed with this file, You 8 * can obtain one at http://mozilla.org/MPL/2.0/. 9 * 10 * Authors: 11 * Vladimir Panteleev <vladimir@thecybershadow.net> 12 */ 13 14 module ae.utils.xmlwriter; 15 16 import ae.utils.textout; 17 18 struct CustomXmlWriter(WRITER, bool PRETTY) 19 { 20 /// You can set this to something to e.g. write to another buffer. 21 WRITER output; 22 23 static if (PRETTY) 24 { 25 uint indentLevel = 0; 26 27 void newLine() 28 { 29 output.put('\n'); 30 } 31 32 void startLine() 33 { 34 output.allocate(indentLevel)[] = '\t'; 35 } 36 37 void indent () { indentLevel++; } 38 void outdent() { assert(indentLevel); indentLevel--; } 39 } 40 41 debug // verify well-formedness 42 { 43 string[] tagStack; 44 void pushTag(string tag) { tagStack ~= tag; } 45 void popTag () 46 { 47 assert(tagStack.length, "No tag to close"); 48 tagStack = tagStack[0..$-1]; 49 } 50 void popTag (string tag) 51 { 52 assert(tagStack.length, "No tag to close"); 53 assert(tagStack[$-1] == tag, "Closing wrong tag (" ~ tag ~ " instead of " ~ tagStack[$-1] ~ ")"); 54 tagStack = tagStack[0..$-1]; 55 } 56 57 bool inAttributes; 58 } 59 60 void startDocument() 61 { 62 output.put(`<?xml version="1.0" encoding="UTF-8"?>`); 63 static if (PRETTY) newLine(); 64 debug assert(tagStack.length==0); 65 } 66 67 deprecated alias text putText; 68 69 void text(in char[] s) 70 { 71 // https://gist.github.com/2192846 72 73 auto start = s.ptr, p = start, end = start+s.length; 74 75 while (p < end) 76 { 77 auto c = *p++; 78 if (Escapes.escaped[c]) 79 output.put(start[0..p-start-1], Escapes.chars[c]), 80 start = p; 81 } 82 83 output.put(start[0..p-start]); 84 } 85 86 // Common 87 88 private enum mixStartWithAttributesGeneric = 89 q{ 90 debug assert(!inAttributes, "Tag attributes not ended"); 91 static if (PRETTY) startLine(); 92 93 static if (STATIC) 94 output.put(OPEN ~ name); 95 else 96 output.put(OPEN, name); 97 98 debug inAttributes = true; 99 debug pushTag(name); 100 }; 101 102 private enum mixEndAttributesAndTagGeneric = 103 q{ 104 debug assert(inAttributes, "Tag attributes not started"); 105 output.put(CLOSE); 106 static if (PRETTY) newLine(); 107 debug inAttributes = false; 108 debug popTag(); 109 }; 110 111 // startTag 112 113 private enum mixStartTag = 114 q{ 115 debug assert(!inAttributes, "Tag attributes not ended"); 116 static if (PRETTY) startLine(); 117 118 static if (STATIC) 119 output.put('<' ~ name ~ '>'); 120 else 121 output.put('<', name, '>'); 122 123 static if (PRETTY) { newLine(); indent(); } 124 debug pushTag(name); 125 }; 126 127 void startTag(string name)() { enum STATIC = true; mixin(mixStartTag); } 128 void startTag()(string name) { enum STATIC = false; mixin(mixStartTag); } 129 130 // startTagWithAttributes 131 132 void startTagWithAttributes(string name)() { enum STATIC = true; enum OPEN = '<'; mixin(mixStartWithAttributesGeneric); } 133 void startTagWithAttributes()(string name) { enum STATIC = false; enum OPEN = '<'; mixin(mixStartWithAttributesGeneric); } 134 135 // addAttribute 136 137 private enum mixAddAttribute = 138 q{ 139 debug assert(inAttributes, "Tag attributes not started"); 140 141 static if (STATIC) 142 output.put(' ' ~ name ~ `="`); 143 else 144 output.put(' ', name, `="`); 145 146 text(value); 147 output.put('"'); 148 }; 149 150 void addAttribute(string name)(string value) { enum STATIC = true; mixin(mixAddAttribute); } 151 void addAttribute()(string name, string value) { enum STATIC = false; mixin(mixAddAttribute); } 152 153 // endAttributes[AndTag] 154 155 void endAttributes() 156 { 157 debug assert(inAttributes, "Tag attributes not started"); 158 output.put('>'); 159 static if (PRETTY) { newLine(); indent(); } 160 debug inAttributes = false; 161 } 162 163 void endAttributesAndTag() { enum CLOSE = " />"; mixin(mixEndAttributesAndTagGeneric); } 164 165 // endTag 166 167 private enum mixEndTag = 168 q{ 169 debug assert(!inAttributes, "Tag attributes not ended"); 170 static if (PRETTY) { outdent(); startLine(); } 171 172 static if (STATIC) 173 output.put("</" ~ name ~ ">"); 174 else 175 output.put("</", name, ">"); 176 177 static if (PRETTY) newLine(); 178 debug popTag(name); 179 }; 180 181 void endTag(string name)() { enum STATIC = true; mixin(mixEndTag); } 182 void endTag()(string name) { enum STATIC = false; mixin(mixEndTag); } 183 184 // Processing instructions 185 186 void startPI(string name)() { enum STATIC = true; enum OPEN = "<?"; mixin(mixStartWithAttributesGeneric); } 187 void startPI()(string name) { enum STATIC = false; enum OPEN = "<?"; mixin(mixStartWithAttributesGeneric); } 188 void endPI() { enum CLOSE = "?>"; mixin(mixEndAttributesAndTagGeneric); } 189 190 // Doctypes 191 192 deprecated alias doctype putDoctype; 193 194 void doctype(string text) 195 { 196 debug assert(!inAttributes, "Tag attributes not ended"); 197 output.put("<!", text, ">"); 198 static if (PRETTY) newLine(); 199 } 200 } 201 202 alias CustomXmlWriter!(StringBuilder, false) XmlWriter; 203 alias CustomXmlWriter!(StringBuilder, true ) PrettyXmlWriter; 204 205 private: 206 207 private struct Escapes 208 { 209 static __gshared string[256] chars; 210 static __gshared bool[256] escaped; 211 212 shared static this() 213 { 214 import std.string; 215 216 escaped[] = true; 217 foreach (c; 0..256) 218 if (c=='<') 219 chars[c] = "<"; 220 else 221 if (c=='>') 222 chars[c] = ">"; 223 else 224 if (c=='&') 225 chars[c] = "&"; 226 else 227 if (c=='"') 228 chars[c] = """; 229 else 230 if (c < 0x20 && c != 0x0D && c != 0x0A) 231 chars[c] = format("&#x%02X;", c); 232 else 233 chars[c] = [cast(char)c], 234 escaped[c] = false; 235 } 236 } 237 238 unittest 239 { 240 string[string] quotes; 241 quotes["Alan Perlis"] = "When someone says, \"I want a programming language in which I need only say what I want done,\" give him a lollipop."; 242 243 XmlWriter xml; 244 xml.startDocument(); 245 xml.startTag!"quotes"(); 246 foreach (author, text; quotes) 247 { 248 xml.startTagWithAttributes!"quote"(); 249 xml.addAttribute!"author"(author); 250 xml.endAttributes(); 251 xml.text(text); 252 xml.endTag!"quote"(); 253 } 254 xml.endTag!"quotes"(); 255 256 auto str = xml.output.get(); 257 assert(str == 258 `<?xml version="1.0" encoding="UTF-8"?>` 259 `<quotes>` 260 `<quote author="Alan Perlis">` 261 `When someone says, "I want a programming language in which I need only say what I want done," give him a lollipop.` 262 `</quote>` 263 `</quotes>`); 264 } 265 266 // TODO: StringBuilder-compatible XML-encoding string sink/filter? 267 // e.g. to allow putTime to write directly to an XML node contents