1 /*******************************************************************************
2  * D Documentation Generator
3  * Copyright: © 2014 Economic Modeling Specialists, Intl., Ferdinand Majerech
4  *            © 2021 Eugene Stulin
5  * Authors: Ferdinand Majerech, Eugene Stulin
6  * License: $(LINK2 http://www.boost.org/LICENSE_1_0.txt Boost License 1.0)
7  */
8 
9 
10 /// Config loading and writing.
11 module config;
12 
13 
14 import std.algorithm;
15 import std.array;
16 import std.conv: to;
17 import std.stdio;
18 import std.string;
19 
20 
21 /*******************************************************************************
22  * Stores configuration data loaded from command-line or config files.
23  *
24  * Note that multiple calls to loadCLI/loadConfigFile are supported;
25  * data loaded with earlier calls is overwritten by later calls
26  * (e.g. command-line overriding config file),
27  * except arrays like macroFileNames/excludes/sourcePaths:
28  * successive calls always add to these arrays instead of overwriting them,
29  * so e.g. extra modules can be excluded with command-line.
30  */
31 struct Config {
32     bool doHelp = false;
33     bool showVersion = false;
34     bool performance = false;
35     bool doGenerateConfig = false;
36     bool doGenerateConfigForLinux = false;
37     string doGenerateCSSPath = null;
38     string[] macroFileNames = [];
39     string indexFileName = null;
40     string[] tocAdditionalFileNames = [];
41     string[] tocAdditionalStrings = [];
42     string cssFileName = null;
43     string outputDirectory = "./doc";
44     string format = "html-aggregated";
45     string projectName = null;
46     bool noMarkdown = false;
47     string projectVersion = null;
48     uint maxFileSizeK = 16384;
49     uint maxModuleListLength = 256;
50     /// Names of packages and modules to exclude from generated documentation.
51     string[] excludes = [];
52     string[] sourcePaths = [];
53 
54     /// Loaded from macroFileNames + default macros;
55     /// not set on the command-line.
56     string[string] macros;
57 
58     /***************************************************************************
59      * Load config options from CLI arguments.
60      *
61      * Params:
62      * cliArgs = Command-line args.
63      */
64     void loadCLI(string[] cliArgs) {
65         import std.getopt;
66 
67         // If the user requests a config file,
68         // we must look for that option  first and process it
69         // before other options so the config file doesn't override CLI options
70         // (it would override them if loaded after processing the CLI options).
71         string configFile;
72         string[] newMacroFiles;
73         string[] newExcludes;
74         try {
75             // -h/--help will not pass through due
76             // to the autogenerated help option
77             auto firstResult = getopt(
78                 cliArgs,
79                 std.getopt.config.caseSensitive,
80                 std.getopt.config.passThrough,
81                 "config|F",
82                 &configFile
83             );
84             doHelp = firstResult.helpWanted;
85             if (configFile !is null) {
86                 loadConfigFile(configFile, true);
87             }
88 
89             auto getoptResult = getopt(
90                 cliArgs, std.getopt.config.caseSensitive,
91                 "css|c",                    &cssFileName,
92                 "generate-css|C",           &doGenerateCSSPath,
93                 "exclude|e",                &newExcludes,
94                 "format|f",                 &format,
95                 "generate-cfg|g",           &doGenerateConfig,
96                 "generate-cfg-linux",       &doGenerateConfigForLinux,
97                 "index|i",                  &indexFileName,
98                 "macros|m",                 &newMacroFiles,
99                 "max-file-size|M",          &maxFileSizeK,
100                 "output-directory|o",       &outputDirectory,
101                 "project-name|p",           &projectName,
102                 "project-version|n",        &projectVersion,
103                 "no-markdown|D",            &noMarkdown,
104                 "toc-additional|t",         &tocAdditionalFileNames,
105                 "toc-additional-direct|T",  &tocAdditionalStrings,
106                 "max-module-list-length|l", &maxModuleListLength,
107                 "version",                  &showVersion,
108                 "performance",              &performance   
109             );
110         } catch(Exception e) {
111             writeln("Failed to parse command-line arguments: ", e.msg);
112             writeln("Maybe try 'hgen -h' for help information?");
113             return;
114         }
115 
116         macroFileNames  ~= newMacroFiles;
117         excludes        ~= newExcludes;
118         sourcePaths     ~= cliArgs[1 .. $];
119     }
120 
121     /***************************************************************************
122      * Load specified config file and add loaded data to the configuration.
123      *
124      * Params:
125      *
126      * fileName        = Name of the config file.
127      * requestedByUser = If true, this is not the default config file and
128      *                   has been explicitly requested by the user, i.e. we have
129      *                   to inform the user if the file was not found.
130      *
131      */
132     void loadConfigFile(string fileName, bool requestedByUser = false) {
133         import std.file: exists, isFile;
134         import std.typecons: tuple;
135 
136         if (!fileName.exists || !fileName.isFile) {
137             if (requestedByUser) {
138                 writefln("Config file '%s' not found", fileName);
139             }
140             return;
141         }
142 
143         writefln("Loading config file '%s'", fileName);
144         try {
145             auto keyValues =
146                 File(fileName)
147                 .byLine
148                 .map!(l => l.until!(c => ";#".canFind(c)))
149                 .map!array
150                 .map!strip
151                 .filter!(s => !s.empty && s.canFind("="))
152                 .map!(l => l.findSplit("="))
153                 .map!(p => tuple(p[0].strip.to!string, p[2].strip.to!string))
154                 .filter!(p => !p[0].empty);
155 
156             foreach (key, value; keyValues) {
157                 processConfigValue(key, value);
158             }
159         } catch(Exception e) {
160             writefln("Failed to parse config file '%s': %s", fileName, e.msg);
161         }
162     }
163 
164 private:
165     void processConfigValue(string key, string value) {
166         // ensures something like "macros = " won't add an empty string value
167         void add(ref string[] array, string value) {
168             if (!value.empty) {
169                 array ~= value;
170             }
171         }
172 
173         switch(key) {
174             case "help":          doHelp = value.to!bool;           break;
175             case "generate-cfg":  doGenerateConfig = value.to!bool; break;
176             case "generate-css":  doGenerateCSSPath = value;        break;
177             case "macros":        add(macroFileNames, value);       break;
178             case "max-file-size": maxFileSizeK = value.to!uint;     break;
179             case "max-module-list-length":
180                 maxModuleListLength = value.to!uint;            break;
181             case "project-name":    projectName = value;        break;
182             case "project-version": projectVersion = value;     break;
183             case "no-markdown":     noMarkdown = value.to!bool; break;
184             case "index":           indexFileName = value;      break;
185             case "toc-additional":
186                 if (value !is null) tocAdditionalFileNames ~= value;   break;
187             case "toc-additional-direct":
188                 if (value !is null) tocAdditionalStrings ~= value;     break;
189             case "css":              cssFileName = value;              break;
190             case "output-directory": outputDirectory = value;          break;
191             case "exclude":          add(excludes, value);             break;
192             case "config": if (value) loadConfigFile(value, true);     break;
193             case "source": add(sourcePaths, value);                    break;
194             default: writefln("Unknown key in config file: '%s'", key);
195         }
196     }
197 }
198 
199 
200 immutable string[string] helpTranslations;
201 shared static this() {
202     helpTranslations["en"] = import("help").strip;
203     helpTranslations["ru"] = import("help_ru").strip;
204 }
205 
206 immutable string versionString = import("version");
207 immutable string defaultConfigString = import("hgen.cfg");