1 module definer;
2 
3 
4 import std.algorithm;
5 import std.array;
6 import std.conv;
7 import std.exception;
8 import std.file;
9 import std.path;
10 import std.range;
11 import std.string;
12 
13 
14 import context;
15 import lexer;
16 import mime;
17 import tags;
18 import token;
19 import utils;
20 
21 
22 struct Definer {
23 	struct Options {
24 		bool lines;
25 		string[] search;
26 		string[]* deps;
27 	}
28 
29 	this(Options options) {
30 		this.options = options;
31 	}
32 
33 	static string process(string fileName, Options options) {
34 		auto contents = Definer.getFileContents(fileName, options.search);
35 		return Definer(options).processString(contents.fileName, 1, contents.content);
36 	}
37 
38 private:
39 	string processString(string sourceName, size_t line, string source) {
40 		try {
41 			auto result = parseSource(sourceName, line, source);
42 			if (options.lines && !isAllWhite(result))
43 				result = lineInfo(sourceName, line) ~ result;
44 
45 			if (options.deps)
46 				*options.deps ~= deps.keys;
47 			return result;
48 		} catch(ParserException parserError) {
49 			auto error = concat(parserError.context.sourceName, '(', parserError.context.line, "): ", parserError.msg);
50 			foreach_reverse(context; contextStack)
51 					error = concat(error, "\n> ", context.sourceName, '(', context.line, ')');
52 
53 			throw new Exception(error);
54 		}
55 	}
56 
57 	string parseSource(string sourceName, size_t line, string source) {
58 		auto context = new Context(sourceName, source, line);
59 		contextStack ~= context;
60 		scope(exit) --contextStack.length;
61 
62 		Appender!string result;
63 		result.reserve(64 * 1024);
64 
65 		const size_t end = source.length - min(Tag.OpenTag.length, Tag.CloseTag.length);
66 		while (context.cursor < end) {
67 			auto remaining = context.remaining();
68 			auto indexOpen = remaining.indexOf(Tag.OpenTag);
69 			while (indexOpen != -1) {
70 				auto tag = remaining[indexOpen + Tag.OpenTag.length..indexOpen + Tag.OpenTag.length + 1];
71 				if ((tag == Tag.Define) || (tag == Tag.Include))
72 					break;
73 				indexOpen = remaining.indexOf(Tag.OpenTag, indexOpen + Tag.OpenTag.length);
74 			}
75 			if (indexOpen == -1)
76 				break;
77 
78 			if (!context.defining)
79 				result.put(remaining[0..indexOpen]);
80 			context.advance(indexOpen);
81 
82 			const size_t contentStart = indexOpen + Tag.OpenTag.length;
83 			auto indexClose = remaining.indexOf(Tag.CloseTag, contentStart);
84 			while (indexClose != -1) {
85 				if (balancedQuotes(remaining[contentStart..indexClose]))
86 					break;
87 
88 				indexClose = remaining.indexOf(Tag.CloseTag, indexClose + Tag.CloseTag.length);
89 			}
90 
91 			if (indexClose == -1)
92 				throw new ParserException(concat("missing '", cast(string)Tag.CloseTag, "' to close tag '", cast(string)Tag.OpenTag, "'"), context);
93 
94 			context.advance(Tag.OpenTag.length);
95 			indexClose -= contentStart;
96 
97 			auto replaced = replacer(source[context.cursor..context.cursor + indexClose], context);
98 			if (!context.defining)
99 				result.put(replaced);
100 			context.advance(indexClose + Tag.CloseTag.length);
101 		}
102 		context.expectTagClosed();
103 
104 		result.put((context.cursor > 0) ? context.remaining() : source);
105 
106 		return result.data();
107 	}
108 
109 
110 	auto include(string content, Context context) {
111 		auto embed = ((content.length > 1) && (content[1] == content[0])) ? 1 : 0;
112 		string[] filters;
113 		auto pipe = content.lastIndexOf("|");
114 		auto end = (pipe != -1) ? pipe : content.length;
115 		if (pipe != -1)
116 			filters = content[pipe + 1..$].split(',');
117 
118 		auto fileName = content[1 + embed..end].strip;
119 		auto fixedFileName = fileName;
120 		string contents;
121 
122 		if (extension(fileName).empty)
123 			fixedFileName = concat(fileName, ".html");
124 
125 		auto pcontents = fileName in deps;
126 		if (!pcontents) {
127 			pcontents = fixedFileName in deps;
128 			if (pcontents)
129 				fileName = fixedFileName;
130 		}
131 
132 		if (!embed) {
133 			auto cyclic = fixedFileName == context.sourceName;
134 			if (!cyclic) {
135 				foreach(size_t i; 0..contextStack.length) {
136 					auto prevContext = contextStack[i];
137 					if (prevContext.sourceName == fixedFileName) {
138 						cyclic = true;
139 						break;
140 					}
141 				}
142 			}
143 
144 			if (cyclic)
145 				throw new ParserException(format("cyclic dependency in file '%s'", fixedFileName), context);
146 		}
147 
148 		if (!pcontents) {
149 			while (true) {
150 				try {
151 					auto fcontents = getFileContents(fileName, options.search, embed != 0);
152 					fileName = fcontents.fileName;
153 					contents = fcontents.content;
154 				} catch(Exception e) {
155 					if (fileName == fixedFileName)
156 						throw new ParserException(format("failed to open %s file '%s'", embed ? "embedded" : "include", content[1 + embed..$].strip), context);
157 					fileName = fixedFileName;
158 					continue;
159 				}
160 
161 				deps[fileName] = contents;
162 				break;
163 			}
164 		} else {
165 			contents = *pcontents;
166 		}
167 
168 		if (!filters.empty) {
169 			foreach(i, filter; filters) {
170 				filter = filter.strip;
171 				// note: if you add a filter here, remember to update parser.d if applicable
172 				switch (filter) {
173 					case "capitalize":
174 						contents = contents.capitalize;
175 						break;
176 					case "lower":
177 						contents = contents.toLower;
178 						break;
179 					case "upper":
180 						contents = contents.toUpper;
181 						break;
182 					case "none":
183 						break;
184 					case "html":
185 						contents = contents.escapeHTML;
186 						break;
187 					case "format_html":
188 						content = contents.formatHTML!(FormatHTMLOptions.Escape);
189 						break;
190 					case "format_html_links":
191 						content = contents.formatHTML!(FormatHTMLOptions.Escape | FormatHTMLOptions.CreateLinks);
192 						break;
193 					case "js":
194 						contents = contents.escapeJS;
195 						break;
196 					case "url":
197 						contents = contents.encodeURI;
198 						break;
199 					case "token_url":
200 						contents = contents.appendURIParam("v", token.get);
201 						break;
202 					default:
203 						throw new ParserException(concat("invalid compile-time filter '", filter, "'"), context);
204 				}
205 			}
206 		}
207 
208 		string result;
209 		if (!embed) {
210 			result = processString(fileName, 1, contents);
211 			if (options.lines && !isAllWhite(result))
212 				result ~= lineInfo(context.sourceName, context.line);
213 		} else {
214 			auto mime = extensionToMimeType(extension(fixedFileName));
215 			if (!mime.length)
216 				throw new ParserException(format("failed to deduce mime-type for embeded file '%s' - supported extensions are .jpg, .jpeg, .png, .tga and .gif", content[1 + embed..$].strip), context);
217 
218 			result = concat("data:", mime, ";base64,", mimeEncode(contents));
219 		}
220 		return result;
221 	}
222 
223 	auto define(string content, Context context) {
224 		auto lex = Lexer(cast(ubyte[])content[1..$]);
225 		try {
226 			auto tok = lex.expect("/", Tok.Identifier);
227 			if (tok == Tok.Identifier) {
228 				if (lex.value == "def") {
229 					def(lex, content, context);
230 				} else if (lex.value == "undef") {
231 					undef(lex, context);
232 				} else {
233 					// keep these for parser
234 					if ((lex.value == "set") || (lex.value == "push") || (lex.value == "pop"))
235 						return concat("{{", content, "}}");
236 
237 					if (!context.defining)
238 						return expand(lex, context);
239 				}
240 			} else {
241 				close(lex, context);
242 			}
243 		} catch (LexerException e) {
244 			throw new ParserException(e.msg, context);
245 		}
246 
247 		return null;
248 	}
249 
250 
251 	void def(ref Lexer lex, string content, Context context) {
252 		auto decl = parseDef(lex, context);
253 
254 		auto pdef = decl.name in defs;
255 		if (pdef) {
256 			if ((pdef.sourceName != decl.sourceName) || (pdef.line != decl.line))
257 				throw new ParserException(format("redefinition of macro '%s' - first defined in '%s(%d)' - if this is intended undefine first", decl.name, decl.sourceName, decl.line), context);
258 		}
259 
260 		Def def;
261 		def.sourceName = decl.sourceName;
262 		def.line = decl.line;
263 		def.args = decl.args;
264 		if (decl.inline) {
265 			def.flags = Def.Flags.Inline;
266 			def.value = decl.value;
267 		} else {
268 			def.flags = Def.Flags.NotYetDefined;
269 			context.tagOpen(content[0..1], content[1..$]);
270 		}
271 
272 		defs[decl.name] = def;
273 	}
274 
275 	void undef(ref Lexer lex, Context context) {
276 		lex.popFront;
277 		lex.expect(Tok.Identifier);
278 		auto name = lex.value;
279 
280 		auto pdef = name in defs;
281 		if (!pdef)
282 			throw new ParserException(concat("trying to undefine unknown macro '", name, "'"), context);
283 		if (pdef.flags & Def.Flags.NotYetDefined)
284 			throw new ParserException(concat("trying to undefine macro '", name, "' inside it's own definition"), context);
285 
286 		defs.remove(name);
287 	}
288 
289 
290 	void close(ref Lexer lex, Context context) {
291 		lex.popFront;
292 		lex.expect(Tok.EndOfInput);
293 
294 		auto tag = context.tagClose();
295 
296 		lex = Lexer(cast(ubyte[])tag.content);
297 		auto decl = parseDef(lex, context);
298 
299 		size_t start = tag.cursor + Tag.Define.length + Tag.CloseTag.length + tag.content.length;
300 		size_t end = context.cursor - Tag.OpenTag.length;
301 		auto value = context.source[start..end];
302 
303 		auto pdef = decl.name in defs;
304 		assert(pdef);
305 		assert(pdef.flags & Def.Flags.NotYetDefined);
306 
307 		pdef.value = value;
308 		pdef.flags &= ~Def.Flags.NotYetDefined;
309 	}
310 
311 
312 	auto expandArg(string name, Context context) {
313 		if (name[0] == '`') {
314 			assert(name[$-1] == name[0]);
315 			return name[1..$-1];
316 		}
317 		if ((name[0] == '\"') || (name[0] == '\'')) {
318 			assert(name[$-1] == name[0]);
319 			return name;
320 		}
321 
322 		auto parg = name in args;
323 		if (parg)
324 			return *parg;
325 
326 		auto pdef = name in defs;
327 		if (pdef) {
328 			auto argsSaved = args;
329 			args = null;
330 			auto result = processString(pdef.sourceName, pdef.line, pdef.value);
331 			args = argsSaved;
332 			return result;
333 		}
334 
335 		throw new ParserException(concat("unknown argument '", name, "'"), context);
336 	}
337 
338 
339 	auto expand(ref Lexer lex, Context context) {
340 		auto name = lex.value;
341 		auto parg = name in args;
342 		if (parg)
343 			return *parg;
344 
345 		auto pdef = name in defs;
346 		if (!pdef)
347 			throw new ParserException(concat("unknown macro '", name, "'"), context);
348 
349 		lex.popFront;
350 		auto tok = lex.expect("(", Tok.EndOfInput);
351 
352 		if (tok != Tok.EndOfInput) {
353 			auto argList = parseArgList(lex, false);
354 			if (pdef.args.length < argList.args.length)
355 				throw new ParserException(concat("too many parameters for macro '", pdef.pretty(name), "'"), context);
356 
357 			auto argsSaved = args;
358 			args = null;
359 
360 			foreach(i, arg; argList.args)
361 				args[pdef.args[i]] = expandArg(arg, context);
362 
363 			// empty out remaining optional parameters
364 			foreach(i, arg; pdef.args[argList.args.length..$])
365 				args[arg] = "";
366 
367 			auto result = processString(pdef.sourceName, pdef.line, pdef.value);
368 			args = argsSaved;
369 
370 			return result;
371 		} else {
372 			return processString(pdef.sourceName, pdef.line, pdef.value);
373 		}
374 	}
375 
376 
377 	auto parseArgList(ref Lexer lex, bool decl) {
378 		struct ArgList {
379 			string[] args;
380 			Tok[] types;
381 		}
382 
383 		assert(lex.value == "(");
384 		lex.popFront;
385 
386 		ArgList argList;
387 
388 		auto tok = decl ? lex.expect(")", Tok.Identifier) : lex.expect(")", Tok.Identifier, Tok.Literal);
389 		if (lex.value != ")") {
390 			while (true) {
391 				argList.args ~= lex.value;
392 				argList.types ~= tok;
393 
394 				lex.popFront;
395 				tok = lex.expect(")", ",");
396 				if (lex.value == ")")
397 					break;
398 				lex.popFront;
399 
400 				tok = decl ? lex.expect(Tok.Identifier) : lex.expect(Tok.Identifier, Tok.Literal);
401 			}
402 		}
403 		lex.popFront;
404 
405 		return argList;
406 	}
407 
408 
409 	auto parseDef(ref Lexer lex, Context context) {
410 		struct DefDecl {
411 			string name;
412 			string value;
413 			bool inline;
414 
415 			string[] args;
416 
417 			string sourceName;
418 			size_t line;
419 		}
420 
421 		assert(lex.value == "def");
422 		lex.popFront;
423 		lex.expect(Tok.Identifier);
424 
425 		DefDecl decl;
426 		decl.name = lex.value;
427 		decl.sourceName = context.sourceName;
428 		decl.line = context.line;
429 		lex.popFront;
430 		auto tok = lex.expect("(", ":", Tok.EndOfInput);
431 		if (tok != Tok.EndOfInput) {
432 			if (lex.value == "(") {
433 				auto argList = parseArgList(lex, true);
434 				decl.args = argList.args;
435 			}
436 
437 			if (lex.value == ":") {
438 				decl.inline = true;
439 				decl.value = lex.remaining;
440 				lex.popFront;
441 			} else {
442 				lex.expect(Tok.EndOfInput);
443 			}
444 		}
445 
446 		return decl;
447 	}
448 
449 
450 	auto replacer(string content, Context context) {
451 		if (content.length > 0) {
452 			auto tag = content[0..1];
453 			switch(tag) {
454 				case Tag.Include:
455 					return include(content, context).strip;
456 				case Tag.Define:
457 					return define(content, context).strip;
458 				default:
459 					assert(0);
460 			}
461 		}
462 		return null;
463 	}
464 
465 
466 	auto lineInfo(string sourceName, size_t lineNumber) {
467 		if (options.lines)
468 			return concat("{{", cast(string)Tag.LineInfo, "#line ", lineNumber, " ", sourceName.quoted, "}}");
469 		return null;
470 	}
471 
472 
473 	static auto getFileContents(string fileName, string[] search, bool binary = false) {
474 		struct Contents {
475 			string fileName;
476 			string content;
477 		}
478 
479 		Throwable error;
480 		try {
481 			if (!binary) {
482 				return Contents(fileName, (cast(string)read(fileName)).stripUTFbyteOrderMarker);
483 			} else {
484 				return Contents(fileName, cast(string)read(fileName));
485 			}
486 		} catch(Throwable e) {
487 			error = e;
488 		}
489 
490 		foreach(path; search) {
491 			auto name = buildNormalizedPath(path, fileName);
492 			try {
493 				if (!binary) {
494 					return Contents(name, (cast(string)read(name)).stripUTFbyteOrderMarker);
495 				} else {
496 					return Contents(name, cast(string)read(name));
497 				}
498 			} catch(Throwable) {
499 			}
500 		}
501 
502 		throw error;
503 	}
504 
505 private:
506 	struct Def {
507 		enum Flags : uint {
508 			NotYetDefined   = 1 << 0,
509 			Inline          = 1 << 1,
510 		}
511 
512 		string value;
513 		string[] args;
514 		uint flags;
515 
516 		string sourceName;
517 		size_t line;
518 
519 		string pretty(string name) {
520 			Appender!string app;
521 			app.reserve(1024);
522 
523 			app.put(name);
524 
525 			app.put("(");
526 
527 			foreach(i, arg; args) {
528 				app.put(arg);
529 				if (i != args.length - 1)
530 					app.put(", ");
531 			}
532 			app.put(")");
533 
534 			return app.data;
535 		}
536 	}
537 
538 	Options options;
539 	Def[string] defs;
540 	string[string] args;
541 	string[string] deps;
542 
543 	Context[] contextStack;
544 }
545 
546 
547 private @property bool isAllWhite(Range)(Range range) {
548 	foreach(ch; range) {
549 		if (!std.uni.isWhite(ch))
550 			return false;
551 	}
552 	return true;
553 }