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