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 // https://gist.github.com/2192846 101 102 auto start = s.ptr, p = start, end = start+s.length; 103 104 while (p < end) 105 { 106 auto c = *p++; 107 if (Escapes.escaped[c]) 108 output.put(start[0..p-start-1], Escapes.chars[c]), 109 start = p; 110 } 111 112 output.put(start[0..p-start]); 113 } 114 115 // Common 116 117 private enum mixStartWithAttributesGeneric = 118 q{ 119 debug assert(!inAttributes, "Tag attributes not ended"); 120 startLine(); 121 122 static if (STATIC) 123 output.put(OPEN ~ name); 124 else 125 output.put(OPEN, name); 126 127 debug inAttributes = true; 128 debug pushTag(name); 129 }; 130 131 private enum mixEndAttributesAndTagGeneric = 132 q{ 133 debug assert(inAttributes, "Tag attributes not started"); 134 output.put(CLOSE); 135 newLine(); 136 debug inAttributes = false; 137 debug popTag(); 138 }; 139 140 // startTag 141 142 private enum mixStartTag = 143 q{ 144 debug assert(!inAttributes, "Tag attributes not ended"); 145 startLine(); 146 147 static if (STATIC) 148 output.put('<' ~ name ~ '>'); 149 else 150 output.put('<', name, '>'); 151 152 newLine(); 153 indent(); 154 debug pushTag(name); 155 }; 156 157 void startTag(string name)() { enum STATIC = true; mixin(mixStartTag); } 158 void startTag()(string name) { enum STATIC = false; mixin(mixStartTag); } 159 160 // startTagWithAttributes 161 162 void startTagWithAttributes(string name)() { enum STATIC = true; enum OPEN = '<'; mixin(mixStartWithAttributesGeneric); } 163 void startTagWithAttributes()(string name) { enum STATIC = false; enum OPEN = '<'; mixin(mixStartWithAttributesGeneric); } 164 165 // addAttribute 166 167 private enum mixAddAttribute = 168 q{ 169 debug assert(inAttributes, "Tag attributes not started"); 170 171 static if (STATIC) 172 output.put(' ' ~ name ~ `="`); 173 else 174 output.put(' ', name, `="`); 175 176 text(value); 177 output.put('"'); 178 }; 179 180 void addAttribute(string name)(string value) { enum STATIC = true; mixin(mixAddAttribute); } 181 void addAttribute()(string name, string value) { enum STATIC = false; mixin(mixAddAttribute); } 182 183 // endAttributes[AndTag] 184 185 void endAttributes() 186 { 187 debug assert(inAttributes, "Tag attributes not started"); 188 output.put('>'); 189 newLine(); 190 indent(); 191 debug inAttributes = false; 192 } 193 194 void endAttributesAndTag() { enum CLOSE = " />"; mixin(mixEndAttributesAndTagGeneric); } 195 196 // endTag 197 198 private enum mixEndTag = 199 q{ 200 debug assert(!inAttributes, "Tag attributes not ended"); 201 outdent(); 202 startLine(); 203 204 static if (STATIC) 205 output.put("</" ~ name ~ ">"); 206 else 207 output.put("</", name, ">"); 208 209 newLine(); 210 debug popTag(name); 211 }; 212 213 void endTag(string name)() { enum STATIC = true; mixin(mixEndTag); } 214 void endTag()(string name) { enum STATIC = false; mixin(mixEndTag); } 215 216 // Processing instructions 217 218 void startPI(string name)() { enum STATIC = true; enum OPEN = "<?"; mixin(mixStartWithAttributesGeneric); } 219 void startPI()(string name) { enum STATIC = false; enum OPEN = "<?"; mixin(mixStartWithAttributesGeneric); } 220 void endPI() { enum CLOSE = "?>"; mixin(mixEndAttributesAndTagGeneric); } 221 222 // Doctypes 223 224 deprecated alias doctype putDoctype; 225 226 void doctype(string text) 227 { 228 debug assert(!inAttributes, "Tag attributes not ended"); 229 output.put("<!", text, ">"); 230 newLine(); 231 } 232 } 233 234 deprecated template CustomXmlWriter(Writer, bool pretty) 235 { 236 static if (pretty) 237 alias CustomXmlWriter = CustomXmlWriter!(Writer, DefaultXmlFormatter); 238 else 239 alias CustomXmlWriter = CustomXmlWriter!(Writer, NullXmlFormatter); 240 } 241 242 alias CustomXmlWriter!(StringBuilder, NullXmlFormatter ) XmlWriter; 243 alias CustomXmlWriter!(StringBuilder, DefaultXmlFormatter) PrettyXmlWriter; 244 245 private: 246 247 private struct Escapes 248 { 249 static __gshared string[256] chars; 250 static __gshared bool[256] escaped; 251 252 shared static this() 253 { 254 import std.string; 255 256 escaped[] = true; 257 foreach (c; 0..256) 258 if (c=='<') 259 chars[c] = "<"; 260 else 261 if (c=='>') 262 chars[c] = ">"; 263 else 264 if (c=='&') 265 chars[c] = "&"; 266 else 267 if (c=='"') 268 chars[c] = """; 269 else 270 if (c < 0x20 && c != 0x0D && c != 0x0A && c != 0x09) 271 chars[c] = format("&#x%02X;", c); 272 else 273 chars[c] = [cast(char)c], 274 escaped[c] = false; 275 } 276 } 277 278 unittest 279 { 280 string[string] quotes; 281 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."; 282 283 XmlWriter xml; 284 xml.startDocument(); 285 xml.startTag!"quotes"(); 286 foreach (author, text; quotes) 287 { 288 xml.startTagWithAttributes!"quote"(); 289 xml.addAttribute!"author"(author); 290 xml.endAttributes(); 291 xml.text(text); 292 xml.endTag!"quote"(); 293 } 294 xml.endTag!"quotes"(); 295 296 auto str = xml.output.get(); 297 assert(str == 298 `<?xml version="1.0" encoding="UTF-8"?>` ~ 299 `<quotes>` ~ 300 `<quote author="Alan Perlis">` ~ 301 `When someone says, "I want a programming language in which I need only say what I want done," give him a lollipop.` ~ 302 `</quote>` ~ 303 `</quotes>`); 304 } 305 306 // TODO: StringBuilder-compatible XML-encoding string sink/filter? 307 // e.g. to allow putTime to write directly to an XML node contents