1 // Copyright © 2017-2021 Rémi Thebault
2 /++
3  +  Wayland scanner for D.
4  +  Generation of common protocol code.
5  +/
6 module wayland.scanner.common;
7 
8 import wayland.scanner;
9 
10 import arsd.dom;
11 
12 import std.algorithm;
13 import std.array;
14 import std.conv;
15 import std.exception;
16 import std.format;
17 import std.range;
18 import std.stdio;
19 import std.typecons;
20 import std.uni;
21 
22 interface Factory
23 {
24     Protocol makeProtocol(Element el);
25     Interface makeInterface(Element el, Protocol protocol);
26     Message makeMessage(Element el, Interface iface);
27     Arg makeArg(Element el);
28 }
29 
30 Factory fact;
31 
32 
33 void writeMultilineParenthesis(SourceFile sf, in string before,
34             in string[] args, in string after)
35 {
36     immutable indent = ' '.repeat(before.length+1).array();
37     sf.write("%s(", before);
38     foreach (i, a; args)
39     {
40         if (i != 0)
41         {
42             sf.writeln(",");
43             sf.write(indent);
44         }
45         sf.write(a);
46     }
47     sf.writeln(")%s", after);
48 }
49 
50 void writeDelegateAlias(SourceFile sf, string name, string ret, string[] rtArgs)
51 {
52     immutable fstLine = format("alias %s = %s delegate(", name, ret);
53     immutable indent = ' '.repeat(fstLine.length).array();
54     sf.write(fstLine);
55     foreach(i, rta; rtArgs)
56     {
57         if (i != 0)
58         {
59             sf.writeln(",");
60             sf.write(indent);
61         }
62         sf.write(rta);
63     }
64     sf.writeln(");");
65 }
66 
67 void writeFnSigRaw(SourceFile sf, string qualifiers, string ret, string name,
68                 string[] rtArgs, string[] ctArgs=[], string constraint="")
69 {
70     immutable ctSig = ctArgs.length ? format("!(%(%s, %))", ctArgs) : "";
71     immutable qual = qualifiers.length ? format("%s ", qualifiers) : "";
72     immutable fstLine = format("%s%s %s%s(", qual, ret, name, ctSig);
73     immutable indent = ' '.repeat(fstLine.length).array();
74     sf.write(fstLine);
75     foreach (i, rta; rtArgs)
76     {
77         if (i != 0)
78         {
79             sf.writeln(",");
80             sf.write(indent);
81         }
82         sf.write(rta);
83     }
84     sf.write(")");
85     if (constraint.length)
86     {
87         sf.write("if (%s)", constraint);
88     }
89 }
90 
91 void writeFnSig(SourceFile sf, string ret, string name, string[] rtArgs,
92                 string[] ctArgs=[], string constraint="")
93 {
94     writeFnSigRaw(sf, "", ret, name, rtArgs, ctArgs, constraint);
95     sf.writeln();
96 }
97 
98 void writeFnExpr(SourceFile sf, string stmt, string[] exprs)
99 {
100     sf.writeln("%s(", stmt);
101     sf.indentedBlock!({
102         immutable ic = sf.indentChars;
103         int width = ic;
104         foreach (i, e; exprs)
105         {
106             if (i != 0)
107             {
108                 sf.write(",");
109                 width += 1;
110             }
111             width += e.length;
112             if (width > wrapWidth)
113             {
114                 sf.writeln();
115                 width = ic;
116             }
117             else if (i != 0)
118             {
119                 sf.write(" ");
120                 width += 1;
121             }
122             sf.write(e);
123         }
124     });
125     sf.writeln();
126     sf.writeln(");");
127 }
128 
129 void writeFnPointer(SourceFile sf, string name, string ret, string[] args)
130 {
131     writeMultilineParenthesis(
132         sf, format("%s function", ret), args, format(" %s;", name)
133     );
134 }
135 
136 void writeFnBody(SourceFile sf, string[] preStmts, string mainStmt,
137                 string[] exprs, string[] postStmt)
138 {
139     sf.bracedBlock!({
140         foreach (ps; preStmts)
141         {
142             sf.writeln(ps);
143         }
144         writeFnExpr(sf, mainStmt, exprs);
145         foreach (ps; postStmt)
146         {
147             sf.writeln(ps);
148         }
149     });
150 }
151 
152 enum ArgType
153 {
154     Int, UInt, Fixed, String, Object, NewId, Array, Fd
155 }
156 
157 @property bool isNullable(in ArgType at) pure
158 {
159     switch (at)
160     {
161     case ArgType.String:
162     case ArgType.Object:
163     case ArgType.NewId:
164     case ArgType.Array:
165         return true;
166     default:
167         return false;
168     }
169 }
170 
171 
172 class Description
173 {
174     string summary;
175     string text;
176 
177     this (Element parentEl)
178     {
179         foreach (el; parentEl.getElementsByTagName("description"))
180         {
181             summary = el.getAttribute("summary");
182             text = el.getElText();
183             break;
184         }
185     }
186 
187     @property bool empty() const
188     {
189         return summary.empty && text.empty;
190     }
191 
192     void writeCode(SourceFile sf)
193     {
194         if (empty) return;
195         if (text.empty)
196         {
197             enforce(!summary.canFind('\n'), "Summary should be a 1 liner.");
198             sf.writeln("/// %s", summary);
199         }
200         else if (summary.empty)
201         {
202             sf.writeDoc("%s", text);
203         }
204         else
205         {
206             sf.writeDoc("%s\n\n%s", summary, text);
207         }
208     }
209 }
210 
211 
212 class EnumEntry
213 {
214     string ifaceName;
215     string enumName;
216     string name;
217     string value;
218     string summary;
219 
220     this (Element el, string ifaceName, string enumName)
221     {
222         assert(el.tagName == "entry");
223         this.ifaceName = ifaceName;
224         this.enumName = enumName;
225         name = el.getAttribute("name");
226         value = el.getAttribute("value");
227         summary = el.getAttribute("summary");
228 
229         enforce(!value.empty, "enum entries without value aren't supported");
230     }
231 
232     void writeCode(SourceFile sf)
233     {
234         if (summary.length)
235         {
236             sf.writeln("/// %s", summary);
237         }
238         sf.writeln(
239             "%s = %s,", validDName(camelName(name)), value
240         );
241     }
242 }
243 
244 
245 class Enum
246 {
247     string name;
248     string ifaceName;
249     Description description;
250     EnumEntry[] entries;
251 
252     this (Element el, string ifaceName)
253     {
254         assert(el.tagName == "enum");
255         name = el.getAttribute("name");
256         this.ifaceName = ifaceName;
257         description = new Description(el);
258         foreach (entryEl; el.getElementsByTagName("entry"))
259         {
260             entries ~= new EnumEntry(entryEl, ifaceName, name);
261         }
262     }
263 
264     @property dName() const
265     {
266         return titleCamelName(name);
267     }
268 
269     bool entriesHaveDoc() const
270     {
271         return entries.any!(e => !e.summary.empty);
272     }
273 
274     void writeCode(SourceFile sf)
275     {
276         description.writeCode(sf);
277         sf.writeln("enum %s : uint", dName);
278         sf.bracedBlock!({
279             foreach(entry; entries)
280             {
281                 entry.writeCode(sf);
282             }
283         });
284     }
285 }
286 
287 
288 
289 abstract class Arg
290 {
291     string name;
292     string summary;
293     string iface;
294     ArgType type;
295     string enumName;
296     bool nullable;
297 
298     this (Element el)
299     {
300         assert(el.tagName == "arg");
301         name = el.getAttribute("name");
302         summary = el.getAttribute("summary");
303         iface = el.getAttribute("interface");
304         enumName = el.getAttribute("enum");
305         switch (el.getAttribute("type"))
306         {
307             case "int":
308                 type = ArgType.Int;
309                 break;
310             case "uint":
311                 type = ArgType.UInt;
312                 break;
313             case "fixed":
314                 type = ArgType.Fixed;
315                 break;
316             case "string":
317                 type = ArgType.String;
318                 break;
319             case "object":
320                 type = ArgType.Object;
321                 break;
322             case "new_id":
323                 type = ArgType.NewId;
324                 break;
325             case "array":
326                 type = ArgType.Array;
327                 break;
328             case "fd":
329                 type = ArgType.Fd;
330                 break;
331             default:
332                 throw new Exception("unknown type: "~el.getAttribute("type"));
333         }
334         immutable allowNull = el.getAttribute("allow-null");
335         enforce (!allowNull.length || (allowNull == "true" || allowNull == "false"));
336         nullable = allowNull == "true";
337         enforce(!nullable || isNullable(type));
338     }
339 
340     abstract @property string dType() const
341     {
342         final switch(type) {
343             case ArgType.Int:
344                 return "int";
345             case ArgType.UInt:
346                 if (enumName.empty) return "uint";
347                 else return qualfiedTypeName(enumName);
348             case ArgType.Fixed:
349                 return "WlFixed";
350             case ArgType.String:
351                 return "string";
352             case ArgType.NewId:
353                 assert(false, "must be implemented in subclasses");
354             case ArgType.Array:
355                 return "wl_array*"; // ?? let's check this later
356             case ArgType.Fd:
357                 return "int";
358             case ArgType.Object:
359                 assert(false, "must be implemented in subclasses");
360         }
361     }
362 
363     abstract @property string cType() const
364     {
365         final switch(type) {
366             case ArgType.Int:
367                 return "int";
368             case ArgType.UInt:
369                 return "uint";
370             case ArgType.Fixed:
371                 return "wl_fixed_t";
372             case ArgType.String:
373                 return "const(char)*";
374             case ArgType.Object:
375                 assert(false, "unimplemented");
376             case ArgType.NewId:
377                 assert(false, "must be implemented in subclasses");
378             case ArgType.Array:
379                 return "wl_array*";
380             case ArgType.Fd:
381                 return "int";
382         }
383     }
384 
385 
386     @property string paramName() const
387     {
388         return validDName(camelName(name));
389     }
390 
391     @property string cCastExpr() const
392     {
393         final switch(type) {
394             case ArgType.Int:
395                 return paramName;
396             case ArgType.UInt:
397                 return paramName;
398             case ArgType.Fixed:
399                 return format("%s.raw", paramName);
400             case ArgType.String:
401                 return format("toStringz(%s)", paramName);
402             case ArgType.NewId:
403                 return paramName;
404             case ArgType.Array:
405                 return paramName;
406             case ArgType.Fd:
407                 return paramName;
408             case ArgType.Object:
409                 assert(false, "must be implemented in subclasses");
410         }
411     }
412 
413     string dCastExpr(string parentIface) const
414     {
415         final switch(type) {
416             case ArgType.Int:
417                 return paramName;
418             case ArgType.UInt:
419                 if (enumName.empty) return paramName;
420                 else
421                 {
422                     immutable en = (parentIface.empty || enumName.canFind('.')) ?
423                             enumName : format("%s.%s", parentIface, enumName);
424                     return format("cast(%s)%s", qualfiedTypeName(en), paramName);
425                 }
426             case ArgType.Fixed:
427                 return format("WlFixed(%s)", paramName);
428             case ArgType.String:
429                 return format("fromStringz(%s).idup", paramName);
430             case ArgType.Object:
431                 assert(false, "unimplemented");
432             case ArgType.NewId:
433                 return paramName;
434             case ArgType.Array:
435                 return paramName;
436             case ArgType.Fd:
437                 return paramName;
438         }
439     }
440 }
441 
442 abstract class Message
443 {
444     enum Type
445     {
446         event,
447         request,
448     }
449 
450     enum ReqType
451     {
452         void_,
453         newObj,
454         dynObj,
455     }
456 
457     string name;
458     Interface iface;
459     int since = 1;
460     bool isDtor;
461     Description description;
462     Arg[] args;
463 
464     string[] argIfaceTypes;
465     size_t ifaceTypeIndex;
466     Type type;
467 
468     ReqType reqType;
469     Arg reqRet;
470 
471     this (Element el, Interface iface)
472     {
473         assert(el.tagName == "request" || el.tagName == "event");
474         this.iface = iface;
475         name = el.getAttribute("name");
476         if (el.hasAttribute("since"))
477         {
478             since = el.getAttribute("since").to!int;
479         }
480         isDtor = (el.getAttribute("type") == "destructor");
481         description = new Description(el);
482         foreach (argEl; el.getElementsByTagName("arg"))
483         {
484             args ~= fact.makeArg(argEl);
485         }
486 
487         argIfaceTypes = args
488             .filter!(a => a.type == ArgType.NewId || a.type == ArgType.Object)
489             .map!(a => a.iface)
490             .filter!(iface => iface.length != 0)
491             .array();
492 
493         type = el.tagName == "event" ? Type.event : Type.request;
494 
495         if (type == Type.request)
496         {
497             auto crr = reqReturn;
498             reqRet = crr[0];
499             reqType = crr[1];
500         }
501     }
502 
503     @property string ifaceName()
504     {
505         return iface.name;
506     }
507 
508     @property Tuple!(Arg, ReqType) reqReturn()
509     {
510         Arg ret;
511         ReqType rt;
512         foreach (arg; args)
513         {
514             if (arg.type == ArgType.NewId)
515             {
516                 enforce(!ret, "more than 1 new-id for a request!");
517                 ret = arg;
518                 rt = arg.iface.length ? ReqType.newObj : ReqType.dynObj;
519             }
520         }
521         if (!ret)
522         {
523             rt = ReqType.void_;
524         }
525         return tuple(ret, rt);
526     }
527 
528     @property bool ifaceTypesAllNull() const
529     {
530         return argIfaceTypes.empty;
531     }
532 
533     @property size_t nullIfaceTypeLength() const
534     {
535         return argIfaceTypes.empty ? args.length : 0;
536     }
537 
538     @property string dMethodName()
539     {
540         return validDName(camelName(name));
541     }
542 
543     @property string opCodeName()
544     {
545         return format("%sOpCode", camelName(name));
546     }
547 
548     @property string onOpCodeName()
549     {
550         return format("on%sOpCode", titleCamelName(name));
551     }
552 
553     @property string dHandlerName()
554     {
555         return "on" ~ titleCamelName(name);
556     }
557 
558     @property string dEvDgType()
559     {
560         return "On" ~ titleCamelName(name) ~ "EventDg";
561     }
562 
563     @property string cEvName()
564     {
565         return validDName(name);
566     }
567 
568     @property string signature() const
569     {
570         string sig;
571         if (since > 1)
572         {
573             sig ~= since.to!string;
574         }
575         foreach(arg; args)
576         {
577             if (arg.nullable) sig ~= '?';
578             final switch(arg.type)
579             {
580             case ArgType.Int:
581                 sig ~= 'i';
582                 break;
583             case ArgType.NewId:
584                 if (arg.iface.empty)
585                     sig ~= "su";
586                 sig ~= "n";
587                 break;
588             case ArgType.UInt:
589                 sig ~= "u";
590                 break;
591             case ArgType.Fixed:
592                 sig ~= "f";
593                 break;
594             case ArgType.String:
595                 sig ~= "s";
596                 break;
597             case ArgType.Object:
598                 sig ~= "o";
599                 break;
600             case ArgType.Array:
601                 sig ~= "a";
602                 break;
603             case ArgType.Fd:
604                 sig ~= "h";
605                 break;
606             }
607         }
608         return sig;
609     }
610 
611     void writePrivIfaceMsg(SourceFile sf)
612     {
613         sf.writeln(
614             "wl_message(\"%s\", \"%s\", &msgTypes[%d]),",
615             name, signature, ifaceTypeIndex
616         );
617     }
618 }
619 
620 
621 abstract class Interface
622 {
623     Protocol protocol;
624     string name;
625     string ver;
626     Description description;
627     Message[] requests;
628     Message[] events;
629     Enum[] enums;
630 
631     // indicate that is created by server rather than by client request.
632     // protocal eventually set to false after all interfaces are parsed
633     bool isGlobal = true;
634 
635 
636     private static Interface[string] ifaceMap;
637 
638     static Interface get(string name)
639     {
640         auto i = name in ifaceMap;
641         if (i) return *i;
642         else return null;
643     }
644 
645     this (Element el, Protocol protocol)
646     {
647         assert(el.tagName == "interface");
648         this.protocol = protocol;
649         name = el.getAttribute("name");
650         ver = el.getAttribute("version");
651         description = new Description(el);
652         foreach (rqEl; el.getElementsByTagName("request"))
653         {
654             requests ~= fact.makeMessage(rqEl, this);
655         }
656         foreach (evEl; el.getElementsByTagName("event"))
657         {
658             events ~= fact.makeMessage(evEl, this);
659         }
660         foreach (enEl; el.getElementsByTagName("enum"))
661         {
662             enums ~= new Enum(enEl, name);
663         }
664         ifaceMap[name] = this;
665     }
666 
667     abstract void writeCode(SourceFile sf);
668 
669     @property string dName() const
670     {
671         return ifaceDName(name);
672     }
673 
674     @property bool writeEvents()
675     {
676         return events.length && name != "wl_display";
677     }
678 
679     @property size_t nullIfaceTypeLength()
680     {
681         size_t max =0;
682         foreach (tl; chain(requests, events)
683                 .map!(msg => msg.nullIfaceTypeLength))
684         {
685             if (tl > max) max = tl;
686         }
687         return max;
688     }
689 
690     void writeVersion(SourceFile sf)
691     {
692         sf.writeln("/// Version of %s.%s", protocol.name, name);
693         sf.writeln("enum ver = %s;", ver);
694     }
695 
696     void writePrivIfaceMsgs(SourceFile sf, Message[] msgs, in string suffix)
697     {
698         if (msgs.empty) return;
699 
700         sf.writeln("auto %s_%s = [", name, suffix);
701         sf.indentedBlock!({
702             foreach(msg; msgs)
703             {
704                 msg.writePrivIfaceMsg(sf);
705             }
706         });
707         sf.writeln("];");
708     }
709 
710     void writePrivIfacePopulate(SourceFile sf)
711     {
712         writePrivIfaceMsgs(sf, requests, "requests");
713         writePrivIfaceMsgs(sf, events, "events");
714         immutable memb = format("ifaces[%s]", indexSymbol(name));
715         sf.writeln(`%s.name = "%s";`, memb, name);
716         sf.writeln("%s.version_ = %s;", memb, ver);
717 
718         if (requests.length)
719         {
720             sf.writeln("%s.method_count = %d;", memb, requests.length);
721             sf.writeln("%s.methods = %s_requests.ptr;", memb, name);
722         }
723         if (events.length)
724         {
725             sf.writeln("%s.event_count = %d;", memb, events.length);
726             sf.writeln("%s.events = %s_events.ptr;", memb, name);
727         }
728     }
729 }
730 
731 
732 abstract class Protocol
733 {
734     string name;
735     string copyright;
736     Interface[] ifaces;
737 
738     this(Element el)
739     {
740         enforce(el.tagName == "protocol");
741         name = el.getAttribute("name");
742         foreach (cr; el.getElementsByTagName("copyright"))
743         {
744             copyright = cr.getElText();
745             break;
746         }
747         Interface[string] ifaceMap;
748         foreach (ifEl; el.getElementsByTagName("interface"))
749         {
750             auto iface = fact.makeInterface(ifEl, this);
751             ifaceMap[iface.name] = iface;
752             ifaces ~= iface;
753         }
754 
755         foreach(iface; ifaces)
756         {
757             foreach(req; iface.requests)
758             {
759                 foreach(arg; req.args)
760                 {
761                     if (arg.type == ArgType.NewId)
762                     {
763                         auto ifp = arg.iface in ifaceMap;
764                         if (ifp) ifp.isGlobal = false;
765                     }
766                 }
767             }
768         }
769     }
770 
771     abstract void writeCode(SourceFile sf, in Options opt);
772 
773     abstract void writePrivIfaces(SourceFile sf);
774 
775     abstract void writeNativeIfacesAssignment(SourceFile sf);
776 
777     bool isLocalIface(string name)
778     {
779         return ifaces.map!(iface => iface.name).canFind(name);
780     }
781 
782     void writeHeader(SourceFile sf, in Options opt)
783     {
784         import std.path : baseName;
785         sf.writeDoc(
786             "Module generated by wayland:scanner-%s for %s protocol\n" ~
787             "  xml protocol:   %s\n" ~
788             "  generated code: %s",
789                 scannerVersion, name,
790                 (opt.inFile.length ? baseName(opt.inFile) : "stdin"),
791                 opt.code.to!string
792         );
793         sf.writeln("module %s;", opt.moduleName);
794         sf.writeComment(
795             "Protocol copyright:\n\n%s", copyright
796         );
797         sf.writeComment(
798             "Bindings copyright:\n\n%s", bindingsCopyright
799         );
800     }
801 
802     void writeNativeIfaces(SourceFile sf)
803     {
804         sf.writeln();
805         sf.writeln("immutable wl_interface[] wl_ifaces;");
806         sf.writeln();
807         foreach (i, iface; ifaces)
808         {
809             sf.writeln("enum %s = %d;", indexSymbol(iface.name), i);
810         }
811         sf.writeln();
812         sf.writeln("shared static this()");
813         sf.bracedBlock!({
814             sf.writeln("auto ifaces = new wl_interface[%d];", ifaces.length);
815             sf.writeln();
816             writePrivMsgTypes(sf);
817             sf.writeln();
818             foreach (iface; ifaces)
819             {
820                 iface.writePrivIfacePopulate(sf);
821                 sf.writeln();
822             }
823             sf.writeln("import std.exception : assumeUnique;");
824             sf.writeln("wl_ifaces = assumeUnique(ifaces);");
825             sf.writeln();
826             writeNativeIfacesAssignment(sf);
827         });
828     }
829 
830     void writePrivMsgTypes(SourceFile sf)
831     {
832         size_t max =0;
833         foreach (l; ifaces.map!(iface => iface.nullIfaceTypeLength))
834         {
835             if (l > max) max = l;
836         }
837 
838         immutable nullLength = max;
839         size_t typeIndex = 0;
840 
841         sf.writeln("auto msgTypes = [");
842         sf.indentedBlock!({
843             foreach(i; 0..nullLength)
844             {
845                 sf.writeln("null,");
846             }
847             foreach (iface; ifaces)
848             {
849                 foreach(msg; chain(iface.requests, iface.events))
850                 {
851                     if (msg.ifaceTypesAllNull)
852                     {
853                         msg.ifaceTypeIndex = 0;
854                         continue;
855                     }
856                     msg.ifaceTypeIndex = nullLength + typeIndex;
857                     typeIndex += msg.args.length;
858                     foreach (arg; msg.args)
859                     {
860                         if (!arg.iface.empty &&
861                             (arg.type == ArgType.NewId ||
862                                 arg.type == ArgType.Object))
863                         {
864                             if (isLocalIface(arg.iface))
865                                 sf.writeln("&ifaces[%s],", indexSymbol(arg.iface));
866                             else
867                                 sf.writeln("cast(wl_interface*)%s.iface.native,", ifaceDName(arg.iface));
868                         }
869                         else
870                         {
871                             sf.writeln("null,");
872                         }
873                     }
874 
875                 }
876             }
877         });
878         sf.writeln("];");
879     }
880 }
881 
882 
883 // for private wl_interface array
884 string indexSymbol(in string name) pure
885 {
886     return camelName(name, "index");
887 }
888 
889 string ifaceDName(in string name) pure
890 {
891     return titleCamelName(name);
892 }
893 
894 string validDName(in string name) pure
895 {
896     switch (name)
897     {
898         case "class": return "class_";
899         case "default": return "default_";
900         case "interface": return "iface";
901         case "version": return "version_";
902         default:
903         {
904             if (name[0] >= '0' && name[0] <= '9') return "_" ~ name;
905             else return name;
906         }
907     }
908 }
909 
910 string getElText(Element el)
911 {
912     string fulltxt;
913     foreach (child; el.children)
914     {
915         if (child.nodeType == NodeType.Text)
916         {
917             fulltxt ~= child.nodeValue;
918         }
919     }
920 
921     string[] lines;
922     string offset;
923     bool offsetdone = false;
924     foreach (l; fulltxt.split('\n'))
925     {
926         immutable bool allwhite = l.all!isWhite;
927         if (!offsetdone && allwhite) continue;
928 
929         if (!offsetdone && !allwhite)
930         {
931             offsetdone = true;
932             offset = l
933                     .until!(c => !c.isWhite)
934                     .to!string;
935         }
936 
937         if (l.startsWith(offset))
938         {
939             l = l[offset.length .. $];
940         }
941 
942         lines ~= l;
943     }
944 
945     foreach_reverse(l; lines) {
946         if (l.all!isWhite) {
947             lines = lines[0..$-1];
948         }
949         else break;
950     }
951 
952     return lines.join("\n");
953 }
954 
955 
956 class SourceFile
957 {
958     File _output;
959     int _indentLev = 0;
960     bool _indentNext = true;
961 
962     invariant()
963     {
964         assert(_indentLev >= 0);
965     }
966 
967     this(File output)
968     {
969         _output = output;
970     }
971 
972     @property File output()
973     {
974         return _output;
975     }
976 
977     @property int indentLev() const
978     {
979         return _indentLev;
980     }
981 
982     @property int indentChars() const
983     {
984         return _indentLev * charsPerIndent;
985     }
986 
987     void indent()
988     {
989         _indentLev += 1;
990     }
991 
992     void unindent()
993     {
994         _indentLev -= 1;
995     }
996 
997     void write(Args...)(string codeFmt, Args args)
998     {
999         immutable code = format(codeFmt, args);
1000         immutable iStr = indentStr(_indentLev);
1001         if (_indentNext) _output.write(iStr);
1002         _output.write(code.replace("\n", "\n"~iStr));
1003         _indentNext = false;
1004     }
1005 
1006     void writeln()
1007     {
1008         _output.writeln();
1009         _indentNext = true;
1010     }
1011 
1012     /++
1013     +   writes indented code and adds a final '\n'
1014     +/
1015     void writeln(Args...)(string codeFmt, Args args)
1016     {
1017         immutable code = format(codeFmt, args);
1018         immutable iStr = indentStr(_indentLev);
1019         if (_indentNext) _output.write(iStr);
1020         _output.writeln(code.replace("\n", "\n"~iStr));
1021         _indentNext = true;
1022     }
1023 
1024     void writeComment(Args...)(string textFmt, Args args)
1025     {
1026         immutable text = format(textFmt, args);
1027         immutable indStr = indentStr(_indentLev);
1028         _output.writeln(indStr, "/+");
1029         foreach (l; text.split("\n")) {
1030             if (l.empty) _output.writeln(indStr, " +");
1031             else _output.writeln(indStr, " +  ", l);
1032         }
1033         _output.writeln(indStr, " +/");
1034     }
1035 
1036     void writeDoc(Args...)(string textFmt, Args args)
1037     {
1038         immutable text = format(textFmt, args);
1039         immutable indStr = indentStr(_indentLev);
1040         _output.writeln(indStr, "/++");
1041         foreach (l; text.split("\n")) {
1042             if (l.empty) {
1043                 _output.writeln(indStr, " +");
1044             }
1045             else {
1046                 _output.write(indStr, " +  ");
1047                 foreach (dchar c; l) {
1048                     switch (c) {
1049                     case '(': _output.write("$(LPAREN)"); break;
1050                     case ')': _output.write("$(RPAREN)"); break;
1051                     case '<': _output.write("&lt;"); break;
1052                     case '>': _output.write("&gt;"); break;
1053                     case '&': _output.write("&amp;"); break;
1054                     default: _output.write(c); break;
1055                     }
1056                 }
1057                 _output.writeln();
1058             }
1059         }
1060         _output.writeln(indStr, " +/");
1061     }
1062 }
1063 
1064 
1065 void bracedBlock(alias writeF)(SourceFile sf)
1066 {
1067     sf.writeln("{");
1068     sf.indent();
1069     writeF();
1070     sf.unindent();
1071     sf.writeln("}");
1072 }
1073 
1074 void indentedBlock(alias writeF)(SourceFile sf)
1075 {
1076     sf.indent();
1077     writeF();
1078     sf.unindent();
1079 }
1080 
1081 
1082 /// Build a camel name from components
1083 string buildCamelName(in string comp, in bool tit) pure
1084 {
1085     string name;
1086     bool cap = tit;
1087     foreach(char c; comp.toLower())
1088     {
1089         if (c != '_')
1090         {
1091             name ~= cap ? c.toUpper() : c;
1092             cap = false;
1093         }
1094         else
1095         {
1096             cap = true;
1097         }
1098     }
1099     return name;
1100 }
1101 
1102 string camelName(in string comp) pure
1103 {
1104     return buildCamelName(comp, false);
1105 }
1106 
1107 string titleCamelName(in string comp) pure
1108 {
1109     return buildCamelName(comp, true);
1110 }
1111 
1112 string camelName(in string[] comps...) pure
1113 {
1114     string name = buildCamelName(comps[0], false);
1115     foreach (c; comps[1 .. $])
1116     {
1117         name ~= buildCamelName(c, true);
1118     }
1119     return name;
1120 }
1121 
1122 string titleCamelName(in string[] comps...) pure
1123 {
1124     string name = buildCamelName(comps[0], true);
1125     foreach (c; comps[1 .. $])
1126     {
1127         name ~= buildCamelName(c, true);
1128     }
1129     return name;
1130 }
1131 
1132 enum charsPerIndent = 4;
1133 enum wrapWidth = 80;
1134 
1135 string indentStr(int indent) pure
1136 {
1137     return ' '.repeat(indent * charsPerIndent).array();
1138 }
1139 
1140 string qualfiedTypeName(in string name) pure
1141 {
1142     return name
1143             .splitter(".")
1144             .map!(part => titleCamelName(part))
1145             .join(".");
1146 }
1147 
1148 string splitLinesForWidth(string input, in string suffix, in string indent,
1149                         in size_t width=wrapWidth) pure
1150 {
1151     string res;
1152     size_t w;
1153     foreach(i, word; input.split(" "))
1154     {
1155         if (i != 0)
1156         {
1157             string spacer = " ";
1158             if (w + word.length + suffix.length >= width)
1159             {
1160                 spacer = suffix ~ "\n" ~ indent;
1161                 w = indent.length;
1162             }
1163             res ~= spacer;
1164         }
1165         res ~= word;
1166         w += word.length;
1167     }
1168     return res;
1169 }