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 }