├── Lust.lua ├── README.md └── examples ├── dispatch.lua └── tests.lua /Lust.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | 3 | Given a template, generate a generator. 4 | 5 | MIT license. 6 | 7 | --]] 8 | 9 | local concat = table.concat 10 | local format = string.format 11 | 12 | -- util: 13 | local 14 | function tree_dump_helper(t, p, strlimit, ignorekeys) 15 | if type(t) == "table" then 16 | local terms = { "{" } 17 | local p1 = p .. " " 18 | for k, v in pairs(t) do 19 | if not ignorekeys[k] then 20 | local key 21 | if type(k) == "number" then 22 | key = tostring(k) 23 | else 24 | key = format("%q", k) 25 | end 26 | terms[#terms+1] = format("[%s] = %s,", key, tree_dump_helper(v, p1, strlimit-2, ignorekeys)) 27 | end 28 | end 29 | return format("%s\n%s}", concat(terms, "\n"..p1), p) 30 | elseif type(t) == "number" then 31 | return tostring(t) 32 | elseif type(t) == "string" and #t > strlimit then 33 | return format("%q...", t:sub(1, strlimit)) 34 | else 35 | return format("%q", t) 36 | end 37 | end 38 | 39 | local 40 | function tree_dump(t, strlimit, ignorekeys) 41 | print(tree_dump_helper(t, "", strlimit and strlimit or 80, ignorekeys or {})) 42 | end 43 | 44 | local 45 | function qesc(str) 46 | local len = str:len() 47 | if(str:sub(1, 1) == '"' and str:sub(len, len) == '"') then 48 | local s = str:sub(2, len-1) 49 | if(s:sub(1, 1) == "\n") then 50 | s = "\n"..s 51 | end 52 | return format("[===[%s]===]", s) 53 | else 54 | return str 55 | end 56 | end 57 | 58 | local 59 | function printlines(s, from, to) 60 | from = from or 0 61 | to = to or 100000000 62 | local l = 1 63 | for w in s:gmatch("(.-)\r?\n") do 64 | if l >= from and l <= to then print(l, w) end 65 | l = l + 1 66 | end 67 | end 68 | 69 | local gensym = (function() 70 | local id = 0 71 | return function(s) 72 | id = id + 1 73 | local pre = s or "v" 74 | return format("%s%d", pre, id) 75 | end 76 | end)() 77 | 78 | -------------------------------------------------------------------------------- 79 | -- Code accumulator (like a rope) 80 | -------------------------------------------------------------------------------- 81 | 82 | local writer = {} 83 | writer.__index = writer 84 | 85 | function writer.create(prefix, indent) 86 | local s = { 87 | level = 0, 88 | prefix = prefix or "", 89 | indent = indent or " ", 90 | stack = {}, 91 | } 92 | s.current = s 93 | return setmetatable(s, writer) 94 | end 95 | 96 | function writer:write(s) 97 | self.current[#self.current+1] = s 98 | return self 99 | end 100 | writer.__call = writer.write 101 | 102 | function writer:format(s, ...) 103 | self.current[#self.current+1] = format(s, ...) 104 | return self 105 | end 106 | 107 | function writer:comment(s, ...) 108 | self.current[#self.current+1] = "-- " .. format(s, ...) 109 | return self 110 | end 111 | 112 | -- indentation: 113 | function writer:push() 114 | local w1 = writer.create(self.indent) 115 | self.current[#self.current+1] = w1 116 | self.stack[#self.stack+1] = self.current 117 | self.current = w1 118 | return self 119 | end 120 | function writer:pop() 121 | if #self.stack > 0 then 122 | self.current = self.stack[#self.stack] 123 | self.stack[#self.stack] = nil 124 | else 125 | error("too many pops") 126 | end 127 | return self 128 | end 129 | 130 | function writer:concat(sep) 131 | sep = sep or "" 132 | sep = sep .. self.prefix 133 | for i = 1, #self do 134 | local v = self[i] 135 | if type(v) == "table" then 136 | if v.concat then 137 | self[i] = v:concat(sep) 138 | else 139 | self[i] = concat(v, sep) 140 | end 141 | end 142 | end 143 | return self.prefix .. concat(self, sep) 144 | end 145 | 146 | -------------------------------------------------------------------------------- 147 | -- Template definition grammar 148 | -------------------------------------------------------------------------------- 149 | 150 | local lpeg = require "lpeg" 151 | local P = lpeg.P 152 | local C = lpeg.C 153 | local Carg = lpeg.Carg 154 | local Cc = lpeg.Cc 155 | local Cg = lpeg.Cg 156 | local Ct = lpeg.Ct 157 | local Cp = lpeg.Cp 158 | local V = lpeg.V 159 | 160 | -- utils: 161 | local 162 | function left_binop(x, pos1, op, y, pos2, ...) 163 | --print("left_binop", op, x, y, ...) 164 | return op and left_binop({x, y, rule="binop", op=op, start=pos1, finish=pos2 }, pos1, ...) or x 165 | end 166 | 167 | local 168 | function left_unop(start, op, x, finish, chained, ...) 169 | if finish then 170 | if chained then 171 | x = left_unop(x, finish, chained, ...) 172 | finish = x.finish 173 | end 174 | return { 175 | x, 176 | rule="unop", 177 | op=op, 178 | start=start, 179 | finish=finish 180 | } 181 | else 182 | return start 183 | end 184 | end 185 | 186 | local function delimited(patt) 187 | return patt * C((1-patt)^0) * patt 188 | end 189 | 190 | -- basic patterns: 191 | 192 | -- numerics: 193 | local digit = lpeg.R("09") 194 | local integer = digit^1 195 | local quoted_integer = P'"' * C(integer) * P'"' 196 | 197 | local newline = lpeg.S"\n\r" 198 | 199 | -- optional white space: 200 | local space = lpeg.S" \t" 201 | local _ = space^0 202 | 203 | -- pure alphabetic words: 204 | local alpha = lpeg.R"az" + lpeg.R"AZ" 205 | local symbol = alpha * (alpha)^0 206 | 207 | local literal = delimited(P"'") + delimited(P'"') 208 | 209 | -- typical C-like variable names: 210 | local name = (alpha + P"_") * (alpha + integer + P"_")^0 211 | 212 | -- the patten to pick up the global state: 213 | local g = Carg(1) 214 | 215 | -- General rule constructors: 216 | local function Rule(patt, name) 217 | return Ct( 218 | Cg(Cp(), "start") * 219 | patt * 220 | Cg(Cp(), "finish") * 221 | Cg(Cc(name), "rule") 222 | ) 223 | end 224 | 225 | -- constructors for left-recursive operator rules: 226 | -- O is the operator, B is the next pattern to try 227 | function BinRule(O, B) 228 | -- matches B 229 | -- matches B op B 230 | -- matches B op B op B etc. 231 | return (B * Cp() * (_ * O *_* B * Cp())^0) / left_binop 232 | end 233 | 234 | -- O is the operator, B is the next pattern to try 235 | function UnRule(O, B) 236 | -- matches op B 237 | -- matches op op B 238 | -- else matches B 239 | return (((Cp() * O * _)^1 * B * Cp()) / left_unop) + B 240 | end 241 | 242 | -- the grammar: 243 | local grammar = P{ 244 | 245 | -- start symbol (grammar entry point): 246 | Rule(V"firstinsert"^-1 * (V"insert" + V"anything")^0, "main"), 247 | 248 | -- anything that isn't an insertion point: 249 | anything = Rule( 250 | C((P(1) - V"insert")^1), 251 | "anything" 252 | ), 253 | 254 | -- all the insertion points are wrapped in an indentation detector: 255 | indent = Cg(C(space^1), "indent"), 256 | -- special rule if an insertion appears before an 'anything' rule 257 | -- because the initial newline anchor is not needed 258 | -- and the generated code will also omit this newline 259 | firstinsert = Rule( 260 | (Cg(Cc"true", "first") * V"indent")^-1 * 261 | V"insertbody", 262 | "insert" 263 | ), 264 | -- subsequent insertions must be anchored by a newline for indent to work: 265 | insert = Rule( 266 | (newline * V"indent")^-1 * 267 | V"insertbody", 268 | "insert" 269 | ), 270 | 271 | insertbody = Rule( 272 | P"@if" * V"cond", 273 | "cond" 274 | ) 275 | + Rule( 276 | P"@iter" * V"iter_env" * P":" * V"iter_body", 277 | "iter" 278 | ) 279 | + Rule( 280 | P"@" * (C"map" + C"rest" + C"first" + C"last") * 281 | (V"env_dict" + V"env") * P":" * V"iter_body", 282 | "map" 283 | ) 284 | + Rule( 285 | ( 286 | (P"@<" * V"apply_body" * P">") 287 | + (P"@" * V"apply_body") 288 | ), 289 | "apply" 290 | ) 291 | + Rule( 292 | ( 293 | (P"$<" * V"index" * P">") 294 | + (P"$" * V"index") 295 | ), 296 | "substitute" 297 | ), 298 | 299 | 300 | -- main applications: 301 | 302 | apply_env_nil = V"path_nil", 303 | apply_env = (V"env" * P":") 304 | + (V"path" * P":"), 305 | 306 | -- dynamic template path: 307 | -- e.g. a.(b.c).d 308 | apply_eval_term = V"eval" 309 | + C(name), 310 | 311 | apply_eval_path = Rule( 312 | C"#"^-1 * V"apply_eval_term" * (P"." * V"apply_eval_term")^0, 313 | "apply_eval_path" 314 | ), 315 | 316 | apply_body_env = Rule(V"apply_env" * V"apply_eval_path", "apply_eval") 317 | + Rule(V"apply_env" * V"inline", "apply_inline"), 318 | 319 | apply_body_no_env = Rule(V"apply_env_nil" * V"apply_eval_path", "apply_eval") 320 | + Rule(V"apply_env_nil" * V"inline", "apply_inline"), 321 | 322 | apply_body = V"apply_body_env" 323 | + V"apply_body_no_env", 324 | 325 | iter_range_term = Rule(quoted_integer, "quoted") 326 | + Rule(V"path", "len"), 327 | 328 | iter_range = Rule( 329 | P"[" *_* V"iter_range_term" 330 | *_* "," *_* V"iter_range_term" *_* P"]", 331 | "iter_range" 332 | ) 333 | + V"iter_range_term", 334 | 335 | iter_env = Rule( 336 | P"{" *_* V"iter_range" *_* 337 | (P"," *_* V"env_separator")^-1 338 | *_* P"}", 339 | "iter_env" 340 | ), 341 | 342 | iter_body = Rule(V"apply_eval_path", "iter_eval") 343 | + Rule(V"inline", "iter_inline"), 344 | 345 | 346 | cond = P"(" * V"cmp7" * P")<" * V"apply_body_no_env" * P">" 347 | * (P"else<" * V"apply_body_no_env" * P">")^-1, 348 | 349 | -- environment indexing: 350 | path_term = V"eval" 351 | + C(name) 352 | + (integer / tonumber), 353 | path_nil = Rule( 354 | Cc".", 355 | "path_nil" 356 | ), 357 | path_current = Rule( 358 | C".", 359 | "path_current" 360 | ), 361 | 362 | path = V"path_current" 363 | + Rule( 364 | V"path_term" * (P"." * V"path_term")^0, 365 | "path" 366 | ), 367 | 368 | index = Rule(P"#" * V"path", "len") 369 | + V"path", 370 | 371 | -- environments: 372 | 373 | env_term_value = V"apply_body_env" 374 | + V"env" 375 | + V"env_array" 376 | + Rule(literal, "literal") 377 | + V"index", 378 | 379 | env_separator = Rule( 380 | ((P"_separator" + P"_") *_* P"=" *_* V"env_term_value"), 381 | "separator" 382 | ), 383 | 384 | env_tuple = V"env_separator" 385 | + Rule( 386 | C(name) *_* P"=" *_* V"env_term_value", 387 | "tuple" 388 | ), 389 | 390 | env_term = V"env_tuple" + V"env_term_value", 391 | 392 | env_term_list = (V"env_term" * (_ * P"," *_* V"env_term")^0 *_* P","^-1), 393 | 394 | env_array = Rule( 395 | P"[" *_* V"env_term_list" *_* P"]", 396 | "env_array" 397 | ), 398 | 399 | env = Rule( 400 | P"{" *_* V"env_term_list" *_* P"}", 401 | "env" 402 | ), 403 | 404 | -- like env, but only allows tuples: 405 | env_tuple_list = (V"env_tuple" * (_ * P"," *_* V"env_tuple")^0 *_* P","^-1), 406 | env_dict = Rule( 407 | P"{" *_* V"env_tuple_list" *_* P"}", 408 | "env_dict" 409 | ), 410 | 411 | 412 | -- application bodies: 413 | 414 | eval = P"(" * V"path" * P")", 415 | 416 | inline_term = V"insert" 417 | + Rule(C((P(1) - (P"}}" + V"insert"))^1), "inline_text"), 418 | inline = P"{{" 419 | * Rule( 420 | V"inline_term"^0, 421 | "inline" 422 | ) 423 | * P"}}", 424 | 425 | -- conditionals: 426 | 427 | cmp0 = Rule(P"?(" *_* V"path" *_* P")", "exists") 428 | + Rule(V"eval", "lookup") 429 | + Rule(quoted_integer, "quoted") 430 | + Rule(C(literal), "quoted") 431 | + V"index", 432 | 433 | cmp1 = UnRule( 434 | C"^", 435 | V"cmp0" 436 | ), 437 | cmp2 = Rule( -- legacy length{x} operator: 438 | Cg(Cc"#", "op") * P"length{" *_* V"cmp0" *_* P"}", 439 | "unop" 440 | ) 441 | + UnRule( 442 | C"not" + C"#" + C"-", 443 | V"cmp1" 444 | ), 445 | cmp3 = BinRule( 446 | C"*" + C"/" + C"%", 447 | V"cmp2" 448 | ), 449 | cmp4 = BinRule( 450 | C"+" + C"-", 451 | V"cmp3" 452 | ), 453 | cmp5 = BinRule( 454 | C"==" + C"~=" + C"!=" + C">=" + C"<=" + C"<" + C">", 455 | V"cmp4" 456 | ), 457 | cmp6 = BinRule( 458 | C"and", 459 | V"cmp5" 460 | ), 461 | cmp7 = BinRule( 462 | C"or", 463 | V"cmp6" 464 | ), 465 | 466 | } 467 | 468 | -------------------------------------------------------------------------------- 469 | -- Generator generator 470 | -------------------------------------------------------------------------------- 471 | 472 | -- a table to store the semantic actions for the grammar 473 | -- i.e. the routines to generate the code generator code 474 | local action = {} 475 | 476 | -- NOTE: the 'self' argument to these actions is not an 'action', but 477 | -- actually a 'gen' object, as defined below. 478 | 479 | -- all actions take two arguments: 480 | -- @node: the parse tree node to evaluate 481 | -- @out: an array of strings to append to 482 | 483 | function action:main(node, out) 484 | out("local out = {}") 485 | for i, v in ipairs(node) do 486 | out( self:dispatch(v, out) ) 487 | end 488 | out("local result = concat(out)") 489 | out("return result") 490 | end 491 | 492 | function action:anything(node, out) 493 | return format("out[#out+1] = %q", node[1]) 494 | end 495 | 496 | function action:insert(node, out) 497 | local indent = node.indent 498 | -- write the first line indentation: 499 | if indent and indent ~= "" then 500 | -- generate the body: 501 | out:comment("rule insert (child)") 502 | out:write("local indented = {}") 503 | out:write("do"):push() 504 | out:comment("accumulate locally into indented:") 505 | out:write("local out = indented") 506 | -- everything in this dispatch should be indented: 507 | out:write(self:dispatch(node[1], out)) 508 | out:pop():write("end") 509 | -- mix sub back into parent: 510 | out:comment("apply insert indentation:") 511 | if not node.first then 512 | out:write("out[#out+1] = newline") 513 | end 514 | out:format("out[#out+1] = %q", indent) 515 | out:format("out[#out+1] = concat(indented):gsub('\\n', %q)", '\n' .. indent) 516 | else 517 | out:write(self:dispatch(node[1], out)) 518 | end 519 | end 520 | 521 | function action:path(node, out) 522 | -- TODO: check if this has already been indexed in this scope 523 | local name = gensym(env) --"env_"..concat(node, "_") 524 | 525 | local env = "env" 526 | local vname = "''" 527 | local nterms = #node 528 | for i = 1, nterms do 529 | local v = node[i] 530 | local term 531 | 532 | if type(v) == "table" then 533 | local value = self:dispatch(v, out) 534 | term = format("%s[%s]", env, value) 535 | --out:format("print('value', %s, %s)", value, term) 536 | vname = gensym("env") 537 | else 538 | if type(v) == "number" then 539 | term = format("%s[%d]", env, v) 540 | elseif type(v) == "string" then 541 | term = format("%s[%q]", env, v) 542 | else 543 | error("bad path") 544 | end 545 | vname = format("%s_%s", env, v) 546 | 547 | end 548 | 549 | out:format("local %s = (type(%s) == 'table') and %s or nil", vname, env, term) 550 | env = vname 551 | end 552 | 553 | return vname 554 | end 555 | 556 | function action:separator(node, out) 557 | return node[1] 558 | end 559 | 560 | function action:len(node, out) 561 | local path = self:dispatch(node[1], out) 562 | 563 | return format("len(%s)", path) 564 | end 565 | 566 | function action:path_current(node, out) 567 | return "env" 568 | end 569 | 570 | function action:path_nil(node, out) 571 | return action.path_current(self, node, out) 572 | end 573 | 574 | function action:substitute(node, out) 575 | local content = assert(node[1]) 576 | local text = self:dispatch(content, out) 577 | return format("out[#out+1] = %s", text) 578 | end 579 | 580 | function action:apply(node, out) 581 | local text = self:dispatch(node[1], out) 582 | if text then 583 | return format("out[#out+1] = %s", text) 584 | end 585 | end 586 | 587 | function action:loop_helper(body, out, it, start, len, terms, mt, sep, parent) 588 | local insep = sep 589 | 590 | -- start loop: 591 | out:format("for %s = %s, %s do", it, start, len) 592 | :push() 593 | 594 | -- the dynamic env: 595 | out("local env = setmetatable({") 596 | :push() 597 | :format("i1 = %s,", it) 598 | :format("i0 = %s - 1,", it) 599 | if terms then 600 | for i, v in ipairs(terms) do 601 | -- TODO: only index if type is table! 602 | out(v) 603 | end 604 | end 605 | out:pop() 606 | :format("}, %s)", mt) 607 | 608 | if parent then 609 | out:format([[if(type(parent[%s]) == "table") then]], it) 610 | :push() 611 | :format("for k, v in pairs(parent[%s]) do", it) 612 | :push() 613 | :write("env[k] = v") 614 | :pop() 615 | :write("end") 616 | :pop() 617 | :write("else") 618 | :push() 619 | :format("env[1] = parent[%s]", it) 620 | :pop() 621 | :write("end") 622 | end 623 | 624 | -- the body: 625 | local result = self:dispatch(body, out) 626 | out:format("out[#out+1] = %s", result) 627 | 628 | -- the separator: 629 | if insep then 630 | out:comment("separator:") 631 | out:format("if %s < %s then", it, len) 632 | :push() 633 | :format("out[#out+1] = %s", qesc(insep)) 634 | :pop() 635 | :write("end") 636 | end 637 | 638 | -- end of loop: 639 | out:pop() 640 | :write("end") 641 | return "" 642 | end 643 | 644 | function action:iter(node, out) 645 | 646 | local env, body = node[1], node[2] 647 | local sep 648 | local start = "1" 649 | local len 650 | 651 | assert(env.rule == "iter_env", "malformed iter env") 652 | local range = assert(env[1], "missing iter range") 653 | local v = env[2] -- optional 654 | if v then 655 | assert(v.rule == "separator", "second @iter term must be a separator") 656 | sep = self:dispatch(v[1], out) 657 | end 658 | 659 | --out:comment("range " .. range.rule) 660 | if range.rule == "iter_range" then 661 | -- explicit range 662 | if #range > 1 then 663 | start = gensym("s") 664 | out:format("local %s = %s", start, self:dispatch(range[1], out)) 665 | 666 | len = gensym("len") 667 | out:format("local %s = %s", len, self:dispatch(range[2], out)) 668 | else 669 | len = gensym("len") 670 | out:format("local %s = %s", len, self:dispatch(range[1], out)) 671 | end 672 | else 673 | len = gensym("len") 674 | out:format("local %s = %s", len, self:dispatch(range, out)) 675 | end 676 | 677 | local it = gensym("it") 678 | -- an environment to inherit: 679 | local mt = gensym("mt") 680 | out:format("local %s = { __index=env }", mt) 681 | return action.loop_helper(self, body, out, it, start, len, terms, mt, sep) 682 | end 683 | 684 | function action:map(node, out) 685 | --tree_dump(node) 686 | 687 | local ty, env, body = node[1], node[2], node[3] 688 | -- loop boundaries: 689 | local it = gensym("it") 690 | local start 691 | local len = gensym("len") 692 | -- loop separator: 693 | local sep 694 | -- loop environment terms: 695 | local terms = {} 696 | local parent 697 | local idx = 1 698 | out:comment("%s over %s", ty, env.rule) 699 | 700 | -- the environment to inherit: 701 | local mt 702 | 703 | if env.rule == "env_dict" then 704 | -- tuple-only iterator 705 | -- each tuple value is indexed during iteration 706 | 707 | -- len will be derived according to max length of each item 708 | -- default zero is needed 709 | out:format("local %s = 0", len) 710 | 711 | for i, v in ipairs(env) do 712 | -- detect & lift out separator: 713 | if v.rule == "separator" then 714 | sep = self:dispatch(v[1], out) 715 | break 716 | end 717 | 718 | 719 | -- parse each item: 720 | local k, s 721 | if v.rule == "tuple" then 722 | k = format("%q", self:dispatch(v[1], out)) 723 | v = v[2] 724 | else 725 | k = idx 726 | idx = idx + 1 727 | end 728 | s = self:dispatch(v, out) 729 | if v.rule == "literal" 730 | or v.rule == "len" 731 | or v.rule == "apply_template" then 732 | -- these rules only return strings: 733 | terms[i] = format("[%s] = %s,", k, s) 734 | elseif v.rule == "env_array" then 735 | -- these rules only return arrays: 736 | terms[i] = format("[%s] = %s[%s] or '',", k, s, it) 737 | out:format("%s = max(len(%s), %s)", len, s, len) 738 | else 739 | -- these rules might be strings or tables, need to index safely: 740 | local b = gensym("b") 741 | out:format("local %s = type(%s) == 'table'", b, s) 742 | out:format("if %s then", b):push() 743 | out:format("%s = max(len(%s), %s)", len, s, len) 744 | out:pop() 745 | out("end") 746 | 747 | --out:format("print('loop', '%s', %s)", len, len) 748 | 749 | 750 | terms[i] = format("[%s] = %s and (%s[%s] or '') or %s,", k, b, s, it, s) 751 | end 752 | end 753 | 754 | -- but meta-environment is always the same: 755 | mt = gensym("mt") 756 | out:format("local %s = { __index=env }", mt) 757 | 758 | elseif env.rule == "env" then 759 | -- list iterator 760 | assert(#env > 0, "missing item to iterate") 761 | -- only the first array item is iterated 762 | for i, v in ipairs(env) do 763 | if v.rule == "separator" then 764 | sep = self:dispatch(v[1], out) 765 | break 766 | elseif v.rule == "tuple" then 767 | -- copy evaluted dict items into the env: 768 | local k = format("%q", self:dispatch(v[1], out)) 769 | local s = self:dispatch(v[2], out) 770 | 771 | terms[#terms+1] = format("[%s] = %s,", k, s) 772 | else 773 | -- the array portion is copied in element by element: 774 | local s = self:dispatch(v, out) 775 | out:format("local parent = %s", s) 776 | :format("local %s = (type(parent) == 'table') and #parent or 0", len, s, s) 777 | 778 | mt = "getmetatable(parent)" 779 | parent = true 780 | end 781 | end 782 | else 783 | error("malformed map env") 784 | end 785 | 786 | if ty == "rest" then 787 | start = "2" 788 | elseif ty == "first" then 789 | start = "1" 790 | len = string.format("math.min(1, %s)", len) 791 | elseif ty == "last" then 792 | start = len 793 | else -- map 794 | start = "1" 795 | end 796 | 797 | return action.loop_helper(self, body, out, it, start, len, terms, mt, sep, parent) 798 | end 799 | 800 | function action:apply_helper(tmp, env, out) 801 | out:comment("apply helper") 802 | local name = gensym("apply") 803 | out:format("local %s = ''", name) 804 | out:format("if %s then", tmp) 805 | :push() 806 | out:format("%s = %s(%s)", name, tmp, env) 807 | out:pop() 808 | :write("end") 809 | return name 810 | end 811 | 812 | function action:apply_template(node, out) 813 | local env = self:dispatch(node[1], out) 814 | return action.apply_template_helper(self, node[2], env, out) 815 | end 816 | function action:iter_template(node, out) 817 | return action.apply_template_helper(self, node[1], "env", out) 818 | end 819 | 820 | function action:lookup_helper(rule, out) 821 | local ctx = self.ctx 822 | local ctxlen = #ctx 823 | 824 | -- construct the candidate rules: 825 | local candidates = {} 826 | for i = ctxlen, 1, -1 do 827 | candidates[#candidates+1] = format("rules[%q .. %s]", concat(ctx, ".", 1, i) .. ".", rule) 828 | end 829 | candidates[#candidates+1] = format("rules[%s]", rule) 830 | 831 | local tmp = gensym("tmp") 832 | out:format("local %s = %s", tmp, concat(candidates, " or ")) 833 | --[==[ 834 | out:format([[if(tem__ == 'unit.self.coordinates.norm') then 835 | print("rule", %s) 836 | end 837 | ]], tmp) 838 | --]==] 839 | return tmp 840 | end 841 | 842 | function action:lookup(node, out) 843 | local rule = action.path(self, node[1], out) 844 | return action.lookup_helper(self, rule, out) 845 | end 846 | 847 | 848 | 849 | function action:apply_template_helper(body, env, out) 850 | out:comment("template application:") 851 | local rule = self:dispatch(body, out) 852 | return action.apply_helper(self, rule, env, out) 853 | end 854 | 855 | -- OK too much special casing here 856 | -- also, #rooted paths are not being handed for the dynamic case 857 | function action:apply_eval_path(node, out) 858 | local terms = {} 859 | local sterms = {} 860 | local isdynamic = false 861 | local isabsolute = false 862 | for i, v in ipairs(node) do 863 | if v == "#" then 864 | isabsolute = true 865 | else 866 | if type(v) ~= "string" then 867 | -- instead, we could break here and just 868 | -- return tailcall to the dynamic method 869 | isdynamic = true 870 | local term = self:dispatch(v, out) 871 | terms[#terms+1] = term 872 | else 873 | local term = v 874 | sterms[#sterms+1] = term 875 | terms[#terms+1] = format("%q", term) 876 | end 877 | end 878 | end 879 | 880 | if isdynamic then 881 | out:comment("dynamic application") 882 | 883 | local evaluated = concat(terms, ", ") 884 | --out:format([[print("dynamic path", %s)]], evaluated) 885 | 886 | local rule = gensym("rule") 887 | out:format([[local %s = concat({%s}, ".")]], rule, evaluated) 888 | --[==[ 889 | out:format([[ 890 | if(tem__ == 'unit.self.coordinates.norm') then 891 | print("%s", %s) 892 | end 893 | ]], rule, rule) 894 | --]==] 895 | return action.lookup_helper(self, rule, out) 896 | else 897 | out:comment("static application") 898 | -- absolute or relative? 899 | local ctx = self.ctx 900 | local ctxlen = #ctx 901 | local pathname = concat(sterms, ".") 902 | --print("pathname", pathname) 903 | 904 | if isabsolute or ctxlen == 0 then 905 | -- we are at root level, just use the absolute version: 906 | return self:reify(pathname) 907 | end 908 | 909 | -- this is the template name we are looking for: 910 | 911 | --print("looking for", pathname) 912 | --print("in context", unpack(self.ctx)) 913 | 914 | -- we are in a sub-namepsace context 915 | -- keep trying options by moving up the namespace: 916 | local ok, rulename 917 | for i = ctxlen, 1, -1 do 918 | -- generate a candidate template name 919 | -- by prepending segments of the current namepace: 920 | local candidate = format("%s.%s", concat(ctx, ".", 1, i), pathname) 921 | 922 | ok, rulename = pcall(self.reify, self, candidate) 923 | if ok then 924 | -- found a match, break here: 925 | return rulename 926 | end 927 | end 928 | 929 | -- try at the root level: 930 | local ok, rulename = pcall(self.reify, self, pathname) 931 | if ok then 932 | -- found a match! 933 | return rulename 934 | end 935 | 936 | -- if we got here, we didn't find a match: 937 | -- should this really be an error, or just return a null template? 938 | for k, v in pairs(self.definitions) do print("defined", k) end 939 | 940 | error(format("could not resolve template %s (%s) in context %s", 941 | pathname, rulename, concat(ctx, ".") 942 | )) 943 | end 944 | end 945 | 946 | -- env, apply_eval 947 | -- apply_eval e.g. a.(x).c 948 | function action:apply_eval(node, out) 949 | local env = self:dispatch(node[1], out) 950 | local tmp = self:dispatch(node[2], out) 951 | return action.apply_helper(self, tmp, env, out) 952 | end 953 | 954 | function action:iter_eval(node, out) 955 | local env = "env" 956 | local tmp = self:dispatch(node[1], out) 957 | return action.apply_helper(self, tmp, env, out) 958 | end 959 | 960 | function action:apply_inline_helper(body, env, out) 961 | local name = gensym("apply") 962 | out:comment("inline application:") 963 | out:format("local %s", name) 964 | :write("--- test") 965 | :write("do") 966 | :push() 967 | :format("local env = %s", env) 968 | :write("local out = {}") 969 | local rule = self:dispatch(body, out) 970 | out:format("%s = concat(out)", name) 971 | :pop() 972 | :write("end") 973 | return name 974 | end 975 | 976 | function action:apply_inline(node, out) 977 | local env = self:dispatch(node[1], out) 978 | return action.apply_inline_helper(self, node[2], env, out) 979 | end 980 | function action:iter_inline(node, out) 981 | return action.apply_inline_helper(self, node[1], "env", out) 982 | end 983 | 984 | function action:cond(node, out) 985 | 986 | local c = self:dispatch(node[1], out) 987 | --[==[ 988 | if(c == "env_formatted") then 989 | out:write("local tem__ = (type(env) == 'table') and env.template") 990 | out:write([[if(tem__ == 'unit.self.coordinates.norm') then 991 | print('THIS IS A unit.self.coordinates.norm') 992 | print("env_formatted", env_formatted) 993 | print("TEMPLATE:", env["template"]) 994 | end]]) 995 | end 996 | --]==] 997 | out:format("if %s then", c):push() 998 | local t = self:dispatch(node[2], out) 999 | if t then 1000 | out:format("out[#out+1] = %s", t) 1001 | end 1002 | out:pop() 1003 | if #node > 2 then 1004 | out("else"):push() 1005 | local f = self:dispatch(node[3], out) 1006 | if f then 1007 | out:format("out[#out+1] = %s", f) 1008 | end 1009 | out:pop() 1010 | end 1011 | out("end") 1012 | end 1013 | 1014 | function action:exists(node, out) 1015 | return action.lookup(self, node, out) 1016 | end 1017 | 1018 | function action:quoted(node, out) 1019 | return node[1] 1020 | end 1021 | 1022 | function action:unop(node, out) 1023 | local o = node.op 1024 | local b = self:dispatch(node[1], out) 1025 | if o == "?" then 1026 | return format("(%s ~= nil)", b) 1027 | elseif o == "#" then 1028 | return format("len(%s)", b) 1029 | else 1030 | return format("(%s %s)", o, b) 1031 | end 1032 | end 1033 | 1034 | function action:binop(node, out) 1035 | local a = self:dispatch(node[1], out) 1036 | local o = node.op 1037 | local b = self:dispatch(node[2], out) 1038 | return format("(%s %s %s)", a, o, b) 1039 | end 1040 | 1041 | function action:tuple(node, out) 1042 | local k = self:dispatch(node[1], out) 1043 | local v = self:dispatch(node[2], out) 1044 | return format("[%q] = %s", k, v) 1045 | end 1046 | 1047 | function action:env(node, out) 1048 | local name = gensym("env") 1049 | local terms = {} 1050 | for i, v in ipairs(node) do 1051 | terms[i] = self:dispatch(v, out) 1052 | end 1053 | out:comment("env create { %s } ", concat(terms, ", ")) 1054 | out:format("local %s = {", name) 1055 | :push() 1056 | for i, v in ipairs(terms) do 1057 | out(v .. ",") 1058 | end 1059 | out:pop() 1060 | :write("}") 1061 | return name 1062 | end 1063 | action.iter_env = action.env 1064 | 1065 | function action:inline_text(node, out) 1066 | out:format("out[#out+1] = %q", node[1]) 1067 | end 1068 | 1069 | function action:inline(node, out) 1070 | for i, v in ipairs(node) do 1071 | out:write( self:dispatch(v, out) ) 1072 | end 1073 | end 1074 | 1075 | function action:env_array(node, out) 1076 | local terms = {} 1077 | for i, v in ipairs(node) do 1078 | terms[i] = format("[%d] = %s,", i, self:dispatch(v, out)) 1079 | end 1080 | 1081 | local name = gensym("array") 1082 | out:format("local %s = {", name) 1083 | :push() 1084 | for i, v in ipairs(terms) do 1085 | out(v) 1086 | end 1087 | out:pop() 1088 | :write("}") 1089 | return name 1090 | end 1091 | 1092 | -- because %q also quotes escape sequences, which isn't what we want: 1093 | function action:literal(node, out) 1094 | local s = node[1] 1095 | local q 1096 | if s:find('"') then 1097 | q = format('[[%s]]', s) 1098 | else 1099 | q = format('"%s"', s) 1100 | end 1101 | return q 1102 | end 1103 | 1104 | -------------------------------------------------------------------------------- 1105 | -- Template constructor 1106 | -------------------------------------------------------------------------------- 1107 | 1108 | local header = [[ 1109 | -- GENERATED CODE: DO NOT EDIT! 1110 | local gen = ... 1111 | -- utils: 1112 | local format, gsub = string.format, string.gsub 1113 | local max = math.max 1114 | local newline = "\n" 1115 | -- need this because table.concat() does not call tostring(): 1116 | local tconcat = table.concat 1117 | local function concat(t, sep) 1118 | local t1 = {} 1119 | for i = 1, #t do t1[i] = tostring(t[i]) end 1120 | return tconcat(t1, sep) 1121 | end 1122 | -- need this because #s returns string length rather than numeric value: 1123 | local function len(t) 1124 | if type(t) == "table" then 1125 | return #t 1126 | else 1127 | return tonumber(t) or 0 1128 | end 1129 | end 1130 | 1131 | -- rules: -- 1132 | local rules = {} 1133 | ]] 1134 | 1135 | local footer = [[ 1136 | 1137 | -- result: 1138 | return rules 1139 | ]] 1140 | 1141 | local gen = {} 1142 | gen.__index = gen 1143 | 1144 | -- the main switch to select actions according to parse-tree nodes: 1145 | function gen:dispatch(node, out) 1146 | if type(node) == "table" then 1147 | local rule = node.rule 1148 | --print("rule", node.rule) 1149 | if rule and action[rule] then 1150 | -- invoke the corresponding action: 1151 | out:comment("action %s", rule) 1152 | local result = action[rule](self, node, out) 1153 | return result 1154 | else 1155 | tree_dump(node) 1156 | error("no rule "..tostring(rule)) 1157 | end 1158 | elseif type(node) == "string" then 1159 | return node 1160 | else 1161 | error( format("-- unexpected type %s", type(node)) ) 1162 | end 1163 | end 1164 | 1165 | -- the routine which generates code for a particular template rule name: 1166 | function gen:reify(name) 1167 | assert(name, "missing template name") 1168 | 1169 | -- only generate if needed: 1170 | local f = self.functions[name] 1171 | if f then return f end 1172 | 1173 | -- the code from which to generate it: 1174 | local src = self.definitions[name] 1175 | if not src then error("missing definition "..name) end 1176 | 1177 | -- parse it: 1178 | local node = grammar:match(src, 1, self) 1179 | if not node or type(node) ~= "table" then 1180 | error("parse failed for template "..name) 1181 | end 1182 | 1183 | self.ast[name] = node 1184 | 1185 | -- generate it: 1186 | local out = writer.create() 1187 | out:comment("rule %s:", name) 1188 | 1189 | local rulename = format("rules[%q]", name) 1190 | local localname 1191 | 1192 | -- header: 1193 | if self.numlocals < 200 then 1194 | self.numlocals = self.numlocals + 1 1195 | localname = "template_" .. name:gsub("(%.)", "_") 1196 | out:format("local function %s(env)", localname) 1197 | else 1198 | out:format("%s = function(env)", rulename) 1199 | end 1200 | out:push() 1201 | 1202 | if self.callbacks[name] then 1203 | out:format("local cb = gen.callbacks[%q]", name) 1204 | :write("if cb then") 1205 | :push() 1206 | :write("env = cb(env)") 1207 | :pop() 1208 | :write("end") 1209 | end 1210 | 1211 | -- push the template namepace context: 1212 | local oldctx = self.ctx 1213 | if type(name) == "string" then 1214 | local ctx = {} 1215 | for w in name:gmatch("[^.]+") do ctx[#ctx+1] = w end 1216 | self.ctx = ctx 1217 | end 1218 | 1219 | -- generate the function: 1220 | self:dispatch(node, out) 1221 | 1222 | -- restore the previous template namepace context: 1223 | self.ctx = oldctx 1224 | 1225 | -- footer: 1226 | out:pop() 1227 | :write("end") 1228 | 1229 | if localname then 1230 | out:format("rules[%q] = %s", name, localname) 1231 | -- prevent duplicates: 1232 | self.functions[name] = localname 1233 | else 1234 | -- prevent duplicates: 1235 | self.functions[name] = rulename 1236 | end 1237 | 1238 | -- synthesize result into root accumulator: 1239 | self.out( out:concat("\n") ) 1240 | 1241 | return rulename 1242 | end 1243 | 1244 | function gen:define(name, t, parent) 1245 | if name == 1 then name = "1" end 1246 | 1247 | if type(t) == "table" then 1248 | assert(t[1], "template table must have an entry at index 1") 1249 | local pre = name 1250 | if parent then 1251 | pre = format("%s.%s", parent, pre) 1252 | end 1253 | for k, v in pairs(t) do 1254 | if k == 1 or k == "1" then 1255 | self:define(name, v, parent) 1256 | else 1257 | self:define(k, v, pre) 1258 | end 1259 | end 1260 | elseif type(t) == "string" then 1261 | if parent then 1262 | local defname = format("%s.%s", parent, name) 1263 | self.definitions[defname] = t 1264 | else 1265 | self.definitions[name] = t 1266 | end 1267 | else 1268 | error("unexpected template type") 1269 | end 1270 | end 1271 | 1272 | -- syntactic sugar for template:define(name, code): 1273 | -- template[name] = code 1274 | gen.__newindex = gen.define 1275 | 1276 | 1277 | -- model is optional; if not given, it will reify but not apply 1278 | -- rulename is optional; if not given, it will assume rule[1] 1279 | function gen:gen(model, rulename) 1280 | rulename = rulename or "1" 1281 | 1282 | -- generate lazily: 1283 | local g = self.rules[rulename] 1284 | if not g then 1285 | 1286 | -- self.out is the top-level accumulator 1287 | -- it is generally used to accumulate the functions 1288 | -- (using rawset here because of gen:__newindex) 1289 | rawset(self, "out", writer.create()) 1290 | self.out(header) 1291 | 1292 | -- make sure the main rule is available 1293 | -- this will also generate any templates statically referred by main 1294 | -- in the correct ordering 1295 | 1296 | local main = self:reify("1") 1297 | 1298 | -- now reify all remaining rules 1299 | -- these will be available via dynamic template name evaluation 1300 | for k, v in pairs(self.definitions) do 1301 | self:reify(k) 1302 | end 1303 | 1304 | -- final: 1305 | self.out(footer) 1306 | 1307 | local code = self.out:concat("\n") 1308 | -- cache for debugging purposes: 1309 | rawset(self, "code", code) 1310 | 1311 | --printlines(code) 1312 | local f, err = loadstring(code) 1313 | if not f then 1314 | --printlines(code, 1, 100) 1315 | printlines(code, 1, 500) 1316 | print("template internal error", err) 1317 | end 1318 | 1319 | -- merge new rules with existing rules 1320 | local rules = assert( f(self), "construct error" ) 1321 | for k, v in pairs(rules) do 1322 | self.rules[k] = v 1323 | end 1324 | 1325 | -- now try again: 1326 | g = self.rules[rulename] 1327 | 1328 | if not g then 1329 | printlines(code, 1, 100) 1330 | error(format("template rule %s not defined", rulename)) 1331 | end 1332 | end 1333 | 1334 | -- invoke it immediately: 1335 | if model then 1336 | return g(model, "") 1337 | end 1338 | end 1339 | 1340 | function gen:register(name, cb) 1341 | self.callbacks[name] = cb 1342 | end 1343 | 1344 | -- call this to un-set the generated code, so that it can be regenerated 1345 | function gen:reset() 1346 | self.functions = {} 1347 | self.gen = nil 1348 | end 1349 | 1350 | function gen:dump(from, to) 1351 | printlines(self.code, from, to) 1352 | end 1353 | 1354 | function gen:ast_dump() 1355 | for k, v in pairs(self.ast) do 1356 | print("rule ", k) 1357 | tree_dump(v, nil, { start=true, finish=true }) 1358 | end 1359 | end 1360 | 1361 | -- return template constructor 1362 | -- optional argument to define the 'start' rule template 1363 | return function(startrule) 1364 | local template = setmetatable({ 1365 | -- the raw source code from which rules are created: 1366 | definitions = {}, 1367 | 1368 | -- the parsed AST thereof: 1369 | ast = {}, 1370 | 1371 | -- the synthesized implementation code thereof: 1372 | functions = {}, 1373 | 1374 | -- the generated generator functions thereof: 1375 | rules = {}, 1376 | 1377 | -- a way to remember the template namespace from which a rule is invoked: 1378 | -- (the call-frame, represented as an array of strings) 1379 | -- the root context is an empty list 1380 | ctx = {}, 1381 | 1382 | -- because Lua doesn't allow more then 200 locals in a chunk 1383 | -- here count how many have been generated 1384 | -- (including 10 from the header) 1385 | numlocals = 10, 1386 | 1387 | -- callbacks triggered during code-gen: 1388 | callbacks = {}, 1389 | 1390 | }, gen) 1391 | 1392 | if startrule then 1393 | if type(startrule) == "string" then 1394 | template:define("1", startrule) 1395 | else 1396 | for k, v in pairs(startrule) do 1397 | template:define(k, v) 1398 | end 1399 | end 1400 | end 1401 | 1402 | return template 1403 | end 1404 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Lust 2 | ==== 3 | 4 | Lua String Templates 5 | 6 | Overview 7 | --- 8 | Lust is a templating system for Lua loosely based on Terrence Parr's [StringTemplate](http://www.stringtemplate.org/). Lust has been designed to enable the simple expression of complex string synthesis problems. It is particularly well suited for generating strings from hierarchical data structures. Lust itself encompases a language for writing templates and an interpreter for applying the templates to a datastructure. 9 | 10 | ### Lust features 11 | * scoped templates 12 | * dynamic template dispatch based on runtime information 13 | * iteration through map and numeric loop mechanisms 14 | * conditional template application 15 | * whitespace indentation preservation 16 | * insertion of separator tokens between strings generated via iteration 17 | 18 | 19 | ### Authors 20 | Lust is developed by Wesley Smith and Graham Wakefield with the support of Cycling '74. 21 | 22 | ### License 23 | Lust is licensed under the standard [MIT license](http://www.lua.org/license.html) just like the Lua Language. 24 | 25 | ### Dependencies 26 | Lust depends on the [Lua Parsing Expression Grammar (LPEG)](http://www.inf.puc-rio.br/~roberto/lpeg/) module. 27 | 28 | 29 | High-Level Overview 30 | --- 31 | In Lust, you author a set of templates, giving each one a name. Templates can be grouped together by putting them in a common namespace. To use the templates for string synthesis, a datastructure is passed to the Lust object, which initiates the synthesis process by applying the root template rule to the datastructure. As the root rule invokes subtemplates, Lust walks the datastructure passed in by following the commands described by operators embedded in the templates. Lust walks a datastructure either by iterating over arrays of values or by looking at named fields. 32 | 33 | 34 | Lust Basics 35 | --- 36 | The most fundamental structures in Lust are *templates* and *environments*. Templates are named strings, and environments represent the set of values a template has access to. The environment of a template is just like the concept of scope in a programming language. It provides a set of named values that can be referenced and operated on. 37 | 38 | ### Stringification ### 39 | Stringification takes a value and renders it directly into a string. In Lust, the stringification operator is indicated by the $ symbol. 40 | 41 | ```lua 42 | -- $. applies the current environment: 43 | Lust([[$.]]):gen("hello") -- res: "hello" 44 | ``` 45 | 46 | ```lua 47 | -- $1 selects item from environment-as-array: 48 | Lust([[$1 $2]]):gen{ "hello", "world" } -- res: "hello world" 49 | ``` 50 | 51 | ```lua 52 | -- $foo selects item from environment-as-dictionary: 53 | Lust[[$foo $bar]]:gen{ foo="hello", bar="world" } -- res: "hello world" 54 | ``` 55 | 56 | ```lua 57 | -- $< > can be used to avoid ambiguity: 58 | Lust[[$<1>2 $bar]]:gen{ "hello", foo="world" } -- res: "hello2 worldbar" 59 | ``` 60 | 61 | ```lua 62 | -- selections can be constructed as paths into the environment: 63 | Lust[[$a.b.c $1.1.1]]:gen{ a={ b={ c="hello" } }, { { "world" } } } -- res: "hello world" 64 | Lust[[$a.1 $1.b]]:gen{ a={ "hello" }, { b="world" } } -- res: "hello world" 65 | ``` 66 | 67 | ```lua 68 | -- the # symbol prints the length of an environment selection: 69 | Lust[[$#.]]:gen{ 1, 2, 3 } -- res: "3" 70 | Lust[[$#foo.bar]]:gen{ foo={ bar={ 1, 2, 3 } } } -- res: "3" 71 | ``` 72 | 73 | ```lua 74 | -- selections can be resolved dynamically using (x): 75 | Lust[[$(x)]]:gen{ x="foo", foo="hello" } -- res: "hello" 76 | Lust[[$(x.y).1]]:gen{ x={ y="foo" }, foo={"hello"} } -- res: "hello" 77 | ``` 78 | 79 | 80 | ### Template Application ### 81 | Template application applies a template to a particular environment. The template invocation operator is indicated by the @ symbol. 82 | 83 | ```lua 84 | -- the @name invokes a statically named sub-template: 85 | local temp = Lust[[@child]] 86 | -- define a subtemplate: 87 | temp.child = "$1 to child" 88 | temp:gen{"hello"} -- res: "hello to child" 89 | ``` 90 | 91 | ```lua 92 | -- subtemplates can also be specified in the constructor-table: 93 | Lust{ 94 | [[@child]], 95 | child = "$1 to child", 96 | }:gen{"hello"} -- res: "hello to child" 97 | ``` 98 | 99 | ```lua 100 | -- subtemplate invocations can use < > to avoid ambiguity: 101 | Lust{ 102 | [[@hood]], 103 | child = "$1 to child", 104 | }:gen{"hello"} -- res: "hello to childhood" 105 | ``` 106 | 107 | ```lua 108 | -- subtemplates with subtemplates: 109 | Lust{ 110 | [[@child, @child.grandchild]], 111 | child = { 112 | "$1 to child", 113 | grandchild = "$1 to grandchild", 114 | }, 115 | }:gen{"hello"} -- res: "hello to child, hello to grandchild" 116 | ``` 117 | 118 | ```lua 119 | -- subtemplates with subtemplates (alternative naming): 120 | Lust{ 121 | [[@child, @child.grandchild]], 122 | child = "$1 to child", 123 | ["child.grandchild"] = "$1 to grandchild", 124 | }:gen{"hello"} -- res: "hello to child, hello to grandchild" 125 | ``` 126 | 127 | ```lua 128 | -- subtemplate names can also be resolved dynamically, according to model values, using (x): 129 | Lust{ 130 | [[@(x), @(y)]], 131 | child1 = "hello world", 132 | child2 = "hi" 133 | }:gen{ x="child1", y="child2" } -- res: "hello world, hi" 134 | ``` 135 | 136 | ```lua 137 | -- the environment passed to a subtemplate can be specifed as a child of the current environment: 138 | Lust{ 139 | [[@1:child @two:child]], 140 | child = [[$. child]], 141 | }:gen{ "one", two="two" } -- res: "one child two child" 142 | ``` 143 | 144 | ```lua 145 | -- the symbol . can be used to explicitly refer to the current environment: 146 | Lust{ 147 | [[@child == @.:child]], 148 | child = [[$1 child]], 149 | }:gen{ "hello" } -- res: "hello child == hello child" 150 | ``` 151 | 152 | ```lua 153 | -- subtemplate paths can mix static and dynamic terms: 154 | Lust{[[@child.(x), @(y).grandchild, @(a.b)]], 155 | child ="$1 to child", 156 | ["child.grandchild"] = "$1 to grandchild", 157 | }:gen{ 158 | x="grandchild", 159 | y="child", 160 | "hello", 161 | a = { b="child" } 162 | } -- res: "hello to grandchild, hello to grandchild, hello to child" 163 | ``` 164 | 165 | ```lua 166 | -- child environments can be specified using multi-part paths: 167 | Lust{ 168 | [[@a.1.foo:child]], 169 | child = [[$. child]], 170 | }:gen{ a={ { foo="hello" } } } -- res: "hello child" 171 | ``` 172 | 173 | ```lua 174 | -- subtemplates can be specified inline using @{{ }}: 175 | Lust([[@foo.bar:{{$1 $2}}]]):gen{ foo={ bar={ "hello", "world" } } } -- res: "hello world" 176 | ``` 177 | 178 | ```lua 179 | -- environments can also be specified dynamically 180 | -- the @{ } construction is similar to Lua table construction 181 | Lust([[@{ ., greeting="hello" }:{{$greeting $1.place}}]]):gen{ place="world" } -- res: "hello world" 182 | Lust([[@{ "hello", a.b.place }:{{$1 $2}}]]):gen{ a = { b = { place="world" } } } -- res: "hello world" 183 | Lust([[@{ 1, place=a.b }:{{$1 $place.1}}]]):gen{ "hello", a = { b = { "world" } } } -- res: "hello world" 184 | ``` 185 | 186 | ```lua 187 | -- dynamic environments can contain arrays: 188 | Lust([[@{ args=["hello", a.b] }:{{$args.1 $args.2.1}}]]):gen{ a = { b = { "world" } } } -- res: "hello world" 189 | ``` 190 | 191 | ```lua 192 | -- dynamic environments can contain subtemplate applications: 193 | Lust{ 194 | [[@{ .:child, a=x:child.grandchild }:{{$1, $a}}]], 195 | child = "$1 to child", 196 | ["child.grandchild"] = "$1 to grandchild", 197 | }:gen{ "hi", x = { "hello" } } -- res: "hi to child, hello to grandchild" 198 | ``` 199 | 200 | ```lua 201 | -- dynamic environments can be nested: 202 | Lust{ 203 | [[@{ { "hello" }, foo={ bar="world" } }:sub]], 204 | sub = [[$1.1 $foo.bar]], 205 | }:gen{} -- res: "hello world" 206 | ``` 207 | 208 | ## Conditions 209 | The @if condition takes a boolean expression and applies a template or value if it evaluates to true. If there is a corresponding else template/value, then it will be applied if the expression evaluates to false 210 | 211 | ```lua 212 | -- conditional templates have a conditional test followed by a template application 213 | -- @if(x) tests for the existence of x in the model 214 | local temp = Lust{ 215 | [[@if(x)]], 216 | greet = "hello", 217 | } 218 | temp:gen{ x=1 } -- res: "hello" 219 | temp:gen{ } -- res: "" 220 | ``` 221 | 222 | ```lua 223 | -- @if(?(x)) evaluates x in the model, and then checks if the result is a valid template name 224 | -- this example also demonstrates using dynamically evalutated template application: 225 | local temp = Lust{ 226 | [[@if(?(op))<(op)>]], 227 | child = "I am a child", 228 | } 229 | temp:gen{ op="child" } -- res: "I am a child" 230 | ``` 231 | 232 | ```lua 233 | -- using else and inline templates: 234 | local temp = Lust[[@if(x)<{{hello}}>else<{{bye bye}}>]] 235 | temp:gen{ x=1 } -- res: "hello" 236 | temp:gen{ } -- res: "bye bye" 237 | ``` 238 | 239 | ```lua 240 | -- @if(#x > n) tests that the number of items in the model term 'x' is greater than n: 241 | local temp = Lust[[@if(#. > "0")<{{at least one}}>]] 242 | temp:gen{ "a" } -- res: "at least one") 243 | temp:gen{ } -- res: "" 244 | ``` 245 | 246 | ```lua 247 | -- compound conditions: 248 | local temp = Lust[[@if(#x > "0" and #x < "5")<{{success}}>]] 249 | temp:gen{ x={ "a", "b", "c", "d" } } -- res: "success" 250 | temp:gen{ x={ "a", "b", "c", "d", "e" } } -- res: "" 251 | temp:gen{ x={ } } -- res: "" 252 | temp:gen{ } -- res: "" 253 | ``` 254 | 255 | ```lua 256 | -- compound conditions: 257 | local temp = Lust[[@if(x or not not not y)<{{success}}>else<{{fail}}>]] 258 | temp:gen{ x=1 } -- res: "success" 259 | temp:gen{ x=1, y=1 } -- res: "success" 260 | temp:gen{ y=1 } -- res: "fail" 261 | temp:gen{ } -- res: "success" 262 | ``` 263 | 264 | ```lua 265 | -- compound conditions: 266 | local temp = Lust[[@if(n*"2"+"1" > #x)<{{success}}>else<{{fail}}>]] 267 | temp:gen{ n=3, x = { "a", "b", "c" } } -- res: "success" 268 | temp:gen{ n=1, x = { "a", "b", "c" } } -- res: "fail" 269 | ``` 270 | 271 | 272 | ### Iteration 273 | 274 | Lust has two main methods for creating iteration statements: a map function and numeric iteration. For the @map function, there are a variety of ways that it can be called depdening on the situation 275 | 276 | ```lua 277 | -- @map can iterate over arrays in the environment: 278 | local temp = Lust[[@map{ n=numbers }:{{$n.name }}]] 279 | temp:gen{ 280 | numbers = { 281 | { name="one" }, 282 | { name="two" }, 283 | { name="three" }, 284 | } 285 | } 286 | -- result: "one two three " 287 | ``` 288 | 289 | ```lua 290 | -- assigning mapped values a name in the environment 291 | local temp = Lust[[@map{ n=numbers }:{{$n }}]] 292 | temp:gen{ 293 | numbers = { "one", "two", "three" } 294 | } 295 | -- result: "one two three " 296 | ``` 297 | 298 | ```lua 299 | -- the _separator field can be used to insert elements between items: 300 | local temp = Lust[[@map{ n=numbers, _separator=", " }:{{$n.name}}]] 301 | temp:gen{ 302 | numbers = { 303 | { name="one" }, 304 | { name="two" }, 305 | { name="three" }, 306 | } 307 | } 308 | -- result: "one, two, three" 309 | ``` 310 | 311 | ```lua 312 | -- _ can be used as a shorthand for _separator: 313 | local temp = Lust[[@map{ n=numbers, _=", " }:{{$n.name}}]] 314 | temp:gen{ 315 | numbers = { 316 | { name="one" }, 317 | { name="two" }, 318 | { name="three" }, 319 | } 320 | } 321 | -- result: "one, two, three" 322 | ``` 323 | 324 | ```lua 325 | -- a map can iterate over multiple arrays in parallel 326 | local temp = Lust[[@map{ a=letters, n=numbers, _=", " }:{{$a $n.name}}]] 327 | temp:gen{ 328 | numbers = { 329 | { name="one" }, 330 | { name="two" }, 331 | { name="three" }, 332 | }, 333 | letters = { 334 | "a", "b", "c", 335 | } 336 | } 337 | -- res: "a one, b two, c three" 338 | ``` 339 | 340 | ```lua 341 | -- if parallel mapped items have different lengths, the longest is used: 342 | local temp = Lust[[@map{ a=letters, n=numbers, _=", " }:{{$a $n.name}}]] 343 | temp:gen{ 344 | numbers = { 345 | { name="one" }, 346 | { name="two" }, 347 | { name="three" }, 348 | }, 349 | letters = { 350 | "a", "b", "c", "d", 351 | } 352 | } 353 | -- res: "a one, b two, c three, d " 354 | ``` 355 | 356 | ```lua 357 | -- if parallel mapped items are not arrays, they are repeated each time: 358 | local temp = Lust[[@map{ a=letters, n=numbers, prefix="hello", count=#letters, _=", " }:{{$prefix $a $n.name of $count}}]] 359 | temp:gen{ 360 | numbers = { 361 | { name="one" }, 362 | { name="two" }, 363 | { name="three" }, 364 | }, 365 | letters = { 366 | "a", "b", "c", "d", 367 | } 368 | } 369 | -- res: "hello a one of 4, hello b two of 4, hello c three of 4, hello d of 4" 370 | ``` 371 | 372 | ```lua 373 | -- the 'i1' and 'i0' fields are added automatically for one- and zero-based array indices: 374 | local temp = Lust[[@map{ n=numbers }:{{$i0-$i1 $n.name }}]] 375 | temp:gen{ 376 | numbers = { 377 | { name="one" }, 378 | { name="two" }, 379 | { name="three" }, 380 | } 381 | } 382 | -- res: "0-1 one 1-2 two 2-3 three " 383 | ``` 384 | 385 | ```lua 386 | -- if the map only contains an un-named array, each item of the array becomes the environment applied in each iteration: 387 | local temp = Lust[["@map{ ., _separator='", "' }:{{$name}}"]] 388 | temp:gen{ 389 | { name="one" }, 390 | { name="two" }, 391 | { name="three" }, 392 | } 393 | -- res: '"one", "two", "three"' 394 | 395 | local temp = Lust[[@map{ numbers, count=#numbers, _separator=", " }:{{$name of $count}}]] 396 | temp:gen{ 397 | numbers = { 398 | { name="one" }, 399 | { name="two" }, 400 | { name="three" }, 401 | } 402 | } 403 | -- res: "one of 3, two of 3, three of 3" 404 | ``` 405 | 406 | ```lua 407 | -- @rest is like @map, but starts from the 2nd item: 408 | local temp = Lust[[@rest{ a=letters, n=numbers, _separator=", " }:{{$a $n.name}}]] 409 | temp:gen{ 410 | numbers = { 411 | { name="one" }, 412 | { name="two" }, 413 | { name="three" }, 414 | }, 415 | letters = { 416 | "a", "b", "c", 417 | } 418 | } 419 | -- res: "b two, c three" 420 | ``` 421 | 422 | ```lua 423 | -- @iter can be used for an explicit number of repetitions: 424 | local temp = Lust[[@iter{ "3" }:{{repeat $i1 }}]] 425 | temp:gen{} -- res: "repeat 1 repeat 2 repeat 3 " 426 | ``` 427 | 428 | ```lua 429 | -- again, _separator works: 430 | local temp = Lust[[@iter{ "3", _separator=", " }:{{repeat $i1}}]] 431 | temp:gen{} -- res: "repeat 1, repeat 2, repeat 3" 432 | ``` 433 | 434 | ```lua 435 | -- @iter can take an array item; it will use the length of that item: 436 | local temp = Lust[[@iter{ numbers, _separator=", " }:{{repeat $i1}}]] 437 | temp:gen{ 438 | numbers = { 439 | { name="one" }, 440 | { name="two" }, 441 | { name="three" }, 442 | } 443 | } 444 | -- res: "repeat 1, repeat 2, repeat 3" 445 | 446 | ``` 447 | 448 | ```lua 449 | -- @iter can take a range for start and end values: 450 | Lust([[@iter{ ["2", "3"] }:{{repeat $i1 }}]]):gen{} 451 | -- res: "repeat 2 repeat 3 " 452 | ``` 453 | 454 | ```lua 455 | local temp = Lust[[@iter{ ["2", numbers], _separator=", " }:{{repeat $i1}}]] 456 | temp:gen{ 457 | numbers = { 458 | { name="one" }, 459 | { name="two" }, 460 | { name="three" }, 461 | } 462 | } 463 | -- res: "repeat 2, repeat 3" 464 | ``` 465 | 466 | ### Indentation 467 | If template application is preceded by only whitespace on a given line, every line the template generates will be indented to the same level as the template application. 468 | 469 | ```lua 470 | -- helper function to un-escape \n and \t in Lua's long string 471 | local function nl(str) return string.gsub(str, [[\n]], "\n"):gsub([[\t]], "\t") end 472 | 473 | -- if a template application occurs after whitespace indentation, 474 | -- any generated newlines will repeat this indentation: 475 | local temp = Lust{nl[[ 476 | @iter{ "3", _separator="\n" }:child]], 477 | child = [[line $i1]], 478 | } 479 | --[=[ res: [[ 480 | line 1 481 | line 2 482 | line 3]] 483 | --]=] 484 | ``` 485 | 486 | 487 | ### Handler Registration 488 | For special cases, handlers can be associated with a template for runtime modification of the template's environment. 489 | 490 | ```lua 491 | -- a handler can be registered for a named template 492 | -- the handler allows a run-time modification of the environment: 493 | local temp = Lust{ 494 | [[@child]], 495 | child = [[$1]], 496 | } 497 | local model = { "foo" } 498 | local function double_env(env) 499 | -- create a new env: 500 | local ee = env[1] .. env[1] 501 | return { ee } 502 | end 503 | temp:register("child", double_env) 504 | -- res: "foofoo" 505 | ``` -------------------------------------------------------------------------------- /examples/dispatch.lua: -------------------------------------------------------------------------------- 1 | local Lust = require"Lust" 2 | 3 | local function nl(str) return string.gsub(str, [[\n]], "\n"):gsub([[\t]], "\t") end 4 | 5 | local temp = Lust{ 6 | [1] = "@dispatch", 7 | dispatch = [[@if(rule)<{{@(rule)}}>else<{{$1}}>]], 8 | statlist = nl[[@map{statements, _="\n"}:dispatch]], 9 | stat = [[@if(outputs)<{{@map{outputs, _=","}:dispatch = }}>@map{inputs, _=","}:dispatch;]], 10 | binop = [[@map{inputs, _=op}:dispatch]], 11 | fcall = [[$name(@map{inputs, _=", "}:dispatch)]], 12 | } 13 | 14 | print(temp:gen{ 15 | rule = "statlist", 16 | statements = { 17 | { 18 | rule = "stat", 19 | outputs = {"res1"}, 20 | inputs = { 21 | { 22 | rule = "binop", 23 | op = "+", 24 | inputs = { 25 | { 26 | rule = "binop", 27 | op="*", 28 | inputs = {"a", "b"}, 29 | }, 30 | "c" 31 | } 32 | }, 33 | } 34 | }, 35 | { 36 | rule = "stat", 37 | inputs = { 38 | { 39 | rule = "fcall", 40 | name = "test", 41 | inputs = {1, 2, 3}, 42 | }, 43 | } 44 | } 45 | } 46 | }) -------------------------------------------------------------------------------- /examples/tests.lua: -------------------------------------------------------------------------------- 1 | LuaAV.addmodulepath(script.path.."/..") 2 | local Lust = require"Lust" 3 | 4 | -------------------------------------------------------------------------------------------------- 5 | -- substitutions: 6 | -------------------------------------------------------------------------------------------------- 7 | 8 | local function test(patt, model, result) 9 | local temp = Lust(patt) 10 | local ok, str = pcall(temp.gen, temp, model) 11 | if not ok then 12 | pcall(temp.dump, temp) 13 | error(str) 14 | elseif (str ~= result) then 15 | temp:ast_dump() 16 | temp:dump() 17 | print(str) 18 | error("test failed", 2) 19 | end 20 | end 21 | 22 | -- $. applies the current environment: 23 | test([[$.]], "hello", "hello") 24 | 25 | -- $1 selects item from environment-as-array: 26 | test([[$1 $2]], { "hello", "world" }, "hello world") 27 | 28 | -- $foo selects item from environment-as-dictionary: 29 | test([[$foo $bar]], { foo="hello", bar="world" }, "hello world") 30 | 31 | -- $< > can be used to avoid ambiguity: 32 | test([[$<1>2 $bar]], { "hello", foo="world" }, "hello2 worldbar") 33 | 34 | -- selections can be constructed as paths into the environment: 35 | test([[$a.b.c $1.1.1]], { a={ b={ c="hello" } }, { { "world" } } }, "hello world") 36 | test([[$a.1 $1.b]], { a={ "hello" }, { b="world" } }, "hello world") 37 | 38 | -- the # symbol prints the length of an environment selection: 39 | test([[$#.]], { 1, 2, 3 }, "3") 40 | test([[$#foo.bar]], { foo={ bar={ 1, 2, 3 } } }, "3") 41 | 42 | -- selections can be resolved dynamically using (x): 43 | test([[$(x)]], { x="foo", foo="hello" }, "hello") 44 | test([[$(x.y).1]], { x={ y="foo" }, foo={"hello"} }, "hello") 45 | 46 | -------------------------------------------------------------------------------------------------- 47 | -- sub-template applications: 48 | -------------------------------------------------------------------------------------------------- 49 | 50 | -- the @name invokes a statically named sub-template: 51 | local temp = Lust[[@child]] 52 | -- define a subtemplate: 53 | temp.child = "$1 to child" 54 | assert(temp:gen{"hello"} == "hello to child") 55 | 56 | -- subtemplates can also be specified in the constructor-table: 57 | local temp = { 58 | [[@child]], 59 | child = { 60 | "$1 to child", 61 | }, 62 | } 63 | test(temp, {"hello"}, "hello to child") 64 | 65 | -- subtemplate invocations can use < > to avoid ambiguity: 66 | local temp = { 67 | [[@hood]], 68 | child = { 69 | "$1 to child", 70 | }, 71 | } 72 | test(temp, {"hello"}, "hello to childhood") 73 | 74 | -- subtemplates with subtemplates: 75 | local temp = { 76 | [[@child, @child.grandchild]], 77 | child = { 78 | "$1 to child", 79 | grandchild = "$1 to grandchild", 80 | }, 81 | } 82 | test(temp, {"hello"}, "hello to child, hello to grandchild") 83 | 84 | local temp = { 85 | [[@child, @child.grandchild]], 86 | child = "$1 to child", 87 | ["child.grandchild"] = "$1 to grandchild", 88 | } 89 | test(temp, {"hello"}, "hello to child, hello to grandchild") 90 | 91 | -- subtemplate names can also be resolved dynamically, according to model values, using (x): 92 | local temp = { 93 | [[@(x), @(y)]], 94 | child1 = { "hello world", }, 95 | child2 = { "hi" }, 96 | } 97 | test(temp, { x="child1", y="child2" }, "hello world, hi") 98 | 99 | -- subtemplate paths can mix static and dynamic terms: 100 | local temp = { 101 | [[@child.(x), @(y).grandchild, @(a.b)]], 102 | child = { 103 | "$1 to child", 104 | grandchild = "$1 to grandchild", 105 | }, 106 | } 107 | local model = { 108 | x="grandchild", 109 | y="child", 110 | "hello", 111 | a = { b="child" } 112 | } 113 | test(temp, model, "hello to grandchild, hello to grandchild, hello to child" ) 114 | 115 | -- the environment passed to a subtemplate can be specifed as a child of the current environment: 116 | local temp = { 117 | [[@1:child @two:child]], 118 | child = [[$. child]], 119 | } 120 | test(temp, { "one", two="two" }, "one child two child") 121 | 122 | -- the symbol . can be used to explicitly refer to the current environment: 123 | local temp = { 124 | [[@child == @.:child]], 125 | child = [[$1 child]], 126 | } 127 | test(temp, { "hello" }, "hello child == hello child") 128 | 129 | -- child environments can be specified using multi-part paths: 130 | local temp = { 131 | [[@a.1.foo:child]], 132 | child = [[$. child]], 133 | } 134 | test(temp, { a={ { foo="hello" } } }, "hello child") 135 | 136 | local temp = { 137 | [[@a.1.foo:child, @a.1.foo:child.grandchild]], 138 | child = { 139 | "$. to child", 140 | grandchild = "$. to grandchild", 141 | }, 142 | } 143 | test(temp, { a={ { foo="hello" } } }, "hello to child, hello to grandchild") 144 | 145 | local temp = { 146 | [[@a.1.foo:child, @a.1.foo:child.grandchild, @a.1.foo:(x.y)]], 147 | child = { 148 | "$. to child", 149 | grandchild = "$. to grandchild", 150 | }, 151 | } 152 | local model = { a={ { foo="hello" } }, x={ y="child" } } 153 | test(temp, model, "hello to child, hello to grandchild, hello to child") 154 | 155 | -- subtemplates can be specified inline using @{{ }}: 156 | test([[@{{$1 $2}}]], { "hello", "world" }, "hello world") 157 | -- this is more useful for dynamic environments etc: 158 | test([[@foo.bar:{{$1 $2}}]], { foo={ bar={ "hello", "world" } } }, "hello world") 159 | 160 | -- environments can also be specified dynamically 161 | -- the @{ } construction is similar to Lua table construction 162 | local temp = [[@{ ., greeting="hello" }:{{$greeting $1.place}}]] 163 | test(temp, { place="world" }, "hello world") 164 | 165 | local temp = [[@{ "hello", a.b.place }:{{$1 $2}}]] 166 | test(temp, { a = { b = { place="world" } } }, "hello world") 167 | 168 | local temp = [[@{ 1, place=a.b }:{{$1 $place.1}}]] 169 | test(temp, { "hello", a = { b = { "world" } } }, "hello world") 170 | 171 | -- dynamic environments can contain arrays: 172 | local temp = [[@{ args=["hello", a.b] }:{{$args.1 $args.2.1}}]] 173 | test(temp, { a = { b = { "world" } } }, "hello world") 174 | 175 | -- dynamic environments can contain subtemplate applications: 176 | local temp = { 177 | [[@{ .:child, a=x:child.grandchild }:{{$1, $a}}]], 178 | child = { 179 | "$1 to child", 180 | grandchild = "$1 to grandchild", 181 | }, 182 | } 183 | test(temp, { "hi", x = { "hello" } }, "hi to child, hello to grandchild") 184 | 185 | -- dynamic environments can be nested: 186 | local temp = { 187 | [[@{ { "hello" }, foo={ bar="world" } }:sub]], 188 | sub = [[$1.1 $foo.bar]], 189 | } 190 | test(temp, {}, "hello world") 191 | 192 | -- conditional templates have a conditional test followed by a template application 193 | -- @if(x) tests for the existence of x in the model 194 | local temp = { 195 | [[@if(x)]], 196 | greet = "hello", 197 | } 198 | test(temp, { x=1 }, "hello") 199 | test(temp, { }, "") 200 | 201 | local temp = { 202 | [[@if(1)]], 203 | greet = "hello", 204 | } 205 | test(temp, { "something" }, "hello") 206 | test(temp, { }, "") 207 | 208 | -- @if(?(x)) evaluates x in the model, and then checks if the result is a valid template name 209 | -- this example also demonstrates using dynamically evalutated template application: 210 | local temp = { 211 | [[@if(?(op))<(op)>]], 212 | child = "I am a child", 213 | } 214 | test(temp, { op="child" }, "I am a child") 215 | 216 | -- using else and inline templates: 217 | local temp = [[@if(x)<{{hello}}>else<{{bye bye}}>]] 218 | test(temp, { x=1 }, "hello") 219 | test(temp, { }, "bye bye") 220 | 221 | -- @if(#x > n) tests that the number of items in the model term 'x' is greater than n: 222 | local temp = [[@if(#. > "0")<{{at least one}}>]] 223 | test(temp, { "a", }, "at least one") 224 | test(temp, { }, "") 225 | 226 | -- compound conditions: 227 | local temp = [[@if(#x > "0" and #x < "5")<{{success}}>]] 228 | test(temp, { x={ "a", "b", "c", "d" } }, "success") 229 | test(temp, { x={ "a", "b", "c", "d", "e" } }, "") 230 | test(temp, { x={ } }, "") 231 | test(temp, { }, "") 232 | 233 | local temp = [[@if(x or not not not y)<{{success}}>else<{{fail}}>]] 234 | test(temp, { x=1 }, "success") 235 | test(temp, { x=1, y=1 }, "success") 236 | test(temp, { y=1 }, "fail") 237 | test(temp, { }, "success") 238 | 239 | local temp = [[@if(n*"2"+"1" > #x)<{{success}}>else<{{fail}}>]] 240 | test(temp, { n=3, x = { "a", "b", "c" } }, "success") 241 | test(temp, { n=1, x = { "a", "b", "c" } }, "fail") 242 | 243 | -- @map can iterate over arrays in the environment: 244 | local temp = [[@map{ n=numbers }:{{$n.name }}]] 245 | local model = { 246 | numbers = { 247 | { name="one" }, 248 | { name="two" }, 249 | { name="three" }, 250 | } 251 | } 252 | test(temp, model, "one two three ") 253 | 254 | local temp = [[@map{ n=numbers }:{{$n }}]] 255 | local model = { 256 | numbers = { "one", "two", "three" } 257 | } 258 | test(temp, model, "one two three ") 259 | 260 | -- the _separator field can be used to insert elements between items: 261 | local temp = [[@map{ n=numbers, _separator=", " }:{{$n.name}}]] 262 | local model = { 263 | numbers = { 264 | { name="one" }, 265 | { name="two" }, 266 | { name="three" }, 267 | } 268 | } 269 | test(temp, model, "one, two, three") 270 | 271 | -- _ can be used as a shorthand for _separator: 272 | local temp = [[@map{ n=numbers, _=", " }:{{$n.name}}]] 273 | local model = { 274 | numbers = { 275 | { name="one" }, 276 | { name="two" }, 277 | { name="three" }, 278 | } 279 | } 280 | test(temp, model, "one, two, three") 281 | 282 | -- a map can iterate over multiple arrays in parallel 283 | local temp = [[@map{ a=letters, n=numbers, _=", " }:{{$a $n.name}}]] 284 | local model = { 285 | numbers = { 286 | { name="one" }, 287 | { name="two" }, 288 | { name="three" }, 289 | }, 290 | letters = { 291 | "a", "b", "c", 292 | } 293 | } 294 | test(temp, model, "a one, b two, c three") 295 | 296 | -- if parallel mapped items have different lengths, the longest is used: 297 | local temp = [[@map{ a=letters, n=numbers, _=", " }:{{$a $n.name}}]] 298 | local model = { 299 | numbers = { 300 | { name="one" }, 301 | { name="two" }, 302 | { name="three" }, 303 | }, 304 | letters = { 305 | "a", "b", "c", "d", 306 | } 307 | } 308 | test(temp, model, "a one, b two, c three, d ") 309 | 310 | -- if parallel mapped items are not arrays, they are repeated each time: 311 | local temp = [[@map{ a=letters, n=numbers, prefix="hello", count=#letters, _=", " }:{{$prefix $a $n.name of $count}}]] 312 | local model = { 313 | numbers = { 314 | { name="one" }, 315 | { name="two" }, 316 | { name="three" }, 317 | }, 318 | letters = { 319 | "a", "b", "c", "d", 320 | } 321 | } 322 | test(temp, model, "hello a one of 4, hello b two of 4, hello c three of 4, hello d of 4") 323 | 324 | -- the 'i1' and 'i0' fields are added automatically for one- and zero-based array indices: 325 | local temp = [[@map{ n=numbers }:{{$i0-$i1 $n.name }}]] 326 | local model = { 327 | numbers = { 328 | { name="one" }, 329 | { name="two" }, 330 | { name="three" }, 331 | } 332 | } 333 | test(temp, model, "0-1 one 1-2 two 2-3 three ") 334 | 335 | 336 | -- if the map only contains an un-named array, each item of the array becomes the environment applied in each iteration: 337 | local temp = [["@map{ ., _separator='", "' }:{{$name}}"]] 338 | local model = { 339 | { name="one" }, 340 | { name="two" }, 341 | { name="three" }, 342 | } 343 | test(temp, model, '"one", "two", "three"') 344 | 345 | local temp = [[@map{ numbers, count=#numbers, _separator=", " }:{{$name of $count}}]] 346 | local model = { 347 | numbers = { 348 | { name="one" }, 349 | { name="two" }, 350 | { name="three" }, 351 | } 352 | } 353 | test(temp, model, "one of 3, two of 3, three of 3") 354 | 355 | -- @rest is like @map, but starts from the 2nd item: 356 | local temp = [[@rest{ a=letters, n=numbers, _separator=", " }:{{$a $n.name}}]] 357 | local model = { 358 | numbers = { 359 | { name="one" }, 360 | { name="two" }, 361 | { name="three" }, 362 | }, 363 | letters = { 364 | "a", "b", "c", 365 | } 366 | } 367 | test(temp, model, "b two, c three") 368 | 369 | -- @iter can be used for an explicit number of repetitions: 370 | local temp = [[@iter{ "3" }:{{repeat $i1 }}]] 371 | test(temp, {}, "repeat 1 repeat 2 repeat 3 ") 372 | 373 | -- again, _separator works: 374 | local temp = [[@iter{ "3", _separator=", " }:{{repeat $i1}}]] 375 | test(temp, {}, "repeat 1, repeat 2, repeat 3") 376 | 377 | -- @iter can take an array item; it will use the length of that item: 378 | local temp = [[@iter{ numbers, _separator=", " }:{{repeat $i1}}]] 379 | local model = { 380 | numbers = { 381 | { name="one" }, 382 | { name="two" }, 383 | { name="three" }, 384 | } 385 | } 386 | test(temp, model, "repeat 1, repeat 2, repeat 3") 387 | 388 | -- @iter can take a range for start and end values: 389 | local temp = [[@iter{ ["2", "3"] }:{{repeat $i1 }}]] 390 | test(temp, {}, "repeat 2 repeat 3 ") 391 | 392 | local temp = [[@iter{ ["2", numbers], _separator=", " }:{{repeat $i1}}]] 393 | test(temp, model, "repeat 2, repeat 3") 394 | 395 | -- a handler can be registered for a named template 396 | -- the handler allows a run-time modification of the environment: 397 | local temp = Lust{ 398 | [[@child]], 399 | child = [[$1]], 400 | } 401 | local model = { "foo" } 402 | local function double_env(env) 403 | -- create a new env: 404 | local ee = env[1] .. env[1] 405 | return { ee } 406 | end 407 | temp:register("child", double_env) 408 | assert(temp:gen(model) == "foofoo") 409 | 410 | 411 | ---[===[ 412 | 413 | -- indentation: 414 | -- if a template application occurs after whitespace indentation, 415 | -- any generated newlines will repeat this indentation: 416 | local function nl(str) return string.gsub(str, [[\n]], "\n"):gsub([[\t]], "\t") end 417 | local temp = {nl[[ 418 | @iter{ "3", _separator="\n" }:child]], 419 | child = [[line $i1]], 420 | } 421 | test(temp, {}, [[ 422 | line 1 423 | line 2 424 | line 3]]) 425 | 426 | local temp = { 427 | nl[[ 428 | @iter{ "3", _="\n" }:row]], 429 | row = nl[[row $i1: 430 | @iter{ "2", _="\n"}:column]], 431 | column = [[col $i1]], 432 | } 433 | test(temp, {}, [[ 434 | row 1: 435 | col 1 436 | col 2 437 | row 2: 438 | col 1 439 | col 2 440 | row 3: 441 | col 1 442 | col 2]]) 443 | 444 | 445 | --]===] 446 | --[[ 447 | 448 | TODO: 449 | 450 | test inheritence & loop nesting 451 | 452 | Thoughts: 453 | 454 | A template should be able invoke itself recursively? 455 | 456 | This is a common pattern for defaults, 457 | perhaps we can add a syntax to make it simpler? 458 | @if(min)<{{$min}}>else<{{0}}> 459 | 460 | Another common idiom, which could benefit from a shorthand: 461 | @if(?x)<{{x}}> => @x? or @?x 462 | @if(?x)<{{x}}>else => @x?foo or @?x/foo 463 | 464 | This idiom was also common: @map{ v=. }:{{$v}} 465 | but perhaps it could simply be @map{.}:{{$.}} ?? 466 | 467 | What does $. mean when the current env is a table? 468 | (what does $1 mean when the current env is a string?) 469 | 470 | What if we also allowed Lua functions as RHS of templates? 471 | 472 | --]] 473 | 474 | 475 | print("tests succeeded") --------------------------------------------------------------------------------