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 	void comment(string text)
234 	{
235 		debug assert(!inAttributes, "Tag attributes not ended");
236 		output.put("<!--", text, "-->");
237 		newLine();
238 	}
239 }
240 
241 deprecated template CustomXmlWriter(Writer, bool pretty)
242 {
243 	static if (pretty)
244 		alias CustomXmlWriter = CustomXmlWriter!(Writer, DefaultXmlFormatter);
245 	else
246 		alias CustomXmlWriter = CustomXmlWriter!(Writer, NullXmlFormatter);
247 }
248 
249 alias CustomXmlWriter!(StringBuilder, NullXmlFormatter   ) XmlWriter;
250 alias CustomXmlWriter!(StringBuilder, DefaultXmlFormatter) PrettyXmlWriter;
251 
252 private:
253 
254 private struct Escapes
255 {
256 	static __gshared string[256] chars;
257 	static __gshared bool[256] escaped;
258 
259 	shared static this()
260 	{
261 		import std.string;
262 
263 		escaped[] = true;
264 		foreach (c; 0..256)
265 			if (c=='<')
266 				chars[c] = "&lt;";
267 			else
268 			if (c=='>')
269 				chars[c] = "&gt;";
270 			else
271 			if (c=='&')
272 				chars[c] = "&amp;";
273 			else
274 			if (c=='"')
275 				chars[c] = "&quot;";
276 			else
277 			if (c < 0x20 && c != 0x0D && c != 0x0A && c != 0x09)
278 				chars[c] = format("&#x%02X;", c);
279 			else
280 				chars[c] = [cast(char)c],
281 				escaped[c] = false;
282 	}
283 }
284 
285 unittest
286 {
287 	string[string] quotes;
288 	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.";
289 
290 	XmlWriter xml;
291 	xml.startDocument();
292 	xml.startTag!"quotes"();
293 	foreach (author, text; quotes)
294 	{
295 		xml.startTagWithAttributes!"quote"();
296 		xml.addAttribute!"author"(author);
297 		xml.endAttributes();
298 		xml.text(text);
299 		xml.endTag!"quote"();
300 	}
301 	xml.endTag!"quotes"();
302 
303 	auto str = xml.output.get();
304 	assert(str ==
305 		`<?xml version="1.0" encoding="UTF-8"?>` ~
306 		`<quotes>` ~
307 			`<quote author="Alan Perlis">` ~
308 				`When someone says, &quot;I want a programming language in which I need only say what I want done,&quot; give him a lollipop.` ~
309 			`</quote>` ~
310 		`</quotes>`);
311 }
312 
313 // TODO: StringBuilder-compatible XML-encoding string sink/filter?
314 // e.g. to allow putTime to write directly to an XML node contents