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 }