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