1 /** Functionality for converting DOM nodes to SDLang documents.
2 */
3 module sdlite.generator;
4 
5 import sdlite.ast;
6 
7 import core.time;
8 import std.datetime;
9 import std.range;
10 import taggedalgebraic.taggedunion : visit;
11 
12 
13 /** Writes out a range of `SDLNode`s to a `char` based output range.
14 */
15 void generateSDLang(R, NR)(ref R dst, NR nodes, size_t level = 0)
16 	if (isOutputRange!(R, char) && isInputRange!NR && is(ElementType!NR : const(SDLNode)))
17 {
18 	foreach (ref n; nodes)
19 		generateSDLang(dst, n, level);
20 }
21 
22 unittest {
23 	auto app = appender!string;
24 	app.generateSDLang([
25 		SDLNode("na"),
26 		SDLNode("nb", [SDLValue.int_(1), SDLValue.int_(2)]),
27 		SDLNode("nc", [SDLValue.int_(1)], [SDLAttribute("a", SDLValue.int_(2))]),
28 		SDLNode("nd", null, [SDLAttribute("a", SDLValue.int_(1)), SDLAttribute("b", SDLValue.int_(2))]),
29 		SDLNode("ne", null, null, [
30 			SDLNode("foo:nf", null, null, [
31 				SDLNode("ng")
32 			]),
33 		])
34 	]);
35 	assert(app.data ==
36 `na
37 nb 1 2
38 nc 1 a=2
39 nd a=1 b=2
40 ne {
41 	foo:nf {
42 		ng
43 	}
44 }
45 `, app.data);
46 }
47 
48 
49 /** Writes out single `SDLNode` to a `char` based output range.
50 */
51 void generateSDLang(R)(ref R dst, in auto ref SDLNode node, size_t level = 0)
52 {
53 	auto name = node.qualifiedName == "content" ? "" : node.qualifiedName;
54 	dst.putIndentation(level);
55 	dst.put(name);
56 	foreach (ref v; node.values) {
57 		dst.put(' ');
58 		dst.generateSDLang(v);
59 	}
60 	foreach (ref a; node.attributes) {
61 		dst.put(' ');
62 		dst.put(a.qualifiedName);
63 		dst.put('=');
64 		dst.generateSDLang(a.value);
65 	}
66 	if (node.children) {
67 		dst.put(" {\n");
68 		dst.generateSDLang(node.children, level + 1);
69 		dst.putIndentation(level);
70 		dst.put("}\n");
71 	} else dst.put('\n');
72 }
73 
74 
75 /** Writes a single SDLang value to the given output range.
76 */
77 void generateSDLang(R)(ref R dst, in auto ref SDLValue value)
78 {
79 	import std.format : formattedWrite;
80 
81 	// NOTE: using final switch instead of visit, because the latter causes
82 	//       the creation of a heap delegate
83 
84 	final switch (value.kind) {
85 		case SDLValue.Kind.null_:
86 			dst.put("null");
87 			break;
88 		case SDLValue.Kind.text:
89 			dst.put('"');
90 			dst.escapeSDLString(value.textValue);
91 			dst.put('"');
92 			break;
93 		case SDLValue.Kind.binary:
94 			dst.put('[');
95 			dst.generateBase64(value.binaryValue);
96 			dst.put(']');
97 			break;
98 		case SDLValue.Kind.int_:
99 			dst.formattedWrite("%s", value.intValue);
100 			break;
101 		case SDLValue.Kind.long_:
102 			dst.formattedWrite("%sL", value.longValue);
103 			break;
104 		case SDLValue.Kind.decimal:
105 			assert(false);
106 		case SDLValue.Kind.float_:
107 			dst.writeFloat(value.floatValue);
108 			dst.put('f');
109 			break;
110 		case SDLValue.Kind.double_:
111 			dst.writeFloat(value.doubleValue);
112 			break;
113 		case SDLValue.Kind.bool_:
114 			dst.put(value.boolValue ? "true" : "false");
115 			break;
116 		case SDLValue.Kind.dateTime:
117 			auto dt = cast(DateTime)value.dateTimeValue;
118 			auto fracsec = value.dateTimeValue.fracSecs;
119 			dst.formattedWrite("%d/%02d/%02d %02d:%02d:%02d",
120 				dt.year, dt.month, dt.day, dt.hour, dt.minute, dt.second);
121 			dst.writeFracSecs(fracsec.total!"hnsecs");
122 
123 			auto tz = value.dateTimeValue.timezone;
124 
125 			if (tz is LocalTime()) {}
126 			else if (tz is UTC()) dst.put("-UTC");
127 			else if (auto sz = cast(immutable(SimpleTimeZone))tz) {
128 				long hours, minutes;
129 				sz.utcOffset.split!("hours", "minutes")(hours, minutes);
130 				if (hours < 0 || minutes < 0)
131 					dst.formattedWrite("-GMT-%02d:%02d", -hours, -minutes); // NOTE: should really be UTC, but we are following the spec here
132 				else dst.formattedWrite("-GMT+%02d:%02d", hours, minutes);
133 			} else {
134 				auto offset = tz.utcOffsetAt(value.dateTimeValue.stdTime);
135 				long hours, minutes;
136 				offset.split!("hours", "minutes")(hours, minutes);
137 				if (hours < 0 || minutes < 0)
138 					dst.formattedWrite("-GMT-%02d:%02d", -hours, -minutes); // NOTE: should really be UTC, but we are following the spec here
139 				else dst.formattedWrite("-GMT+%02d:%02d", hours, minutes);
140 				//dst.formattedWrite("-%s", tz.stdName); // Q: should this be name instead (e.g. CEST vs. CET)
141 			}
142 			break;
143 		case SDLValue.Kind.date:
144 			auto d = value.dateValue;
145 			dst.formattedWrite("%d/%02d/%02d", d.year, d.month, d.day);
146 			break;
147 		case SDLValue.Kind.duration:
148 			long days, hours, minutes, seconds, hnsecs;
149 			value.durationValue.split!("days", "hours", "minutes", "seconds", "hnsecs")
150 				(days, hours, minutes, seconds, hnsecs);
151 			if (days > 0) dst.formattedWrite("%sd:", days);
152 			dst.formattedWrite("%02d:%02d", hours, minutes);
153 			if (seconds != 0 || hnsecs != 0) {
154 				dst.formattedWrite(":%02d", seconds);
155 				dst.writeFracSecs(hnsecs);
156 			}
157 			break;
158 	}
159 }
160 
161 unittest {
162 	import std.array : appender;
163 
164 	void test(SDLValue v, string exp)
165 	{
166 		auto app = appender!string;
167 		app.generateSDLang(v);
168 		assert(app.data == exp, app.data);
169 	}
170 
171 	test(SDLValue.null_, "null");
172 	test(SDLValue.bool_(false), "false");
173 	test(SDLValue.bool_(true), "true");
174 	test(SDLValue.text("foo\"bar"), `"foo\"bar"`);
175 	test(SDLValue.binary(cast(immutable(ubyte)[])"hello, world!"), "[aGVsbG8sIHdvcmxkIQ==]");
176 	test(SDLValue.int_(int.max), "2147483647");
177 	test(SDLValue.int_(int.min), "-2147483648");
178 	test(SDLValue.long_(long.max), "9223372036854775807L");
179 	test(SDLValue.long_(long.min), "-9223372036854775808L");
180 	test(SDLValue.float_(2.2f), "2.2f");
181 	test(SDLValue.double_(2.2), "2.2");
182 	test(SDLValue.double_(1.0), "1.0"); // make sure there is always a fractional part
183 	test(SDLValue.date(Date(2015, 12, 6)), "2015/12/06");
184 	test(SDLValue.duration(12.hours + 14.minutes + 34.seconds), "12:14:34");
185 	test(SDLValue.duration(12.hours + 14.minutes + 34.seconds + 123.msecs), "12:14:34.123");
186 	test(SDLValue.duration(2.days + 12.hours + 14.minutes + 34.seconds), "2d:12:14:34");
187 	test(SDLValue.dateTime(SysTime(DateTime(2015, 12, 6, 12, 0, 0))), "2015/12/06 12:00:00");
188 	test(SDLValue.dateTime(SysTime(DateTime(2015, 12, 6, 12, 0, 0), 123.msecs)), "2015/12/06 12:00:00.123");
189 	test(SDLValue.dateTime(SysTime(DateTime(2015, 12, 6, 12, 0, 0), UTC())), "2015/12/06 12:00:00-UTC");
190 	test(SDLValue.dateTime(SysTime(DateTime(2015, 12, 6, 12, 0, 0), new immutable SimpleTimeZone(-2.hours - 30.minutes))), "2015/12/06 12:00:00-GMT-02:30");
191 	test(SDLValue.dateTime(SysTime(DateTime(2015, 12, 6, 12, 0, 0), new immutable SimpleTimeZone(31.minutes))), "2015/12/06 12:00:00-GMT+00:31");
192 	test(SDLValue.dateTime(SysTime(DateTime(2017, 11, 22, 18, 0, 0), new immutable SimpleTimeZone(0.hours))), "2017/11/22 18:00:00-GMT+00:00");
193 }
194 
195 
196 /** Escapes a given string to ensure safe usage within an SDLang quoted string.
197 */
198 void escapeSDLString(R)(ref R dst, in char[] str)
199 {
200 	// TODO: insert line breaks
201 	foreach (char ch; str) {
202 		switch (ch) {
203 			default: dst.put(ch); break;
204 			case '"': dst.put(`\"`); break;
205 			case '\\': dst.put(`\\`); break;
206 			case '\t': dst.put(`\t`); break;
207 			case '\n': dst.put(`\n`); break;
208 			case '\r': dst.put(`\r`); break;
209 		}
210 	}
211 }
212 
213 unittest {
214 	import std.array : appender;
215 
216 	auto app = appender!string;
217 	app.escapeSDLString("foo\\bar\r\n\t\tbäz\"");
218 	assert(app.data == `foo\\bar\r\n\t\tbäz\"`, app.data);
219 }
220 
221 private void putIndentation(R)(ref R dst, size_t level)
222 {
223 	foreach (i; 0 .. level)
224 		dst.put('\t');
225 }
226 
227 // output a floating point number in pure decimal format, without losing
228 // precision (at least approximately) and without redundant zeros
229 private void writeFloat(R, F)(ref R dst, const(F) num)
230 {
231 	import std.format : formattedWrite;
232 	import std.math : floor, fmod, isNaN, log10;
233 
234 	static if (is(F == float)) enum sig = 7;
235 	else enum sig = 15;
236 
237 	if (num.isNaN || num == F.infinity || num == -F.infinity) {
238 		dst.put("0.0");
239 		return;
240 	}
241 
242 	if (!num) {
243 		dst.put("0.0");
244 		return;
245 	}
246 
247 	if (fmod(num, F(1)) == 0) dst.formattedWrite("%.1f", num);
248 	else {
249 		F unum;
250 		if (num < 0) {
251 			dst.put('-');
252 			unum = -num;
253 		} else unum = num;
254 
255 		auto firstdig = cast(long)floor(log10(unum));
256 		if (firstdig >= sig) dst.formattedWrite("%.1f", unum);
257 		else {
258 			char[32] fmt;
259 			char[] fmtdst = fmt[];
260 			fmtdst.formattedWrite("%%.%sg", sig - firstdig);
261 			dst.formattedWrite(fmt[0 .. $-fmtdst.length], unum);
262 		}
263 	}
264 }
265 
266 unittest {
267 	void test(F)(F v, string txt)
268 	{
269 		auto app = appender!string;
270 		app.writeFloat(v);
271 		assert(app.data == txt, app.data);
272 	}
273 
274 	test(float.infinity, "0.0");
275 	test(-float.infinity, "0.0");
276 	test(float.nan, "0.0");
277 
278 	test(double.infinity, "0.0");
279 	test(-double.infinity, "0.0");
280 	test(double.nan, "0.0");
281 
282 	test(0.0, "0.0");
283 	test(1.0, "1.0");
284 	test(-1.0, "-1.0");
285 	test(0.0f, "0.0");
286 	test(1.0f, "1.0");
287 	test(-1.0f, "-1.0");
288 
289 	test(100.0, "100.0");
290 	test(0.0078125, "0.0078125");
291 	test(100.001, "100.001");
292 	test(-100.0, "-100.0");
293 	test(-0.0078125, "-0.0078125");
294 	test(-100.001, "-100.001");
295 	test(100.0f, "100.0");
296 	test(0.0078125f, "0.0078125");
297 	test(100.01f, "100.01");
298 	test(-100.0f, "-100.0");
299 	test(-0.0078125f, "-0.0078125");
300 	test(-100.01f, "-100.01");
301 }
302 
303 private void writeFracSecs(R)(ref R dst, long hnsecs)
304 {
305 	import std.format : formattedWrite;
306 
307 	assert(hnsecs >= 0 && hnsecs < 10_000_000);
308 
309 	if (hnsecs > 0) {
310 		if (hnsecs % 10_000 == 0)
311 			dst.formattedWrite(".%03d", hnsecs / 10_000);
312 		else dst.formattedWrite(".%07d", hnsecs);
313 	}
314 }
315 
316 unittest {
317 	import std.array : appender;
318 
319 	void test(Duration dur, string exp)
320 	{
321 		auto app = appender!string;
322 		app.writeFracSecs(dur.total!"hnsecs");
323 		assert(app.data == exp, app.data);
324 	}
325 
326 	test(0.msecs, "");
327 	test(123.msecs, ".123");
328 	test(123400.usecs, ".1234000");
329 	test(1234567.hnsecs, ".1234567");
330 }
331 
332 
333 private void generateBase64(R)(ref R dst, in ubyte[] bytes)
334 {
335 	import std.base64 : Base64;
336 
337 	Base64.encode(bytes, dst);
338 }