1 // Written in the D programming language.
2 
3 /**
4 
5 This module contains implementation of settings container.
6 
7 Similar to JSON, can be written/read to/from JSON.
8 
9 Difference from usual JSON implementations: map (object) is ordered - will be written in the same order as read (or created).
10 
11 Has a lot of methods for convenient storing/accessing of settings.
12 
13 
14 Synopsis:
15 
16 ----
17 import dlangui.core.settings;
18 
19 Setting s = new Setting();
20 
21 ----
22 
23 Copyright: Vadim Lopatin, 2014
24 License:   Boost License 1.0
25 Authors:   Vadim Lopatin, coolreader.org@gmail.com
26 */
27 module dlangui.core.settings;
28 
29 import dlangui.core.logger;
30 import dlangui.core.types : parseHexDigit;
31 public import dlangui.core.parseutils;
32 import std.range;
33 //import std.algorithm : clamp, equal;
34 import std.algorithm : equal;
35 import std.conv : to;
36 import std.utf : encode;
37 import std.math : pow;
38 import std.file;
39 import std.path;
40 import std.datetime : SysTime;
41 
42 /// setting types - same as in std.json
43 enum SettingType {
44     STRING,
45     INTEGER,
46     UINTEGER,
47     FLOAT,
48     OBJECT,
49     ARRAY,
50     TRUE,
51     FALSE,
52     NULL
53 }
54 
55 /// settings file format
56 enum SettingsFileFormat {
57     JSON,
58     SDL,
59 }
60 
61 /// Settings object whith file information
62 class SettingsFile {
63     protected Setting _setting;
64     protected string _filename;
65     protected SysTime _lastModificationTime;
66     protected bool _loaded;
67     protected SettingsFileFormat _format = SettingsFileFormat.JSON;
68 
69     @property Setting setting() { return _setting; }
70     @property Setting copySettings() {
71         return _setting.clone();
72     }
73     /// replace setting object
74     void replaceSetting(Setting s) {
75         _setting = s;
76     }
77     @property void applySettings(Setting settings) {
78         // TODO copy only changed settings
79         _setting = settings;
80         //_setting.apply(settings);
81     }
82 
83     /// create settings file object; if filename is provided, attempts to load settings from file
84     this(string filename = null) {
85         _setting = new Setting();
86         _filename = filename;
87         if (_filename) {
88             string dir = dirName(_filename);
89             if (load()) {
90                 // loaded ok
91             } else {
92             }
93         }
94     }
95 
96     static int limitInt(long value, int minvalue, int maxvalue) {
97         if (value < minvalue)
98             return minvalue;
99         if (value > maxvalue)
100             return maxvalue;
101         return cast(int)value;
102         // remove clamp to support older compilers
103         //return clamp(cast(int)value, minvalue, maxvalue);
104     }
105 
106     static string limitString(string value, const string[] values)
107     in { assert(values.length > 0); }
108     do {
109         foreach(v; values)
110             if (v.equal(value))
111                 return value;
112         return values[0];
113     }
114 
115 
116     @property bool loaded() {
117         return _loaded;
118     }
119 
120     /// filename
121     @property string filename() { return _filename; }
122     /// filename
123     @property void filename(string fn) { _filename = fn; }
124 
125     protected bool updateModificationTime() {
126         if (_filename is null)
127             return false;
128         try {
129             if (!_filename.exists || !_filename.isFile)
130                 return false;
131             SysTime accTime;
132             getTimes(_filename, accTime, _lastModificationTime);
133             return true;
134         } catch (Exception e) {
135             return false;
136         }
137     }
138 
139     /// load settings from file
140     bool load(string filename = null) {
141         if (filename !is null)
142             _filename = filename;
143         assert(_filename !is null);
144         if (updateModificationTime()) {
145             bool res = _setting.load(_filename);
146             if (res)
147                 _loaded = true;
148             afterLoad();
149             return res;
150         }
151         return false;
152     }
153 
154     /// save settings to file
155     bool save(string filename = null, bool pretty = true) {
156         if (filename !is null)
157             _filename = filename;
158         assert(_filename);
159         string dir = dirName(_filename);
160         if (!dir.exists) {
161             try {
162                 mkdirRecurse(dir);
163             } catch (Exception e) {
164                 return false;
165             }
166         } else if (!dir.isDir) {
167             Log.d("", dir, " is file");
168             return false;
169         }
170         bool res = _setting.save(_filename, pretty);
171         res = updateModificationTime() || res;
172         afterSave();
173         return res;
174     }
175 
176     /// override to add default values if missing
177     void updateDefaults() {
178     }
179 
180     /// override to do something after loading - e.g. set defaults
181     void afterLoad() {
182     }
183 
184     /// override to do something after saving
185     void afterSave() {
186     }
187 
188     bool merge(string json) {
189         try {
190             Setting setting = new Setting();
191             setting.parseJSON(json);
192             _setting.apply(setting);
193         } catch (Exception e) {
194             Log.e("SettingsFile.merge - failed to parse json", e);
195             return false;
196         }
197         return true;
198     }
199 }
200 
201 /// setting object
202 final class Setting {
203     union Store {
204         string str;
205         long integer;
206         ulong uinteger;
207         double floating;
208         SettingArray array;
209         SettingMap * map;
210     }
211     private Setting _parent;
212     private Store _store;
213     private bool _changed;
214     private SettingType _type = SettingType.NULL;
215 
216     this() {
217         // NULL setting
218     }
219     this(long v) {
220         integer = v;
221     }
222     this(ulong v) {
223         uinteger = v;
224     }
225     this(string v) {
226         str = v;
227     }
228     this(double v) {
229         floating = v;
230     }
231     this(bool v) {
232         boolean = v;
233     }
234     this(Setting[] v) {
235         clear(SettingType.ARRAY);
236         _store.array.list = v;
237     }
238 
239     this(string[] v) {
240         clear(SettingType.ARRAY);
241         this.strArray = v;
242     }
243 
244     this(string[string] v) {
245         clear(SettingType.ARRAY);
246         this.strMap = v;
247     }
248 
249     /// returns true if setting has been changed
250     @property bool changed() {
251         return _changed;
252     }
253 
254     /// sets change flag
255     @property void changed(bool changed) {
256         _changed = changed;
257     }
258 
259     /// array
260     private static struct SettingArray {
261         Setting[] list;
262         @property bool empty() inout { return list.length == 0; }
263         Setting set(int index, Setting value, Setting parent = null) {
264             if (index < 0)
265                 index = cast(int)(list.length);
266             if (index >= list.length) {
267                 int oldlen = cast(int)list.length;
268                 list.length = index + 1;
269                 foreach(i; oldlen .. index)
270                     list[i] = new Setting(); // insert NULL items in holes
271             }
272             list[index] = value;
273             value.parent = parent;
274             return value;
275         }
276         /// get item by index, returns null if index out of bounds
277         Setting get(int index) {
278             if (index < 0 || index >= list.length)
279                 return null;
280             return list[index];
281         }
282         /// remove by index, returns removed value
283         Setting remove(int index) {
284             Setting res = get(index);
285             if (!res)
286                 return null;
287             foreach(i; index .. list.length - 1)
288                 list[i] = list[i + 1];
289             list[$ - 1] = null;
290             list.length--;
291             return res;
292         }
293         @property int length() {
294             return cast(int)list.length;
295         }
296         /// deep copy
297         void copyFrom(ref SettingArray v) {
298             list.length = v.list.length;
299             foreach(i; 0 .. v.list.length) {
300                 list[i] = v.list[i].clone();
301             }
302         }
303     }
304 
305     /// ordered map
306     private static struct SettingMap {
307         Setting[] list;
308         int[string] map;
309         @property bool empty() inout { return list.length == 0; }
310         /// get item by index, returns null if index out of bounds
311         Setting get(int index) {
312             if (index < 0 || index >= list.length)
313                 return null;
314             return list[index];
315         }
316         /// get item by key, returns null if key is not found
317         Setting get(string key) {
318             auto p = (key in map);
319             if (!p)
320                 return null;
321             return list[*p];
322         }
323         Setting set(string key, Setting value, Setting parent) {
324             value.parent = parent;
325             auto p = (key in map);
326             if (p) {
327                 // key is found
328                 list[*p] = value;
329             } else {
330                 // new value
331                 list ~= value;
332                 map[key] = cast(int)list.length - 1;
333             }
334             return value;
335         }
336 
337         /// remove by index, returns removed value
338         Setting remove(int index) {
339             Setting res = get(index);
340             if (!res)
341                 return null;
342             foreach(i; index .. list.length - 1)
343                 list[i] = list[i + 1];
344             list[$ - 1] = null;
345             list.length--;
346             string key;
347             foreach(k, ref v; map) {
348                 if (v == index) {
349                     key = k;
350                 } else if (v > index) {
351                     v--;
352                 }
353             }
354             if (key)
355                 map.remove(key);
356             return res;
357         }
358         /// returns key for index
359         string keyByIndex(int index) {
360             foreach(k, ref v; map) {
361                 if (v == index) {
362                     return k;
363                 }
364             }
365             return null;
366         }
367         /// remove by key, returns removed value
368         Setting remove(string key) {
369             auto p = (key in map);
370             if (!p)
371                 return null;
372             return remove(*p);
373         }
374         @property int length() {
375             return cast(int)list.length;
376         }
377         /// deep copy
378         void copyFrom(SettingMap * v) {
379             list.length = v.list.length;
380             foreach(i; 0 .. v.list.length) {
381                 list[i] = v.list[i].clone();
382             }
383             destroy(map);
384             foreach(key, value; v.map)
385                 map[key] = value;
386         }
387     }
388 
389 
390     /// get parent
391     @property inout(Setting) parent() inout { return _parent; }
392     /// set parent
393     @property Setting parent(Setting v) {
394         _parent = v;
395         return v;
396     }
397 
398     /// returns SettingType of setting
399     @property SettingType type() const { return _type; }
400 
401     @property bool isString() { return _type == SettingType.STRING; }
402     @property bool isInteger() { return _type == SettingType.INTEGER; }
403     @property bool isUinteger() { return _type == SettingType.UINTEGER; }
404     @property bool isFloating() { return _type == SettingType.FLOAT; }
405     @property bool isObject() { return _type == SettingType.OBJECT; }
406     @property bool isArray() { return _type == SettingType.ARRAY; }
407     @property bool isBoolean() { return _type == SettingType.TRUE || _type == SettingType.FALSE; }
408     @property bool isNull() { return _type == SettingType.NULL; }
409 
410     /// clear value and set new type
411     void clear(SettingType newType) {
412         if (newType != _type) {
413             clear();
414             _type = newType;
415         }
416         clear();
417     }
418     /// clear value
419     void clear() {
420         final switch(_type) with(SettingType) {
421             case STRING:
422                 _store.str = null;
423                 break;
424             case ARRAY:
425                 _store.array = _store.array.init;
426                 break;
427             case OBJECT:
428                 _store.map = _store.map.init;
429                 break;
430             case INTEGER:
431                 _store.integer = _store.integer.init;
432                 break;
433             case UINTEGER:
434                 _store.uinteger = _store.uinteger.init;
435                 break;
436             case FLOAT:
437                 _store.floating = _store.floating.init;
438                 break;
439             case TRUE:
440             case FALSE:
441             case NULL:
442                 break;
443         }
444     }
445 
446     void apply(Setting settings) {
447         if (settings.isObject) {
448             foreach(key, value; settings.map) {
449                 this[key] = value;
450             }
451         }
452     }
453 
454     /// deep copy of settings
455     Setting clone() {
456         Setting res = new Setting();
457         res.clear(_type);
458         final switch(_type) with(SettingType) {
459             case STRING:
460                 res._store.str = _store.str;
461                 break;
462             case ARRAY:
463                 res._store.array.copyFrom(_store.array);
464                 break;
465             case OBJECT:
466                 if (_store.map) {
467                     res._store.map = new SettingMap();
468                     res._store.map.copyFrom(_store.map);
469                 }
470                 break;
471             case INTEGER:
472                 res._store.integer = _store.integer;
473                 break;
474             case UINTEGER:
475                 res._store.uinteger = _store.uinteger;
476                 break;
477             case FLOAT:
478                 res._store.floating = _store.floating;
479                 break;
480             case TRUE:
481             case FALSE:
482             case NULL:
483                 break;
484         }
485         res._changed = false;
486         return res;
487     }
488 
489 
490     /// read as string value
491     @property string str() {
492         final switch(_type) with(SettingType) {
493             case STRING:
494                 return _store.str;
495             case INTEGER:
496                 return to!string(_store.integer);
497             case UINTEGER:
498                 return to!string(_store.uinteger);
499             case FLOAT:
500                 return to!string(cast(double)_store.floating);
501             case TRUE:
502                 return "true";
503             case FALSE:
504                 return "false";
505             case NULL:
506             case ARRAY:
507             case OBJECT:
508                 return null;
509         }
510     }
511     /// read as string value
512     inout (string) strDef(inout (string) defValue) {
513         final switch(_type) with(SettingType) {
514             case STRING:
515                 return _store.str;
516             case INTEGER:
517                 return to!string(_store.integer);
518             case UINTEGER:
519                 return to!string(_store.uinteger);
520             case FLOAT:
521                 return to!string(cast(double)_store.floating);
522             case TRUE:
523                 return "true";
524             case FALSE:
525                 return "false";
526             case NULL:
527             case ARRAY:
528             case OBJECT:
529                 return defValue;
530         }
531     }
532     /// set string value for object
533     @property string str(string v) {
534         if (_type != SettingType.STRING)
535             clear(SettingType.STRING);
536         _store.str = v;
537         return v;
538     }
539 
540     /// returns items as string array
541     @property string[] strArray() {
542         final switch(_type) with(SettingType) {
543             case STRING:
544                 return [_store.str];
545             case INTEGER:
546                 return [to!string(_store.integer)];
547             case UINTEGER:
548                 return [to!string(_store.uinteger)];
549             case FLOAT:
550                 return [to!string(_store.floating)];
551             case TRUE:
552                 return ["true"];
553             case FALSE:
554                 return ["false"];
555             case NULL:
556                 return null;
557             case ARRAY:
558             case OBJECT:
559                 string[] res;
560                 foreach(i; 0 .. length)
561                     res ~= this[i].str;
562                 return res;
563         }
564     }
565     /// sets string array
566     @property string[] strArray(string[] list) {
567         clear(SettingType.ARRAY);
568         foreach(s; list) {
569             this[length] = new Setting(s);
570         }
571         return list;
572     }
573 
574     /// returns items as int array
575     @property int[] intArray() {
576         final switch(_type) with(SettingType) {
577             case STRING:
578             case INTEGER:
579             case UINTEGER:
580             case FLOAT:
581             case TRUE:
582             case FALSE:
583                 return [cast(int)integer];
584             case NULL:
585                 return null;
586             case ARRAY:
587             case OBJECT:
588                 int[] res;
589                 foreach(i; 0 .. length)
590                     res ~= cast(int)this[i].integer;
591                 return res;
592         }
593     }
594     /// sets int array
595     @property int[] intArray(int[] list) {
596         clear(SettingType.ARRAY);
597         foreach(s; list) {
598             this[length] = new Setting(cast(long)s);
599         }
600         return list;
601     }
602 
603     /// returns items as Setting array
604     @property Setting[] array() {
605         final switch(_type) with(SettingType) {
606             case STRING:
607             case INTEGER:
608             case UINTEGER:
609             case FLOAT:
610             case TRUE:
611             case FALSE:
612                 return [this];
613             case NULL:
614                 return null;
615             case ARRAY:
616             case OBJECT:
617                 Setting[] res;
618                 foreach(i; 0 .. length)
619                     res ~= this[i];
620                 return res;
621         }
622     }
623     /// sets Setting array
624     @property Setting[] array(Setting[] list) {
625         clear(SettingType.ARRAY);
626         foreach(s; list) {
627             this[length] = s;
628         }
629         return list;
630     }
631 
632     /// returns items as string[string] map
633     @property string[string] strMap() {
634         final switch(_type) with(SettingType) {
635             case STRING:
636             case INTEGER:
637             case UINTEGER:
638             case FLOAT:
639             case TRUE:
640             case FALSE:
641             case NULL:
642             case ARRAY:
643                 return null;
644             case OBJECT:
645                 string[string] res;
646                 if (_store.map) {
647                     foreach(key, value; _store.map.map)  {
648                         Setting v = _store.map.get(value);
649                         res[key] = v ? v.str : null;
650                     }
651                 }
652                 return res;
653         }
654     }
655     /// sets string[string] map
656     @property string[string] strMap(string[string] list) {
657         clear(SettingType.OBJECT);
658         foreach(key, value; list) {
659             this[key] = new Setting(value);
660         }
661         return list;
662     }
663 
664     /// returns items as int[string] map
665     @property int[string] intMap() {
666         final switch(_type) with(SettingType) {
667             case STRING:
668             case INTEGER:
669             case UINTEGER:
670             case FLOAT:
671             case TRUE:
672             case FALSE:
673             case NULL:
674             case ARRAY:
675                 return null;
676             case OBJECT:
677                 int[string] res;
678                 foreach(key, value; _store.map.map)
679                     res[key] = cast(int)this[value].integer;
680                 return res;
681         }
682     }
683     /// sets int[string] map
684     @property int[string] intMap(int[string] list) {
685         clear(SettingType.OBJECT);
686         foreach(key, value; list) {
687             this[key] = new Setting(cast(long)value);
688         }
689         return list;
690     }
691 
692     /// returns items as Setting[string] map
693     @property Setting[string] map() {
694         final switch(_type) with(SettingType) {
695             case STRING:
696             case INTEGER:
697             case UINTEGER:
698             case FLOAT:
699             case TRUE:
700             case FALSE:
701             case NULL:
702             case ARRAY:
703                 return null;
704             case OBJECT:
705                 Setting[string] res;
706                 foreach(key, value; _store.map.map)
707                     res[key] = this[value];
708                 return res;
709         }
710     }
711     /// sets Setting[string] map
712     @property Setting[string] map(Setting[string] list) {
713         clear(SettingType.OBJECT);
714         foreach(key, value; list) {
715             this[key] = value;
716         }
717         return list;
718     }
719 
720     /// to iterate using foreach
721     int opApply(int delegate(ref Setting)dg) {
722         int result = 0;
723         if (_type == SettingType.ARRAY) {
724             for(int i = 0; i < _store.array.list.length; i++) {
725                 result = dg(_store.array.list[i]);
726                 if (result)
727                     break;
728             }
729         } else if (_type == SettingType.OBJECT) {
730             for(int i = 0; i < _store.map.list.length; i++) {
731                 result = dg(_store.map.list[i]);
732                 if (result)
733                     break;
734             }
735         }
736         return result;
737     }
738 
739     /// to iterate over OBJECT using foreach(key, value; map)
740     int opApply(int delegate(ref string, ref Setting)dg) {
741         int result = 0;
742         if (_type == SettingType.OBJECT) {
743             for(int i = 0; i < _store.map.list.length; i++) {
744                 string key = _store.map.keyByIndex(i);
745                 result = dg(key, _store.map.list[i]);
746                 if (result)
747                     break;
748             }
749         }
750         return result;
751     }
752 
753     /// to iterate using foreach_reverse
754     int opApplyReverse(int delegate(ref Setting)dg) {
755         int result = 0;
756         if (_type == SettingType.ARRAY) {
757             for(int i = cast(int)_store.array.list.length - 1; i >= 0; i--) {
758                 result = dg(_store.array.list[i]);
759                 if (result)
760                     break;
761             }
762         } else if (_type == SettingType.OBJECT) {
763             for(int i = cast(int)_store.map.list.length - 1; i >= 0; i--) {
764                 result = dg(_store.map.list[i]);
765                 if (result)
766                     break;
767             }
768         }
769         return result;
770     }
771 
772     /// read as long value
773     @property inout(long) integer() inout {
774         final switch(_type) with(SettingType) {
775             case STRING:
776                 return parseLong(_store.str);
777             case INTEGER:
778                 return _store.integer;
779             case UINTEGER:
780                 return cast(long)_store.uinteger;
781             case FLOAT:
782                 return cast(long)_store.floating;
783             case TRUE:
784                 return 1;
785             case FALSE:
786             case NULL:
787             case ARRAY:
788             case OBJECT:
789                 return 0;
790         }
791     }
792 
793     /// read as long value
794     inout(long) integerDef(long defValue) inout {
795         final switch(_type) with(SettingType) {
796             case STRING:
797                 return parseLong(_store.str, defValue);
798             case INTEGER:
799                 return _store.integer;
800             case UINTEGER:
801                 return cast(long)_store.uinteger;
802             case FLOAT:
803                 return cast(long)_store.floating;
804             case TRUE:
805                 return 1;
806             case FALSE:
807                 return 0;
808             case NULL:
809             case ARRAY:
810             case OBJECT:
811                 return defValue;
812         }
813     }
814     /// set long value for object
815     @property long integer(long v) {
816         if (_type != SettingType.INTEGER)
817             clear(SettingType.INTEGER);
818         _store.integer = v;
819         return v;
820     }
821 
822     /// read as ulong value
823     @property inout(long) uinteger() inout {
824         final switch(_type) with(SettingType) {
825             case STRING:
826                 return parseULong(_store.str);
827             case INTEGER:
828                 return cast(ulong)_store.integer;
829             case UINTEGER:
830                 return _store.uinteger;
831             case FLOAT:
832                 return cast(ulong)_store.floating;
833             case TRUE:
834                 return 1;
835             case FALSE:
836             case NULL:
837             case ARRAY:
838             case OBJECT:
839                 return 0;
840         }
841     }
842     /// read as ulong value
843     inout(long) uintegerDef(ulong defValue) inout {
844         final switch(_type) with(SettingType) {
845             case STRING:
846                 return parseULong(_store.str, defValue);
847             case INTEGER:
848                 return cast(ulong)_store.integer;
849             case UINTEGER:
850                 return _store.uinteger;
851             case FLOAT:
852                 return cast(ulong)_store.floating;
853             case TRUE:
854                 return 1;
855             case FALSE:
856                 return 0;
857             case NULL:
858             case ARRAY:
859             case OBJECT:
860                 return defValue;
861         }
862     }
863     /// set ulong value for object
864     @property ulong uinteger(ulong v) {
865         if (_type != SettingType.UINTEGER)
866             clear(SettingType.UINTEGER);
867         _store.uinteger = v;
868         return v;
869     }
870 
871     /// read as double value
872     @property inout(double) floating() inout {
873         final switch(_type) with(SettingType) {
874             case STRING:
875                 return 0; //parseULong(_store.str);
876             case INTEGER:
877                 return cast(double)_store.integer;
878             case UINTEGER:
879                 return cast(double)_store.uinteger;
880             case FLOAT:
881                 return _store.floating;
882             case TRUE:
883                 return 1;
884             case FALSE:
885             case NULL:
886             case ARRAY:
887             case OBJECT:
888                 return 0;
889         }
890     }
891     /// read as double value with default
892     inout(double) floatingDef(double defValue) inout {
893         final switch(_type) with(SettingType) {
894             case STRING:
895                 return defValue; //parseULong(_store.str);
896             case INTEGER:
897                 return cast(double)_store.integer;
898             case UINTEGER:
899                 return cast(double)_store.uinteger;
900             case FLOAT:
901                 return _store.floating;
902             case TRUE:
903                 return 1;
904             case FALSE:
905                 return 0;
906             case NULL:
907             case ARRAY:
908             case OBJECT:
909                 return defValue;
910         }
911     }
912     /// set ulong value for object
913     @property double floating(double v) {
914         if (_type != SettingType.FLOAT)
915             clear(SettingType.FLOAT);
916         _store.floating = v;
917         return v;
918     }
919 
920     /// parse string as boolean; supports 1, 0, y, n, yes, no, t, f, true, false; returns defValue if cannot be parsed
921     static bool parseBool(inout string v, bool defValue = false) {
922         int len = cast(int)v.length;
923         if (len == 0)
924             return defValue;
925         char ch = v[0];
926         if (len == 1) {
927             if (ch == '1' || ch == 'y' || ch == 't')
928                 return true;
929             if (ch == '1' || ch == 'y' || ch == 't')
930                 return false;
931             return defValue;
932         }
933         if (v.equal("yes") || v.equal("true"))
934             return true;
935         if (v.equal("no") || v.equal("false"))
936             return false;
937         return defValue;
938     }
939 
940     /// read as boolean value
941     @property inout(bool) boolean() inout {
942         final switch(_type) with(SettingType) {
943             case STRING:
944                 return parseBool(_store.str);
945             case INTEGER:
946                 return _store.integer != 0;
947             case UINTEGER:
948                 return _store.uinteger != 0;
949             case FLOAT:
950                 return _store.floating != 0;
951             case TRUE:
952                 return true;
953             case FALSE:
954             case NULL:
955                 return false;
956             case ARRAY:
957                 return !_store.array.empty;
958             case OBJECT:
959                 return _store.map && !_store.map.empty;
960         }
961     }
962     /// read as boolean value
963     inout(bool) booleanDef(bool defValue) inout {
964         final switch(_type) with(SettingType) {
965             case STRING:
966                 return parseBool(_store.str, defValue);
967             case INTEGER:
968                 return _store.integer != 0;
969             case UINTEGER:
970                 return _store.uinteger != 0;
971             case FLOAT:
972                 return _store.floating != 0;
973             case TRUE:
974                 return true;
975             case FALSE:
976             case NULL:
977                 return false;
978             case ARRAY:
979                 return defValue;
980             case OBJECT:
981                 return defValue;
982         }
983     }
984     /// set bool value for object
985     @property bool boolean(bool v) {
986         if (_type == SettingType.TRUE) {
987             if (!v) _type = SettingType.FALSE;
988         } else if (_type == SettingType.FALSE) {
989             if (v) _type = SettingType.TRUE;
990         } else {
991             clear(v ? SettingType.TRUE : SettingType.FALSE);
992         }
993         return v;
994     }
995 
996     /// get number of elements for array or map, returns 0 for other types
997     int length() inout {
998         if (_type == SettingType.ARRAY) {
999             return cast(int)_store.array.list.length;
1000         } else if (_type == SettingType.OBJECT) {
1001             return _store.map ? cast(int)_store.map.list.length : 0;
1002         } else
1003             return 0;
1004     }
1005 
1006     /// for array or object returns item by index, null if index is out of bounds or setting is neither array nor object
1007     Setting opIndex(int index) {
1008         if (_type == SettingType.ARRAY) {
1009             return _store.array.get(index);
1010         } else if (_type == SettingType.OBJECT) {
1011             if (!_store.map)
1012                 return null;
1013             return _store.map.get(index);
1014         } else {
1015             return null;
1016         }
1017     }
1018 
1019     /// for object returns item by key, null if not found or this setting is not an object
1020     Setting opIndex(string key) {
1021         if (_type == SettingType.OBJECT) {
1022             if (!_store.map)
1023                 return null;
1024             return _store.map.get(key);
1025         } else {
1026             return null;
1027         }
1028     }
1029 
1030     /// for array or object remove item by index, returns removed item or null if index is out of bounds or setting is neither array nor object
1031     Setting remove(int index) {
1032         if (_type == SettingType.ARRAY) {
1033             return _store.array.remove(index);
1034         } else if (_type == SettingType.OBJECT) {
1035             if (!_store.map)
1036                 return null;
1037             return _store.map.remove(index);
1038         } else {
1039             return null;
1040         }
1041     }
1042 
1043     /// for object remove item by key, returns removed item or null if is not found or setting is not an object
1044     Setting remove(string key) {
1045         if (_type == SettingType.OBJECT) {
1046             if (!_store.map)
1047                 return null;
1048             return _store.map.remove(key);
1049         } else {
1050             return null;
1051         }
1052     }
1053 
1054     // assign long value
1055     long opAssign(long value) {
1056         return (integer = value);
1057     }
1058     // assign ulong value
1059     ulong opAssign(ulong value) {
1060         return (uinteger = value);
1061     }
1062     // assign string value
1063     string opAssign(string value) {
1064         return (str = value);
1065     }
1066     // assign bool value
1067     bool opAssign(bool value) {
1068         return (boolean = value);
1069     }
1070     // assign double value
1071     double opAssign(double value) {
1072         return (floating = value);
1073     }
1074     // assign int[] value
1075     int[] opAssign(int[] value) {
1076         return (intArray = value);
1077     }
1078     // assign string[string] value
1079     string[string] opAssign(string[string] value) {
1080         return (strMap = value);
1081     }
1082     // assign string[] value
1083     string[] opAssign(string[] value) {
1084         return (strArray = value);
1085     }
1086     // assign int[string] value
1087     int[string] opAssign(int[string] value) {
1088         return (intMap = value);
1089     }
1090     // assign Setting[] value
1091     Setting[] opAssign(Setting[] value) {
1092         return (array = value);
1093     }
1094     // assign Setting[string] value
1095     Setting[string] opAssign(Setting[string] value) {
1096         return (map = value);
1097     }
1098 
1099     // array methods
1100     /// sets value for array item by integer index
1101     T opIndexAssign(T)(T value, int index) {
1102         if (_type != SettingType.ARRAY)
1103             clear(SettingType.ARRAY);
1104         static if (is(T: Setting)) {
1105             _store.array.set(index, value, this);
1106         } else {
1107             Setting item = _store.array.get(index);
1108             if (item) {
1109                 // existing item
1110                 item = value;
1111             } else {
1112                 // create new item
1113                 _store.array.set(index, new Setting(value), this);
1114             }
1115         }
1116         return value;
1117     }
1118     /// sets value for array item by integer index if not already present
1119     T setDef(T)(T value, int index) {
1120         if (_type != SettingType.ARRAY)
1121             clear(SettingType.ARRAY);
1122         Setting item = _store.array.get(index);
1123         if (item)
1124             return value;
1125         static if (is(value == Setting)) {
1126             _store.array.set(index, value, this);
1127         } else {
1128             // create new item
1129             _store.array.set(index, new Setting(value), this);
1130         }
1131         return value;
1132     }
1133 
1134     /// returns setting by path like "editors/sourceEditor/tabSize", creates object tree "editors/sourceEditor" and object of specified type if part of path does not exist.
1135     Setting settingByPath(string path, SettingType type, bool createIfNotExist = true) {
1136         if (_type != SettingType.OBJECT)
1137             clear(SettingType.OBJECT);
1138         string part1, part2;
1139         if (splitKey(path, part1, part2)) {
1140             auto s = this[part1];
1141             if (!s) {
1142                 s = new Setting();
1143                 s.clear(SettingType.OBJECT);
1144                 this[part1] = s;
1145             }
1146             return s.settingByPath(part2, type);
1147         } else {
1148             auto s = this[path];
1149             if (!s && createIfNotExist) {
1150                 s = new Setting();
1151                 s.clear(type);
1152                 this[path] = s;
1153             }
1154             return s;
1155         }
1156     }
1157 
1158     /// get (or optionally create) object (map) by slash delimited path (e.g. key1/subkey2/subkey3)
1159     Setting objectByPath(string path, bool createIfNotExist = false) {
1160         if (type != SettingType.OBJECT) {
1161             if (!createIfNotExist)
1162                 return null;
1163             // do we need to allow this conversion to object?
1164             clear(SettingType.OBJECT);
1165         }
1166         string part1, part2;
1167         if (splitKey(path, part1, part2)) {
1168             auto s = this[part1];
1169             if (!s) {
1170                 if (!createIfNotExist)
1171                     return null;
1172                 s = new Setting();
1173                 s.clear(SettingType.OBJECT);
1174                 this[part1] = s;
1175             }
1176             return s.objectByPath(part2, createIfNotExist);
1177         } else {
1178             auto s = this[path];
1179             if (!s) {
1180                 if (!createIfNotExist)
1181                     return null;
1182                 s = new Setting();
1183                 s.clear(SettingType.OBJECT);
1184                 this[path] = s;
1185             }
1186             return s;
1187         }
1188     }
1189 
1190     private static bool splitKey(string key, ref string part1, ref string part2) {
1191         int dashPos = -1;
1192         for (int i = 0; i < key.length; i++) {
1193             if (key[i] == '/') {
1194                 dashPos = i;
1195                 break;
1196             }
1197         }
1198         if (dashPos >= 0) {
1199             // path
1200             part1 = key[0 .. dashPos];
1201             part2 = key[dashPos + 1 .. $];
1202             return true;
1203         }
1204         return false;
1205     }
1206 
1207     // map methods
1208     /// sets value for object item by string key
1209     T opIndexAssign(T)(T value, string key) {
1210         if (_type != SettingType.OBJECT)
1211             clear(SettingType.OBJECT);
1212         if (!_store.map)
1213             _store.map = new SettingMap();
1214         static if (is(T: Setting)) {
1215             _store.map.set(key, value, this);
1216         } else {
1217             Setting item = _store.map.get(key);
1218             if (item) {
1219                 // existing item
1220                 item = value;
1221             } else {
1222                 // create new item
1223                 _store.map.set(key, new Setting(value), this);
1224             }
1225         }
1226         return value;
1227     }
1228     /// sets value for object item by string key
1229     T setDef(T)(T value, string key) {
1230         if (_type != SettingType.OBJECT)
1231             clear(SettingType.OBJECT);
1232         if (!_store.map)
1233             _store.map = new SettingMap();
1234         Setting item = _store.map.get(key);
1235         if (item)
1236             return value;
1237         static if (is(value == Setting)) {
1238             _store.map.set(key, value, this);
1239         } else {
1240             // create new item
1241             _store.map.set(key, new Setting(value), this);
1242         }
1243         return value;
1244     }
1245 
1246     /// sets long item by index of array or map
1247     long setInteger(int index, long value) {
1248         return opIndexAssign(value, index);
1249     }
1250     /// sets ulong item by index of array or map
1251     ulong setUinteger(int index, ulong value) {
1252         return opIndexAssign(value, index);
1253     }
1254     /// sets bool item by index of array or map
1255     bool setBoolean(int index, bool value) {
1256         return opIndexAssign(value, index);
1257     }
1258     /// sets double item by index of array or map
1259     double setFloating(int index, double value) {
1260         return opIndexAssign(value, index);
1261     }
1262     /// sets str item by index of array or map
1263     string setString(int index, string value) {
1264         return opIndexAssign(value, index);
1265     }
1266 
1267     /// sets long item by index of array or map only if it's фдкуфвн present
1268     long setIntegerDef(int index, long value) {
1269         return setDef(value, index);
1270     }
1271     /// sets ulong item by index of array or map only if it's фдкуфвн present
1272     ulong setUintegerDef(int index, ulong value) {
1273         return setDef(value, index);
1274     }
1275     /// sets bool item by index of array or map only if it's фдкуфвн present
1276     bool setBooleanDef(int index, bool value) {
1277         return setDef(value, index);
1278     }
1279     /// sets double item by index of array or map only if it's фдкуфвн present
1280     double setFloatingDef(int index, double value) {
1281         return setDef(value, index);
1282     }
1283     /// sets str item by index of array or map only if it's фдкуфвн present
1284     string setStringDef(int index, string value) {
1285         return setDef(value, index);
1286     }
1287 
1288 
1289     /// returns long item by index of array or map
1290     long getInteger(int index, long defValue = 0) {
1291         if (auto item = opIndex(index))
1292             return item.integerDef(defValue);
1293         return defValue;
1294     }
1295     /// returns ulong item by index of array or map
1296     ulong getUinteger(int index, ulong defValue = 0) {
1297         if (auto item = opIndex(index))
1298             return item.uintegerDef(defValue);
1299         return defValue;
1300     }
1301     /// returns bool item by index of array or map
1302     bool getBoolean(int index, bool defValue = false) {
1303         if (auto item = opIndex(index))
1304             return item.booleanDef(defValue);
1305         return defValue;
1306     }
1307     /// returns double item by index of array or map
1308     double getFloating(int index, double defValue = 0) {
1309         if (auto item = opIndex(index))
1310             return item.floatingDef(defValue);
1311         return defValue;
1312     }
1313     /// returns str item by index of array or map
1314     string getString(int index, string defValue = null) {
1315         if (auto item = opIndex(index))
1316             return item.strDef(defValue);
1317         return defValue;
1318     }
1319 
1320 
1321     /// sets long item of map
1322     long setInteger(string key, long value) {
1323         return opIndexAssign(value, key);
1324     }
1325     /// sets ulong item of map
1326     ulong setUinteger(string key, ulong value) {
1327         return opIndexAssign(value, key);
1328     }
1329     /// sets bool item of map
1330     bool setBoolean(string key, bool value) {
1331         return opIndexAssign(value, key);
1332     }
1333     /// sets double item of map
1334     double setFloating(string key, double value) {
1335         return opIndexAssign(value, key);
1336     }
1337     /// sets str item of map
1338     string setString(string key, string value) {
1339         return opIndexAssign(value, key);
1340     }
1341 
1342     /// sets long item of map if key is not yet present in map
1343     long setIntegerDef(string key, long value) {
1344         return setDef(value, key);
1345     }
1346     /// sets ulong item of map if key is not yet present in map
1347     ulong setUintegerDef(string key, ulong value) {
1348         return setDef(value, key);
1349     }
1350     /// sets bool item of map if key is not yet present in map
1351     bool setBooleanDef(string key, bool value) {
1352         return setDef(value, key);
1353     }
1354     /// sets double item of map if key is not yet present in map
1355     double setFloatingDef(string key, double value) {
1356         return setDef(value, key);
1357     }
1358     /// sets str item of map if key is not yet present in map
1359     string setStringDef(string key, string value) {
1360         return setDef(value, key);
1361     }
1362 
1363 
1364 
1365     /// returns long item by key from map
1366     long getInteger(string key, long defValue = 0) {
1367         if (auto item = opIndex(key))
1368             return item.integerDef(defValue);
1369         return defValue;
1370     }
1371     /// returns ulong item by key from map
1372     ulong getUinteger(string key, ulong defValue = 0) {
1373         if (auto item = opIndex(key))
1374             return item.uintegerDef(defValue);
1375         return defValue;
1376     }
1377     /// returns bool item by key from map
1378     bool getBoolean(string key, bool defValue = false) {
1379         if (auto item = opIndex(key))
1380             return item.booleanDef(defValue);
1381         return defValue;
1382     }
1383     /// returns double item by key from map
1384     double getFloating(string key, double defValue = 0) {
1385         if (auto item = opIndex(key))
1386             return item.floatingDef(defValue);
1387         return defValue;
1388     }
1389     /// returns str item by key from map
1390     string getString(string key, string defValue = null) {
1391         if (auto item = opIndex(key))
1392             return item.strDef(defValue);
1393         return defValue;
1394     }
1395     /// returns string array item by key from map, returns null if not found
1396     string[] getStringArray(string key) {
1397         if (auto item = opIndex(key))
1398             return item.strArray();
1399         return null;
1400     }
1401 
1402     /// serialize to json
1403     string toJSON(bool pretty = false) {
1404         Buf buf;
1405         toJSON(buf, 0, pretty);
1406         return buf.get();
1407     }
1408     private static struct Buf {
1409         char[] buffer;
1410         int pos;
1411         string get() {
1412             return buffer[0 .. pos].dup;
1413         }
1414         void reserve(size_t size) {
1415             if (pos + size >= buffer.length)
1416                 buffer.length = buffer.length ? 4096 : (pos + size + 4096) * 2;
1417         }
1418         void append(char ch) {
1419             buffer[pos++] = ch;
1420         }
1421         void append(string s) {
1422             foreach(ch; s)
1423                 buffer[pos++] = ch;
1424         }
1425         void appendEOL() {
1426             append('\n');
1427         }
1428 
1429         void appendTabs(int level) {
1430             reserve(level * 4 + 1024);
1431             foreach(i; 0 .. level) {
1432                 buffer[pos++] = ' ';
1433                 buffer[pos++] = ' ';
1434                 buffer[pos++] = ' ';
1435                 buffer[pos++] = ' ';
1436             }
1437         }
1438 
1439         void appendHex(uint ch) {
1440             buffer[pos++] = '\\';
1441             buffer[pos++] = 'u';
1442             for (int i = 3; i >= 0; i--) {
1443                 uint d = (ch >> (4 * i)) & 0x0F;
1444                 buffer[pos++] = "0123456789abcdef"[d];
1445             }
1446         }
1447         void appendJSONString(string s) {
1448             reserve(s.length * 3 + 8);
1449             if (s is null) {
1450                 append("null");
1451             } else {
1452                 append('\"');
1453                 foreach(ch; s) {
1454                     switch (ch) {
1455                         case '\\':
1456                             buffer[pos++] = '\\';
1457                             buffer[pos++] = '\\';
1458                             break;
1459                         case '\"':
1460                             buffer[pos++] = '\\';
1461                             buffer[pos++] = '\"';
1462                             break;
1463                         case '\r':
1464                             buffer[pos++] = '\\';
1465                             buffer[pos++] = 'r';
1466                             break;
1467                         case '\n':
1468                             buffer[pos++] = '\\';
1469                             buffer[pos++] = 'n';
1470                             break;
1471                         case '\b':
1472                             buffer[pos++] = '\\';
1473                             buffer[pos++] = 'b';
1474                             break;
1475                         case '\t':
1476                             buffer[pos++] = '\\';
1477                             buffer[pos++] = 't';
1478                             break;
1479                         case '\f':
1480                             buffer[pos++] = '\\';
1481                             buffer[pos++] = 'f';
1482                             break;
1483                         default:
1484                             if (ch < ' ') {
1485                                 appendHex(ch);
1486                             } else {
1487                                 buffer[pos++] = ch;
1488                             }
1489                             break;
1490                     }
1491                 }
1492                 append('\"');
1493             }
1494         }
1495     }
1496 
1497     void toJSON(ref Buf buf, int level, bool pretty) {
1498         buf.reserve(1024);
1499         final switch(_type) with(SettingType) {
1500             case STRING:
1501                 buf.appendJSONString(_store.str);
1502                 break;
1503             case INTEGER:
1504                 buf.append(to!string(_store.integer));
1505                 break;
1506             case UINTEGER:
1507                 buf.append(to!string(_store.uinteger));
1508                 break;
1509             case FLOAT:
1510                 buf.append(to!string(_store.floating));
1511                 break;
1512             case TRUE:
1513                 buf.append("true");
1514                 break;
1515             case FALSE:
1516                 buf.append("false");
1517                 break;
1518             case NULL:
1519                 buf.append("null");
1520                 break;
1521             case ARRAY:
1522                 buf.append('[');
1523                 if (pretty && _store.array.length > 0)
1524                     buf.appendEOL();
1525                 foreach(i; 0 .. _store.array.length) {
1526                     if (pretty)
1527                         buf.appendTabs(level + 1);
1528                     _store.array.get(i).toJSON(buf, level + 1, pretty);
1529                     if (i >= _store.array.length - 1)
1530                         break;
1531                     buf.append(',');
1532                     if (pretty)
1533                         buf.appendEOL();
1534                 }
1535                 if (pretty) {
1536                     buf.appendEOL();
1537                     buf.appendTabs(level);
1538                 }
1539                 buf.append(']');
1540                 break;
1541             case OBJECT:
1542                 buf.append('{');
1543                 if (_store.map && _store.map.length) {
1544                     if (pretty)
1545                         buf.appendEOL();
1546                     for (int i = 0; ; i++) {
1547                         string key = _store.map.keyByIndex(i);
1548                         if (pretty)
1549                             buf.appendTabs(level + 1);
1550                         buf.appendJSONString(key);
1551                         buf.append(':');
1552                         if (pretty)
1553                             buf.append(' ');
1554                         _store.map.get(i).toJSON(buf, level + 1, pretty);
1555                         if (i >= _store.map.length - 1)
1556                             break;
1557                         buf.append(',');
1558                         if (pretty)
1559                             buf.appendEOL();
1560                     }
1561                 }
1562                 if (pretty) {
1563                     buf.appendEOL();
1564                     buf.appendTabs(level);
1565                 }
1566                 buf.append('}');
1567                 break;
1568         }
1569     }
1570 
1571     /// save to file
1572     bool save(string filename, bool pretty = true) {
1573         try {
1574             write(filename, toJSON(pretty));
1575             return true;
1576         } catch (Exception e) {
1577             Log.e("exception while saving settings file: ", e);
1578             return false;
1579         }
1580     }
1581 
1582     private static struct JsonParser {
1583         string json;
1584         int pos;
1585         bool allowEol; // for SDL parsing where EOLs are meaningful
1586         void initialize(string s, bool allowEol) {
1587             json = s;
1588             pos = 0;
1589             this.allowEol = allowEol;
1590         }
1591         /// returns current char
1592         @property char peek() {
1593             return pos < json.length ? json[pos] : 0;
1594         }
1595         /// return fragment of text in current position
1596         @property string currentContext() {
1597             if (pos >= json.length)
1598                 return "end of file";
1599             string res = json[pos .. $];
1600             if (res.length > 100)
1601                 res.length = 100;
1602             return res;
1603         }
1604         /// skips current char, returns next one (or null if eof)
1605         @property char nextChar() {
1606             if (pos + 1 < json.length) {
1607                 return json[++pos];
1608             } else {
1609                 if (pos < json.length)
1610                     pos++;
1611             }
1612             return 0;
1613         }
1614         void error(string msg) {
1615             string context;
1616             // calculate error position line and column
1617             int line = 1;
1618             int col = 1;
1619             int lineStart = 0;
1620             foreach(int i; 0 .. pos) {
1621                 char ch = json[i];
1622                 if (ch == '\r') {
1623                     if (i < json.length - 1 && json[i + 1] == '\n')
1624                         i++;
1625                     line++;
1626                     col = 1;
1627                     lineStart = i + 1;
1628                 } else if (ch == '\n') {
1629                     if (i < json.length - 1 && json[i + 1] == '\r')
1630                         i++;
1631                     line++;
1632                     col = 1;
1633                     lineStart = i + 1;
1634                 }
1635             }
1636             int contextStart = pos;
1637             int contextEnd = pos;
1638             for (; contextEnd < json.length; contextEnd++) {
1639                 if (json[contextEnd] == '\r' || json[contextEnd] == '\n')
1640                     break;
1641             }
1642             if (contextEnd - contextStart < 3) {
1643                 for (int i = 0; i < 3 && contextStart > 0; contextStart--, i++) {
1644                     if (json[contextStart - 1] == '\r' || json[contextStart - 1] == '\n')
1645                         break;
1646                 }
1647             } else if (contextEnd > contextStart + 10)
1648                 contextEnd = contextStart + 10;
1649             if (contextEnd > contextStart && contextEnd < json.length)
1650                 context = "near `" ~ json[contextStart .. contextEnd] ~ "` ";
1651             else if (pos >= json.length)
1652                 context = "at end of file";
1653             throw new Exception("JSON parsing error in (" ~ to!string(line) ~ ":" ~ to!string(col) ~ ") " ~ context ~ ": " ~ msg);
1654         }
1655         static bool isAlpha(char ch) {
1656             static import std.ascii;
1657             return std.ascii.isAlpha(ch) || ch == '_';
1658         }
1659         static bool isAlNum(char ch) {
1660             static import std.ascii;
1661             return std.ascii.isAlphaNum(ch) || ch == '_';
1662         }
1663         /// skip spaces and comments, return next available character
1664         @property char skipSpaces() {
1665             static import std.ascii;
1666             for(;pos < json.length;pos++) {
1667                 char ch = json[pos];
1668                 char nextch = pos + 1 < json.length ? json[pos + 1] : 0;
1669                 if (allowEol && ch == '\n')
1670                     break;
1671                 if (ch == '#' || (ch == '/' && nextch == '/') || (ch == '-' && nextch == '-')) {
1672                     // skip one line comment // or # or --
1673                     pos++;
1674                     for(;pos < json.length;pos++) {
1675                         ch = json[pos];
1676                         if (ch == '\n')
1677                             break;
1678                     }
1679                     if (allowEol && ch == '\n')
1680                         break;
1681                     continue;
1682                 } else if (ch == '/' && nextch == '*') {
1683                     // skip multiline /* */ comment
1684                     pos += 2;
1685                     for(;pos < json.length;pos++) {
1686                         ch = json[pos];
1687                         nextch = pos + 1 < json.length ? json[pos + 1] : 0;
1688                         if (ch == '*' && nextch == '/') {
1689                             pos += 2;
1690                             break;
1691                         }
1692                     }
1693                     continue;
1694                 } else if (ch == '\\' && nextch == '\n') {
1695                     // continue to next line
1696                     pos += 2;
1697                     continue;
1698                 }
1699                 if (!std.ascii.isWhite(ch))
1700                     break;
1701             }
1702             return peek;
1703         }
1704 
1705         string parseUnicodeChar() {
1706             if (pos >= json.length - 3)
1707                 error("unexpected end of file while parsing unicode character entity inside string");
1708             dchar ch = 0;
1709             foreach(i; 0 .. 4) {
1710                 uint d = parseHexDigit(nextChar);
1711                 if (d == uint.max)
1712                     error("error while parsing unicode character entity inside string");
1713                 ch = (ch << 4) | d;
1714             }
1715             char[4] buf;
1716             size_t sz = encode(buf, ch);
1717             return buf[0..sz].dup;
1718         }
1719 
1720         @property string parseString() {
1721             char[] res;
1722             char ch = peek;
1723             char quoteChar = ch;
1724             if (ch != '\"' && ch != '`')
1725                 error("cannot parse string");
1726             for (;;) {
1727                 ch = nextChar;
1728                 if (!ch)
1729                     error("unexpected end of file while parsing string");
1730                 if (ch == quoteChar) {
1731                     nextChar;
1732                     return cast(string)res;
1733                 }
1734                 if (ch == '\\' && quoteChar != '`') {
1735                     // escape sequence
1736                     ch = nextChar;
1737                     switch (ch) {
1738                         case 'n':
1739                             res ~= '\n';
1740                             break;
1741                         case 'r':
1742                             res ~= '\r';
1743                             break;
1744                         case 'b':
1745                             res ~= '\b';
1746                             break;
1747                         case 'f':
1748                             res ~= '\f';
1749                             break;
1750                         case '\\':
1751                             res ~= '\\';
1752                             break;
1753                         case '/':
1754                             res ~= '/';
1755                             break;
1756                         case '\"':
1757                             res ~= '\"';
1758                             break;
1759                         case 'u':
1760                             res ~= parseUnicodeChar();
1761                             break;
1762                         default:
1763                             error("unexpected escape sequence in string");
1764                             break;
1765                     }
1766                 } else {
1767                     res ~= ch;
1768                 }
1769             }
1770         }
1771         @property string parseIdent() {
1772             char ch = peek;
1773             if (ch == '\"' || ch == '`') {
1774                 return parseString;
1775             }
1776             char[] res;
1777             if (isAlpha(ch)) {
1778                 res ~= ch;
1779                 for (;;) {
1780                     ch = nextChar;
1781                     if (isAlNum(ch)) {
1782                         res ~= ch;
1783                     } else {
1784                         break;
1785                     }
1786                 }
1787             } else
1788                 error("cannot parse ident");
1789             return cast(string)res;
1790         }
1791         bool parseKeyword(string ident) {
1792             // returns true if parsed ok
1793             if (pos + ident.length > json.length)
1794                 return false;
1795             foreach(i; 0 .. ident.length) {
1796                 if (ident[i] != json[pos + i])
1797                     return false;
1798             }
1799             if (pos + ident.length < json.length) {
1800                 char ch = json[pos + ident.length];
1801                 if (isAlNum(ch))
1802                     return false;
1803             }
1804             pos += ident.length;
1805             return true;
1806         }
1807 
1808         // parse long, ulong or double
1809         void parseNumber(Setting res) {
1810             import std.ascii : isDigit;
1811             char ch = peek;
1812             int sign = 1;
1813             if (ch == '-') {
1814                 sign = -1;
1815                 ch = nextChar;
1816             }
1817             if (!isDigit(ch))
1818                 error("cannot parse number");
1819             ulong n = 0;
1820             while (isDigit(ch)) {
1821                 n = n * 10 + (ch - '0');
1822                 ch = nextChar;
1823             }
1824             if (ch == '.' || ch == 'e' || ch == 'E') {
1825                 // floating
1826                 ulong n2 = 0;
1827                 ulong n2_div = 1;
1828                 if (ch == '.') {
1829                     ch = nextChar;
1830                     while(isDigit(ch)) {
1831                         n2 = n2 * 10 + (ch - '0');
1832                         n2_div *= 10;
1833                         ch = nextChar;
1834                     }
1835                     if (isAlpha(ch) && ch != 'e' && ch != 'E')
1836                         error("error while parsing number");
1837                 }
1838                 int shift = 0;
1839                 int shiftSign = 1;
1840                 if (ch == 'e' || ch == 'E') {
1841                     ch = nextChar;
1842                     if (ch == '-') {
1843                         shiftSign = -1;
1844                         ch = nextChar;
1845                     }
1846                     if (!isDigit(ch))
1847                         error("error while parsing number");
1848                     while(isDigit(ch)) {
1849                         shift = shift * 10 + (ch - '0');
1850                         ch = nextChar;
1851                     }
1852                 }
1853                 if (isAlpha(ch))
1854                     error("error while parsing number");
1855                 double v = cast(double)n;
1856                 if (n2) // part after period
1857                     v += cast(double)n2 / n2_div;
1858                 if (sign < 0)
1859                     v = -v;
1860                 if (shift) { // E part - pow10
1861                     double p = pow(10.0, shift);
1862                     if (shiftSign > 0)
1863                         v *= p;
1864                     else
1865                         v /= p;
1866                 }
1867                 res.floating = v;
1868             } else {
1869                 // integer
1870                 if (isAlpha(ch))
1871                     error("cannot parse number");
1872                 if (sign < 0 || !(n & 0x8000000000000000L))
1873                     res.integer = cast(long)(n * sign); // signed
1874                 else
1875                     res.uinteger = n; // unsigned
1876             }
1877         }
1878     }
1879 
1880     private void parseMap(ref JsonParser parser) {
1881         clear(SettingType.OBJECT);
1882         int startPos = parser.pos;
1883         //Log.v("parseMap at context ", parser.currentContext);
1884         char ch = parser.peek;
1885         parser.nextChar; // skip initial {
1886         if (ch != '{') {
1887             Log.e("expected { at ", parser.currentContext);
1888         }
1889         for(;;) {
1890             ch = parser.skipSpaces;
1891             if (ch == '}') {
1892                 parser.nextChar;
1893                 break;
1894             }
1895             string key = parser.parseIdent;
1896             ch = parser.skipSpaces;
1897             if (ch != ':')
1898                 parser.error("no : char after object field name");
1899             parser.nextChar;
1900             this[key] = (new Setting()).parseJSON(parser);
1901             //Log.v("context before skipSpaces: ", parser.currentContext);
1902             ch = parser.skipSpaces;
1903             //Log.v("context after skipSpaces: ", parser.currentContext);
1904             if (ch == ',') {
1905                 parser.nextChar;
1906                 parser.skipSpaces;
1907             } else if (ch != '}') {
1908                 parser.error("unexpected character when waiting for , or } while parsing object; { position is "~ to!string(startPos));
1909             }
1910         }
1911     }
1912 
1913     private void parseArray(ref JsonParser parser) {
1914         clear(SettingType.ARRAY);
1915         parser.nextChar; // skip initial [
1916         for(;;) {
1917             char ch = parser.skipSpaces;
1918             if (ch == ']') {
1919                 parser.nextChar;
1920                 break;
1921             }
1922             Setting value = new Setting();
1923             value.parseJSON(parser);
1924             this[_store.array.length] = value;
1925             ch = parser.skipSpaces;
1926             if (ch == ',') {
1927                 parser.nextChar;
1928                 parser.skipSpaces;
1929             } else if (ch != ']') {
1930                 parser.error("unexpected character when waiting for , or ] while parsing array");
1931             }
1932         }
1933     }
1934 
1935     private Setting parseJSON(ref JsonParser parser) {
1936         static import std.ascii;
1937         char ch = parser.skipSpaces;
1938         if (ch == '\"') {
1939             this = parser.parseString;
1940         } else if (ch == '[') {
1941             parseArray(parser);
1942         } else if (ch == '{') {
1943             parseMap(parser);
1944         } else if (parser.parseKeyword("null")) {
1945             // do nothing - we already have NULL value
1946         } else if (parser.parseKeyword("true")) {
1947             this = true;
1948         } else if (parser.parseKeyword("false")) {
1949             this = false;
1950         } else if (ch == '-' || std.ascii.isDigit(ch)) {
1951             parser.parseNumber(this);
1952         } else {
1953             parser.error("cannot parse JSON value");
1954         }
1955         return this;
1956     }
1957 
1958     void parseJSON(string s) {
1959         clear(SettingType.NULL);
1960         JsonParser parser;
1961         parser.initialize(convertEols(s), false);
1962         parseJSON(parser);
1963     }
1964 
1965     /// SDL identifiers to be converted to JSON array (name should be changed, with 's' suffix)
1966     private static immutable (string[]) identsToConvertToArrays = [
1967         "subPackage", // in JSON it's subPackages
1968         "configuration", // in JSON it's configurations
1969         "buildType", // in JSON it's buildTypes
1970     ];
1971 
1972     /// SDL identifiers to be converted to JSON object (name should be changed, with 's' suffix)
1973     private static immutable (string[]) identsToConvertToObjects = [
1974         "dependency", // in JSON it's dependencies
1975         "subConfiguration", // in JSON it's subConfigurations
1976     ];
1977 
1978     /// SDL identifiers of JSON array w/o name conversion
1979     private static immutable (string[]) arrayIdents = [
1980         "authors",
1981         "x:ddoxFilterArgs",
1982         "sourcePaths",
1983         "importPaths",
1984         "buildOptions",
1985         "libs",
1986         "sourceFiles",
1987         "buildRequirements",
1988         "excludedSourceFiles",
1989         "copyFiles",
1990         "versions",
1991         "debugVersions",
1992         "stringImportPaths",
1993         "preGenerateCommands",
1994         "postGenerateCommands",
1995         "preBuildCommands",
1996         "postBuildCommands",
1997         "dflags",
1998         "lflags",
1999         "platforms",
2000     ];
2001 
2002     protected bool isArrayItemNameIdent(string ident) {
2003         foreach(s; identsToConvertToArrays) {
2004             if (ident == s)
2005                 return true;
2006         }
2007         return false;
2008     }
2009 
2010     protected bool isObjectItemNameIdent(string ident) {
2011         foreach(s; identsToConvertToObjects) {
2012             if (ident == s)
2013                 return true;
2014         }
2015         return false;
2016     }
2017 
2018     protected bool isArrayIdent(string ident) {
2019         foreach(s; arrayIdents) {
2020             if (ident == s)
2021                 return true;
2022         }
2023         return false;
2024     }
2025 
2026     private void skipEol(ref JsonParser parser) {
2027         char ch = parser.skipSpaces;
2028         if (ch == 0)
2029             return;
2030         if (ch == '\n') {
2031             parser.nextChar;
2032             return;
2033         }
2034         parser.error("end of line expected");
2035     }
2036 
2037     private void parseSDLAttributes(ref JsonParser parser, bool ignorePlatformAttribute = true) {
2038         string attrName;
2039         Setting attrValue;
2040         for (;;) {
2041             char ch = parser.skipSpaces;
2042             if (ch == 0)
2043                 return;
2044             if (ch == '\n') {
2045                 parser.nextChar;
2046                 return;
2047             }
2048             if (!JsonParser.isAlpha(ch))
2049                 parser.error("attr=value expected");
2050             attrName = parser.parseIdent();
2051             attrValue = new Setting();
2052             ch = parser.skipSpaces;
2053             if (ch != '=')
2054                 parser.error("= expected after " ~ attrName);
2055             ch = parser.nextChar; // skip '='
2056             ch = parser.skipSpaces;
2057             if (ch == '\"' || ch == '`') {
2058                 // string value
2059                 string v = parser.parseString;
2060                 attrValue = v;
2061                 if (!ignorePlatformAttribute || attrName != "platform")
2062                     this[attrName] = attrValue;
2063                 continue;
2064             }
2065             if (JsonParser.isAlpha(ch)) {
2066                 string v = parser.parseIdent;
2067                 if (v == "true" || v == "on") {
2068                     attrValue = true;
2069                     this[attrName] = attrValue;
2070                     continue;
2071                 }
2072                 if (v == "false" || v == "off") {
2073                     attrValue = false;
2074                     this[attrName] = attrValue;
2075                     continue;
2076                 }
2077                 parser.error("unexpected attribue value " ~ v);
2078             }
2079             parser.error("only string and boolean values supported for SDL attributes now");
2080         }
2081     }
2082 
2083     // peek platform="value" from current line
2084     private string peekSDLPlatformAttribute(ref JsonParser parser) {
2085         string res = null;
2086         int oldpos = parser.pos; // save position
2087         for(;;) {
2088             char ch = parser.skipSpaces;
2089             if (ch == 0 || ch == '\n' || ch == '{' || ch == '}')
2090                 break;
2091             if (parser.isAlpha(ch)) {
2092                 string ident = parser.parseIdent;
2093                 ch = parser.skipSpaces;
2094                 if (ch != '=')
2095                     continue;
2096                 parser.nextChar;
2097                 ch = parser.skipSpaces;
2098                 string attrvalue;
2099                 if (ch == '\"' || ch == '`')
2100                     attrvalue = parser.parseString;
2101                 else if (parser.isAlpha(ch))
2102                     attrvalue = parser.parseIdent;
2103                 if (ident == "platform") {
2104                     res = attrvalue;
2105                     break;
2106                 }
2107             } else if (ch == '\"' || ch == '`') {
2108                 string str = parser.parseString;
2109             } else if (ch == '=') {
2110                 parser.nextChar;
2111                 continue;
2112             } else {
2113                 break;
2114             }
2115         }
2116         parser.pos = oldpos; // restore position
2117         return res;
2118     }
2119 
2120     private void skipPlatformAttribute(ref JsonParser parser) {
2121         char ch = parser.skipSpaces;
2122         int oldpos = parser.pos;
2123         if (parser.isAlpha(ch)) {
2124             string attrName = parser.parseIdent;
2125             if (attrName == "platform") {
2126                 ch = parser.skipSpaces;
2127                 if (ch == '=') {
2128                     parser.nextChar;
2129                     ch = parser.skipSpaces;
2130                     string value = parser.parseString;
2131                     return; // skipped platform attribute
2132                 }
2133             }
2134         }
2135         // no changes
2136         parser.pos = oldpos;
2137     }
2138 
2139     private Setting parseSDL(ref JsonParser parser, bool insideCurly = false) {
2140         //static import std.ascii;
2141         for (;;) {
2142             // looking for ident
2143             char ch = parser.skipSpaces;
2144             if (ch == 0)
2145                 break;
2146             if (ch == '\n') {
2147                 parser.nextChar; // skip
2148                 continue;
2149             }
2150             if (ch == '}') {
2151                 if (!insideCurly)
2152                     parser.error("unexpected }");
2153                 parser.nextChar; // skip
2154                 return this;
2155             }
2156             string ident = parser.parseIdent();
2157             if (!ident.length)
2158                 parser.error("identifier expected");
2159             ch = parser.skipSpaces;
2160             string platform = peekSDLPlatformAttribute(parser);
2161             bool isArrayConvName = isArrayItemNameIdent(ident);
2162             bool isObjectConvName= isObjectItemNameIdent(ident);
2163             bool isArrayName = isArrayIdent(ident) || isArrayConvName;
2164             if (isArrayConvName || isObjectConvName) {
2165                 import std.algorithm : endsWith;
2166                 if (ident.endsWith("y"))
2167                     ident = ident[0 .. $-1] ~ "ies"; // e.g. dependency->dependencies
2168                 else if (!ident.endsWith("s"))
2169                     ident = ident ~ "s"; // a.g. author->authors
2170             }
2171             if (platform.length)
2172                 ident = ident ~ "-" ~ platform;
2173             Setting valueObj = this[ident]; // looking for existing object
2174             if (!valueObj) { // create if not exist
2175                 valueObj = new Setting();
2176                 this[ident] = valueObj;
2177             }
2178             if (isArrayName) {
2179                 if (!valueObj.isArray) {
2180                     // convert to array
2181                     valueObj.clear(SettingType.ARRAY);
2182                 }
2183             }
2184             // now we have identifier
2185             if (ch == '\"' || ch == '`') {
2186                 string value = parser.parseString;
2187                 skipPlatformAttribute(parser);
2188                 ch = parser.skipSpaces;
2189                 if (ch == '{') {
2190                     /*
2191                         ident "name" {
2192                             //...
2193                         }
2194                     */
2195                     parser.nextChar; // skip {
2196                     Setting obj = isArrayName ? new Setting() : valueObj;
2197                     obj["name"] = value;
2198                     obj.parseSDL(parser, true);
2199                     if (isArrayName)
2200                         valueObj.array = valueObj.array ~ obj;
2201                     continue;
2202                 }
2203                 if (JsonParser.isAlpha(ch)) {
2204                     // ident=value pairs after "name"
2205                     Setting obj = (isArrayName || isObjectConvName) ? new Setting() : valueObj;
2206                     if (!isObjectConvName)
2207                         obj["name"] = value;
2208                     obj.parseSDLAttributes(parser);
2209                     if (isArrayName)
2210                         valueObj.array = valueObj.array ~ obj;
2211                     else if (isObjectConvName)
2212                         valueObj[value] = obj;
2213                     continue;
2214                 }
2215                 if (isArrayName) {
2216                     Setting[] values = valueObj.array;
2217                     Setting svalue = new Setting();
2218                     svalue = value;
2219                     values ~= svalue;
2220                     for (;;) {
2221                         skipPlatformAttribute(parser);
2222                         ch = parser.skipSpaces;
2223                         if (ch == '\n' || ch == 0)
2224                             break;
2225                         if (ch == '\"' || ch == '`') {
2226                             value = parser.parseString;
2227                             svalue = new Setting();
2228                             svalue = value;
2229                             values ~= svalue;
2230                         } else
2231                             parser.error("array of strings expected");
2232                     }
2233                     valueObj.array = values;
2234                 } else {
2235                     if (isObjectConvName) {
2236                         string svalue = parser.parseString;
2237                         valueObj[value] = svalue;
2238                     } else {
2239                         valueObj = value;
2240                     }
2241                 }
2242                 skipPlatformAttribute(parser);
2243                 skipEol(parser);
2244                 continue;
2245             } else if (ch == '{') {
2246                 // object
2247                 parser.nextChar; // skip {
2248                 if (isArrayName) {
2249                     Setting[] values = valueObj.array;
2250                     Setting item = new Setting();
2251                     item.clear(SettingType.OBJECT);
2252                     item.parseSDL(parser, true);
2253                     values ~= item;
2254                     valueObj.array = values;
2255                 } else {
2256                     valueObj.parseSDL(parser, true);
2257                 }
2258                 continue;
2259             } else {
2260                 parser.error("cannot parse SDL value");
2261             }
2262         }
2263         if (insideCurly)
2264             parser.error("} expected");
2265         return this;
2266     }
2267 
2268     void parseSDL(string s) {
2269         clear(SettingType.NULL);
2270         JsonParser parser;
2271         parser.initialize(convertEols(s), true);
2272         parseSDL(parser);
2273     }
2274 
2275     /// convert CR LF, LF CR, LF, CR to '\n' eol format
2276     static string convertEols(string src) {
2277         char[] res;
2278         res.assumeSafeAppend;
2279         for (int i = 0; i < src.length; i++) {
2280             char ch = src[i];
2281             if (ch == '\r' || ch == '\n') {
2282                 char nextch = i + 1 < src.length ? src[i + 1] : 0;
2283                 if (nextch != ch && (nextch == '\r' || nextch == '\n')) {
2284                     // pair \r\n or \n\r
2285                     res ~= '\n';
2286                     i++;
2287                 } else {
2288                     // single \r or \n
2289                     res ~= '\n';
2290                 }
2291             } else {
2292                 res ~= ch;
2293             }
2294         }
2295         return res.dup;
2296     }
2297 
2298     /// load from file; autodetect SDL format using ".sdl" and ".SDL" extension mask; returns true if loaded successfully
2299     bool load(string filename) {
2300         try {
2301             import std.algorithm : endsWith;
2302             string s = readText(filename);
2303             if (filename.endsWith(".sdl") || filename.endsWith(".SDL"))
2304                 parseSDL(s);
2305             else
2306                 parseJSON(s);
2307             return true;
2308         } catch (Exception e) {
2309             // Failed
2310             Log.e("exception while parsing json: ", e);
2311             return false;
2312         }
2313     }
2314 }