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 NullXmlFormatter 19 { 20 @property bool enabled() { return false; } 21 @property void enabled(bool value) {} 22 23 mixin template Mixin(alias formatter) 24 { 25 void newLine() {} 26 void startLine() {} 27 void indent() {} 28 void outdent() {} 29 } 30 } 31 32 struct CustomXmlFormatter(char indentCharP, uint indentSizeP) 33 { 34 enum indentChar = indentCharP; 35 enum indentSize = indentSizeP; 36 37 bool enabled = true; 38 39 mixin template Mixin(alias formatter) 40 { 41 uint indentLevel = 0; 42 43 void newLine() 44 { 45 if (formatter.enabled) 46 output.put('\n'); 47 } 48 49 void startLine() 50 { 51 if (formatter.enabled) 52 output.allocate(indentLevel * formatter.indentSize)[] = formatter.indentChar; 53 } 54 55 void indent () { indentLevel++; } 56 void outdent() { assert(indentLevel); indentLevel--; } 57 } 58 } 59 60 alias DefaultXmlFormatter = CustomXmlFormatter!('\t', 1); 61 62 struct CustomXmlWriter(WRITER, Formatter) 63 { 64 /// You can set this to something to e.g. write to another buffer. 65 WRITER output; 66 67 Formatter formatter; 68 mixin Formatter.Mixin!formatter; 69 70 debug // verify well-formedness 71 { 72 string[] tagStack; 73 void pushTag(string tag) { tagStack ~= tag; } 74 void popTag () 75 { 76 assert(tagStack.length, "No tag to close"); 77 tagStack = tagStack[0..$-1]; 78 } 79 void popTag (string tag) 80 { 81 assert(tagStack.length, "No tag to close"); 82 assert(tagStack[$-1] == tag, "Closing wrong tag (" ~ tag ~ " instead of " ~ tagStack[$-1] ~ ")"); 83 tagStack = tagStack[0..$-1]; 84 } 85 86 bool inAttributes; 87 } 88 89 void startDocument() 90 { 91 output.put(`<?xml version="1.0" encoding="UTF-8"?>`); 92 newLine(); 93 debug assert(tagStack.length==0); 94 } 95 96 deprecated alias text putText; 97 98 void text(in char[] s) 99 { 100 escapedText!(EscapeScope.text)(s); 101 } 102 103 alias attrText = escapedText!(EscapeScope.attribute); 104 105 private void escapedText(EscapeScope escapeScope)(in char[] s) 106 { 107 // https://gist.github.com/2192846 108 109 auto start = s.ptr, p = start, end = start+s.length; 110 111 alias E = Escapes!escapeScope; 112 113 while (p < end) 114 { 115 auto c = *p++; 116 if (E.escaped[c]) 117 output.put(start[0..p-start-1], E.chars[c]), 118 start = p; 119 } 120 121 output.put(start[0..p-start]); 122 } 123 124 // Common 125 126 private enum mixStartWithAttributesGeneric = 127 q{ 128 debug assert(!inAttributes, "Tag attributes not ended"); 129 startLine(); 130 131 static if (STATIC) 132 output.put(OPEN ~ name); 133 else 134 output.put(OPEN, name); 135 136 debug inAttributes = true; 137 debug pushTag(name); 138 }; 139 140 private enum mixEndAttributesAndTagGeneric = 141 q{ 142 debug assert(inAttributes, "Tag attributes not started"); 143 output.put(CLOSE); 144 newLine(); 145 debug inAttributes = false; 146 debug popTag(); 147 }; 148 149 // startTag 150 151 private enum mixStartTag = 152 q{ 153 debug assert(!inAttributes, "Tag attributes not ended"); 154 startLine(); 155 156 static if (STATIC) 157 output.put('<' ~ name ~ '>'); 158 else 159 output.put('<', name, '>'); 160 161 newLine(); 162 indent(); 163 debug pushTag(name); 164 }; 165 166 void startTag(string name)() { enum STATIC = true; mixin(mixStartTag); } 167 void startTag()(string name) { enum STATIC = false; mixin(mixStartTag); } 168 169 // startTagWithAttributes 170 171 void startTagWithAttributes(string name)() { enum STATIC = true; enum OPEN = '<'; mixin(mixStartWithAttributesGeneric); } 172 void startTagWithAttributes()(string name) { enum STATIC = false; enum OPEN = '<'; mixin(mixStartWithAttributesGeneric); } 173 174 // addAttribute 175 176 private enum mixAddAttribute = 177 q{ 178 debug assert(inAttributes, "Tag attributes not started"); 179 180 static if (STATIC) 181 output.put(' ' ~ name ~ `="`); 182 else 183 output.put(' ', name, `="`); 184 185 attrText(value); 186 output.put('"'); 187 }; 188 189 void addAttribute(string name)(string value) { enum STATIC = true; mixin(mixAddAttribute); } 190 void addAttribute()(string name, string value) { enum STATIC = false; mixin(mixAddAttribute); } 191 192 // endAttributes[AndTag] 193 194 void endAttributes() 195 { 196 debug assert(inAttributes, "Tag attributes not started"); 197 output.put('>'); 198 newLine(); 199 indent(); 200 debug inAttributes = false; 201 } 202 203 void endAttributesAndTag() { enum CLOSE = "/>"; mixin(mixEndAttributesAndTagGeneric); } 204 205 // endTag 206 207 private enum mixEndTag = 208 q{ 209 debug assert(!inAttributes, "Tag attributes not ended"); 210 outdent(); 211 startLine(); 212 213 static if (STATIC) 214 output.put("</" ~ name ~ ">"); 215 else 216 output.put("</", name, ">"); 217 218 newLine(); 219 debug popTag(name); 220 }; 221 222 void endTag(string name)() { enum STATIC = true; mixin(mixEndTag); } 223 void endTag()(string name) { enum STATIC = false; mixin(mixEndTag); } 224 225 // Processing instructions 226 227 void startPI(string name)() { enum STATIC = true; enum OPEN = "<?"; mixin(mixStartWithAttributesGeneric); } 228 void startPI()(string name) { enum STATIC = false; enum OPEN = "<?"; mixin(mixStartWithAttributesGeneric); } 229 void endPI() { enum CLOSE = "?>"; mixin(mixEndAttributesAndTagGeneric); } 230 231 // Doctypes 232 233 deprecated alias doctype putDoctype; 234 235 void doctype(string text) 236 { 237 debug assert(!inAttributes, "Tag attributes not ended"); 238 output.put("<!", text, ">"); 239 newLine(); 240 } 241 242 void comment(string text) 243 { 244 debug assert(!inAttributes, "Tag attributes not ended"); 245 output.put("<!--", text, "-->"); 246 newLine(); 247 } 248 } 249 250 deprecated template CustomXmlWriter(Writer, bool pretty) 251 { 252 static if (pretty) 253 alias CustomXmlWriter = CustomXmlWriter!(Writer, DefaultXmlFormatter); 254 else 255 alias CustomXmlWriter = CustomXmlWriter!(Writer, NullXmlFormatter); 256 } 257 258 alias CustomXmlWriter!(StringBuilder, NullXmlFormatter ) XmlWriter; 259 alias CustomXmlWriter!(StringBuilder, DefaultXmlFormatter) PrettyXmlWriter; 260 261 private: 262 263 enum EscapeScope 264 { 265 text, 266 attribute, 267 } 268 269 private struct Escapes(EscapeScope escapeScope) 270 { 271 static __gshared string[256] chars; 272 static __gshared bool[256] escaped; 273 274 shared static this() 275 { 276 import std.string; 277 278 escaped[] = true; 279 foreach (c; 0..256) 280 if (c=='<') 281 chars[c] = "<"; 282 else 283 if (c=='>') 284 chars[c] = ">"; 285 else 286 if (c=='&') 287 chars[c] = "&"; 288 else 289 if (escapeScope == EscapeScope.attribute && 290 c=='"') 291 chars[c] = """; 292 else 293 if (c < 0x20 && c != 0x0D && c != 0x0A && c != 0x09) 294 chars[c] = format("&#x%02X;", c); 295 else 296 chars[c] = [cast(char)c], 297 escaped[c] = false; 298 } 299 } 300 301 unittest 302 { 303 string[string] quotes; 304 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."; 305 306 XmlWriter xml; 307 xml.startDocument(); 308 xml.startTag!"quotes"(); 309 foreach (author, text; quotes) 310 { 311 xml.startTagWithAttributes!"quote"(); 312 xml.addAttribute!"author"(author); 313 xml.endAttributes(); 314 xml.text(text); 315 xml.endTag!"quote"(); 316 } 317 xml.endTag!"quotes"(); 318 319 auto str = xml.output.get(); 320 assert(str == 321 `<?xml version="1.0" encoding="UTF-8"?>` ~ 322 `<quotes>` ~ 323 `<quote author="Alan Perlis">` ~ 324 `When someone says, "I want a programming language in which I need only say what I want done," give him a lollipop.` ~ 325 `</quote>` ~ 326 `</quotes>`); 327 } 328 329 // TODO: StringBuilder-compatible XML-encoding string sink/filter? 330 // e.g. to allow putTime to write directly to an XML node contents