1 /** Contains common definitions and logic to collect input dependencies.
2 
3 	This module is typically only used by generator implementations.
4 */
5 module diet.input;
6 
7 import diet.traits : DietTraitsAttribute;
8 import diet.internal.string;
9 import memutils.vector;
10 import memutils.scoped;
11 
12 @safe:
13 nothrow:
14 
15 /** Converts a `Group` with alternating file names and contents to an array of
16 	`InputFile`s.
17 */
18 @property InputFile[] filesFromGroup(alias FILES_GROUP)()
19 {
20 	static assert(FILES_GROUP.expand.length % 2 == 0);
21 	auto ret = Array!InputFile(FILES_GROUP.expand.length / 2);
22 	foreach (i, F; FILES_GROUP.expand) {
23 		static if (i % 2 == 0) {
24 			ret[i / 2].name = FILES_GROUP.expand[i+1];
25 			ret[i / 2].contents = FILES_GROUP.expand[i];
26 		}
27 	}
28 	return ret[].copy();
29 }
30 
31 bool canFindFile(T, U)(T baseFiles, U file) {
32 	foreach(baseFile; baseFiles) {
33 		if (baseFile.name[] == file) {
34 			return true;
35 		}
36 	}
37 	return false;
38 }
39 /** Using the file name of a string import Diet file, returns a list of all
40 	required files.
41 
42 	These files recursively include all imports or extension templates that
43 	are used. The type of the list is `InputFile[]`.
44 */
45 template collectFiles(string root_file)
46 {
47 	import diet.internal.string : stripUTF8BOM;
48 	private static immutable contents = stripUTF8BOM(import(root_file));
49 	enum collectFiles = collectFiles!(root_file, contents);
50 }
51 /// ditto
52 template collectFiles(string root_file, alias root_contents)
53 {
54 	enum baseFiles = collectReferencedFiles!(root_file, root_contents);
55 	static if (baseFiles.canFindFile(root_file))
56 		enum collectFiles = baseFiles;
57 	else enum collectFiles = baseFiles ~ InputFile(root_file, root_contents);
58 
59 }
60 
61 /// Encapsulates a single input file.
62 struct InputFile {
63 	string name;
64 	string contents;
65 }
66 
67 /** Helper template to aggregate a list of compile time values.
68 
69 	This is similar to `AliasSeq`, but does not auto-expand.
70 */
71 template Group(A...) {
72 	import std.typetuple;
73 	alias expand = TypeTuple!A;
74 }
75 
76 /** Returns a mixin string that makes all passed symbols available in the
77 	mixin's scope.
78 */
79 template localAliasesMixin(int i, ALIASES...)
80 {
81 	import std.traits : hasUDA;
82 	static if (i < ALIASES.length) {
83 		import std.conv : to;
84 		static if (hasUDA!(ALIASES[i], DietTraitsAttribute)) enum string localAliasesMixin = localAliasesMixin!(i+1);
85 		else enum string localAliasesMixin = "alias ALIASES["~i.to!string~"] "~__traits(identifier, ALIASES[i])~";\n"
86 			~localAliasesMixin!(i+1, ALIASES);
87 	} else {
88 		enum string localAliasesMixin = "";
89 	}
90 }
91 
92 string ctExtension(string file_name) {
93 	Vector!char ext;
94 	bool is_ext;
95 	foreach(c; file_name) {
96 		if (c == '.') {
97 			is_ext = true;
98 			ext.clear();
99 		}
100 		if (is_ext) {
101 			ext ~= c;
102 		}
103 	}
104 	return ext[].copy();
105 }
106 
107 private template collectReferencedFiles(string file_name, alias file_contents)
108 {
109 
110 	enum references = collectReferences(file_contents);
111 	template impl(size_t i) {
112 		static if (i < references.length) {
113 			enum rfiles = impl!(i+1);
114 			static if (__traits(compiles, import(references[i]))) {
115 				enum ifiles = collectFiles!(references[i]);
116 				enum impl = merge(ifiles, rfiles);
117 			} else static if (__traits(compiles, import(references[i] ~ ctExtension(file_name)))) {
118 				enum ifiles = collectFiles!(references[i] ~ ctExtension(file_name));
119 				enum impl = merge(ifiles, rfiles);
120 			} else enum impl = rfiles;
121 		} else enum InputFile[] impl = [];
122 	}
123 	alias collectReferencedFiles = impl!0;
124 }
125 
126 private string[] collectReferences(string content)
127 {
128 	Vector!string ret;
129 	// content.stripLeft().splitLines()
130 	Vector!char stripped_content;
131 	stripped_content[] = ctstripLeft(content);
132 	Vector!string line_broken_content = ctsplit(stripped_content[], '\n');
133 
134 	foreach (k, ln; line_broken_content[]) {
135 		// FIXME: this produces false-positives when a text paragraph is used:
136 		// p.
137 		//     This is some text.
138 		//     import oops, this is also just text.
139 		ln = ln.ctstripLeft();
140 		if (k == 0 && ln.ctstartsWith("extends "))
141 			ret ~= ln[8 .. $].ctstrip();
142 		else if (ln.ctstartsWith("include "))
143 			ret ~= ln[8 .. $].ctstrip();
144 	}
145 	return ret[].copy();
146 }
147 
148 private InputFile[] merge(T, U)(ref T a, ref U b)
149 {
150 	auto ret = Vector!InputFile(a);
151 	foreach (f; b[]) {
152 		bool found;
153 		foreach (f2; a[]) {
154 			if (f2.name[] == f.name[]) {
155 				found = true;
156 				break;
157 			}
158 		}
159 		if (!found)
160 			ret ~= f;
161 
162 	}
163 	return ret[].copy();
164 }
165 
166 version(DietUseLive)
167 {
168 	/**
169 	Runtime equivalent of collectFiles. This version uses std.file to read files
170 	from the appropriate directory. Note that for collectFiles, the directory to
171 	use is passed on the command line, whereas in this case, we must receive the
172 	directory containing the files from the caller.
173 
174 	Params:
175 			file = The root file that will be used to find all referenced files.
176 			source_directories = Optional base directory list from which all files will be searched.
177 
178 	Returns:
179 			An array of InputFile structs containing the list of files that are
180 			referenced from the root file, and their contents.
181 	*/
182 	InputFile[] rtGetInputs(string file, string[] source_directories...)
183 	{
184 		// for each of the files, import the file, get all the references, and
185 		// continually import files until we have them all.
186 		import std.range;
187 		import std.file : readText, exists;
188 		import diet.internal.string : stripUTF8BOM;
189 		import std.algorithm : canFind;
190 		import std.path : buildPath, extension;
191 
192 		if (!source_directories.length)
193 			source_directories = [""];
194 
195 		auto ext = extension(file);
196 		string[] filesToProcess = [file];
197 		Vector!InputFile result;
198 
199 		void addFile(string fname) {
200 			if (!filesToProcess.canFind(fname) && !result.canFind!(g => g.name == fname))
201 				filesToProcess ~= fname;
202 		}
203 
204 		next_file:
205 		while (filesToProcess.length) {
206 			auto nextFile = filesToProcess.front;
207 			filesToProcess.popFront();
208 			foreach (dir; source_directories) {
209 				if (exists(buildPath(dir, nextFile))) {
210 					auto newInput = InputFile(nextFile, stripUTF8BOM(readText(buildPath(dir, nextFile))));
211 					result ~= newInput;
212 					foreach (f; collectReferences(newInput.contents))
213 						addFile(f);
214 					continue next_file;
215 				}
216 
217 				if (exists(buildPath(dir, nextFile ~ ext))) {
218 					addFile(nextFile ~ ext);
219 					continue next_file;
220 				}
221 			}
222 			throw new Exception("Cannot find necessary file " ~ nextFile ~ " to parse strings for " ~ file);
223 		}
224 
225 		assert(result.length > 0);
226 		return result[].copy();
227 	}
228 }