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] = "&lt;";
260 			else
261 			if (c=='>')
262 				chars[c] = "&gt;";
263 			else
264 			if (c=='&')
265 				chars[c] = "&amp;";
266 			else
267 			if (c=='"')
268 				chars[c] = "&quot;";
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, &quot;I want a programming language in which I need only say what I want done,&quot; 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