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 CustomXmlWriter(WRITER, bool PRETTY)
19 {
20 	/// You can set this to something to e.g. write to another buffer.
21 	WRITER output;
22 
23 	static if (PRETTY)
24 	{
25 		uint indentLevel = 0;
26 
27 		void newLine()
28 		{
29 			output.put('\n');
30 		}
31 
32 		void startLine()
33 		{
34 			output.allocate(indentLevel)[] = '\t';
35 		}
36 
37 		void indent () {                      indentLevel++; }
38 		void outdent() { assert(indentLevel); indentLevel--; }
39 	}
40 
41 	debug // verify well-formedness
42 	{
43 		string[] tagStack;
44 		void pushTag(string tag) { tagStack ~= tag; }
45 		void popTag ()
46 		{
47 			assert(tagStack.length, "No tag to close");
48 			tagStack = tagStack[0..$-1];
49 		}
50 		void popTag (string tag)
51 		{
52 			assert(tagStack.length, "No tag to close");
53 			assert(tagStack[$-1] == tag, "Closing wrong tag (" ~ tag ~ " instead of " ~ tagStack[$-1] ~ ")");
54 			tagStack = tagStack[0..$-1];
55 		}
56 
57 		bool inAttributes;
58 	}
59 
60 	void startDocument()
61 	{
62 		output.put(`<?xml version="1.0" encoding="UTF-8"?>`);
63 		static if (PRETTY) newLine();
64 		debug assert(tagStack.length==0);
65 	}
66 
67 	deprecated alias text putText;
68 
69 	void text(in char[] s)
70 	{
71 		// https://gist.github.com/2192846
72 
73 		auto start = s.ptr, p = start, end = start+s.length;
74 
75 		while (p < end)
76 		{
77 			auto c = *p++;
78 			if (Escapes.escaped[c])
79 				output.put(start[0..p-start-1], Escapes.chars[c]),
80 				start = p;
81 		}
82 
83 		output.put(start[0..p-start]);
84 	}
85 
86 	// Common
87 
88 	private enum mixStartWithAttributesGeneric =
89 	q{
90 		debug assert(!inAttributes, "Tag attributes not ended");
91 		static if (PRETTY) startLine();
92 
93 		static if (STATIC)
94 			output.put(OPEN ~ name);
95 		else
96 			output.put(OPEN, name);
97 
98 		debug inAttributes = true;
99 		debug pushTag(name);
100 	};
101 
102 	private enum mixEndAttributesAndTagGeneric =
103 	q{
104 		debug assert(inAttributes, "Tag attributes not started");
105 		output.put(CLOSE);
106 		static if (PRETTY) newLine();
107 		debug inAttributes = false;
108 		debug popTag();
109 	};
110 
111 	// startTag
112 
113 	private enum mixStartTag =
114 	q{
115 		debug assert(!inAttributes, "Tag attributes not ended");
116 		static if (PRETTY) startLine();
117 
118 		static if (STATIC)
119 			output.put('<' ~ name ~ '>');
120 		else
121 			output.put('<', name, '>');
122 
123 		static if (PRETTY) { newLine(); indent(); }
124 		debug pushTag(name);
125 	};
126 
127 	void startTag(string name)() { enum STATIC = true;  mixin(mixStartTag); }
128 	void startTag()(string name) { enum STATIC = false; mixin(mixStartTag); }
129 
130 	// startTagWithAttributes
131 
132 	void startTagWithAttributes(string name)() { enum STATIC = true;  enum OPEN = '<'; mixin(mixStartWithAttributesGeneric); }
133 	void startTagWithAttributes()(string name) { enum STATIC = false; enum OPEN = '<'; mixin(mixStartWithAttributesGeneric); }
134 
135 	// addAttribute
136 
137 	private enum mixAddAttribute =
138 	q{
139 		debug assert(inAttributes, "Tag attributes not started");
140 
141 		static if (STATIC)
142 			output.put(' ' ~ name ~ `="`);
143 		else
144 			output.put(' ', name, `="`);
145 
146 		text(value);
147 		output.put('"');
148 	};
149 
150 	void addAttribute(string name)(string value)   { enum STATIC = true;  mixin(mixAddAttribute); }
151 	void addAttribute()(string name, string value) { enum STATIC = false; mixin(mixAddAttribute); }
152 
153 	// endAttributes[AndTag]
154 
155 	void endAttributes()
156 	{
157 		debug assert(inAttributes, "Tag attributes not started");
158 		output.put('>');
159 		static if (PRETTY) { newLine(); indent(); }
160 		debug inAttributes = false;
161 	}
162 
163 	void endAttributesAndTag() { enum CLOSE = " />"; mixin(mixEndAttributesAndTagGeneric); }
164 
165 	// endTag
166 
167 	private enum mixEndTag =
168 	q{
169 		debug assert(!inAttributes, "Tag attributes not ended");
170 		static if (PRETTY) { outdent(); startLine(); }
171 
172 		static if (STATIC)
173 			output.put("</" ~ name ~ ">");
174 		else
175 			output.put("</", name, ">");
176 
177 		static if (PRETTY) newLine();
178 		debug popTag(name);
179 	};
180 
181 	void endTag(string name)() { enum STATIC = true;  mixin(mixEndTag); }
182 	void endTag()(string name) { enum STATIC = false; mixin(mixEndTag); }
183 
184 	// Processing instructions
185 
186 	void startPI(string name)() { enum STATIC = true;  enum OPEN = "<?"; mixin(mixStartWithAttributesGeneric); }
187 	void startPI()(string name) { enum STATIC = false; enum OPEN = "<?"; mixin(mixStartWithAttributesGeneric); }
188 	void endPI() { enum CLOSE = "?>"; mixin(mixEndAttributesAndTagGeneric); }
189 
190 	// Doctypes
191 
192 	deprecated alias doctype putDoctype;
193 
194 	void doctype(string text)
195 	{
196 		debug assert(!inAttributes, "Tag attributes not ended");
197 		output.put("<!", text, ">");
198 		static if (PRETTY) newLine();
199 	}
200 }
201 
202 alias CustomXmlWriter!(StringBuilder, false) XmlWriter;
203 alias CustomXmlWriter!(StringBuilder, true ) PrettyXmlWriter;
204 
205 private:
206 
207 private struct Escapes
208 {
209 	static __gshared string[256] chars;
210 	static __gshared bool[256] escaped;
211 
212 	shared static this()
213 	{
214 		import std.string;
215 
216 		escaped[] = true;
217 		foreach (c; 0..256)
218 			if (c=='<')
219 				chars[c] = "&lt;";
220 			else
221 			if (c=='>')
222 				chars[c] = "&gt;";
223 			else
224 			if (c=='&')
225 				chars[c] = "&amp;";
226 			else
227 			if (c=='"')
228 				chars[c] = "&quot;";
229 			else
230 			if (c < 0x20 && c != 0x0D && c != 0x0A)
231 				chars[c] = format("&#x%02X;", c);
232 			else
233 				chars[c] = [cast(char)c],
234 				escaped[c] = false;
235 	}
236 }
237 
238 unittest
239 {
240 	string[string] quotes;
241 	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.";
242 
243 	XmlWriter xml;
244 	xml.startDocument();
245 	xml.startTag!"quotes"();
246 	foreach (author, text; quotes)
247 	{
248 		xml.startTagWithAttributes!"quote"();
249 		xml.addAttribute!"author"(author);
250 		xml.endAttributes();
251 		xml.text(text);
252 		xml.endTag!"quote"();
253 	}
254 	xml.endTag!"quotes"();
255 
256 	auto str = xml.output.get();
257 	assert(str ==
258 		`<?xml version="1.0" encoding="UTF-8"?>`
259 		`<quotes>`
260 			`<quote author="Alan Perlis">`
261 				`When someone says, &quot;I want a programming language in which I need only say what I want done,&quot; give him a lollipop.`
262 			`</quote>`
263 		`</quotes>`);
264 }
265 
266 // TODO: StringBuilder-compatible XML-encoding string sink/filter?
267 // e.g. to allow putTime to write directly to an XML node contents