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] = "&lt;";
282 			else
283 			if (c=='>')
284 				chars[c] = "&gt;";
285 			else
286 			if (c=='&')
287 				chars[c] = "&amp;";
288 			else
289 			if (escapeScope == EscapeScope.attribute &&
290 				c=='"')
291 				chars[c] = "&quot;";
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