├── README ├── lutem.lua ├── test.lua ├── test.tmpl └── test_sub.tmpl /README: -------------------------------------------------------------------------------- 1 | Welcome 2 | -------------------------- 3 | lutem (short for LUa TEMplate engine) is a template render 4 | engine like jinja2(a famous template engine written in Python) 5 | It's used for auto code generation, dynamic html page generation 6 | or other kinds of dynamic text generation 7 | 8 | 9 | Template Syntax 10 | --------------------------- 11 | The syntax is almost the same with jinja2 or Django template, 12 | 13 | Loop generate: 14 | 15 | {% for k in mp %} 16 | ... --this area would be repeated 17 | {% endfor %} 18 | 19 | variable replacement: 20 | {{ variable/raw }} 21 | variable example: {{ xxx }} {{ tbl.v }} 22 | special raw: {{ '{{' }} 23 | 24 | 25 | Template Inheritance 26 | -------------------------------- 27 | child template inheritance by parent template: 28 | {% extends abc.tmpl %} 29 | 30 | Declare block: 31 | {% block blockname %} 32 | ... 33 | {% endblock %} 34 | 35 | The child template will overwrite the parent's block content 36 | if correspond block name declared in child template 37 | 38 | 39 | Example 40 | -------------------------- 41 | See the test.lua and test.tmpl 42 | 43 | 44 | -------------------------------------------------------------------------------- /lutem.lua: -------------------------------------------------------------------------------- 1 | -- lutem - lua template engine 2 | -- @Copyright Daly 3 | -- 4 | 5 | --node type define 6 | local NODE_TEXT = 1 7 | local NODE_FOR = 2 8 | local NODE_EXPR = 3 9 | local NODE_BLOCK = 4 10 | local NODE_RAW = 5 11 | 12 | ast_node = { 13 | node_type = NODE_BLOCK, 14 | child_ = {}, 15 | parent_ = nil, 16 | 17 | lno_ = 0, --start line no 18 | content = "", --raw content, different nodes has different meaning 19 | } 20 | 21 | lutem = { 22 | output_ = {}, --render output buffer 23 | state_ = 0, --finish parsing or not 24 | args_ = {}, 25 | 26 | lineno_ = 0, --line count 27 | node_root_ = nil, --block 28 | blocks_ = {}, 29 | 30 | involve_file_ = {}, 31 | file_queue_ = {}, --inherit by extends 32 | path_root_ = "./", 33 | } 34 | 35 | 36 | --utils 37 | local function obj_copy(t1) 38 | local tb = {} 39 | for k,v in pairs(t1) do 40 | if type(v) == "table" then 41 | tb[k] = obj_copy(v) 42 | else 43 | tb[k] = v 44 | end 45 | end 46 | return tb 47 | end 48 | 49 | local function new_ast_node(ntype, parent, content) 50 | tb = obj_copy(ast_node) 51 | tb.parent_ = parent 52 | tb.node_type = ntype 53 | tb.content = content 54 | return tb 55 | end 56 | 57 | function lutem:new() 58 | local o = {} 59 | o.output_ = {} 60 | o.args_ = {} 61 | o.node_root_ = nil 62 | o.blocks_ = {} 63 | o.involve_file_ = {} 64 | o.file_queue_ = {} 65 | 66 | setmetatable(o, self) 67 | self.__index = self 68 | return o 69 | end 70 | 71 | -- parse command in {% %} 72 | -- return: (cmd, arglist) 73 | -- cmd -- command name 74 | -- arglist -- an array of arglist(according to command) 75 | local function parse_instr(s) 76 | local nf = 0 77 | local cmd = nil 78 | local arglist = {} 79 | local arr_token = {} 80 | for f in string.gmatch(s, "([^ \t\r\n]+)") do 81 | table.insert(arr_token, f) 82 | nf = nf + 1 83 | end 84 | --check token 85 | if nf < 1 then return "", -1 end 86 | cmd = arr_token[1] 87 | if cmd == "for" then 88 | if nf ~= 4 and nf ~= 5 then return "",{} end 89 | if arr_token[nf-1] ~= "in" then return "",{} end 90 | if nf == 5 then 91 | --maybe has space between iter key and value, join them 92 | table.insert(arglist, arr_token[2]..arr_token[3]) 93 | else 94 | table.insert(arglist, arr_token[2]) 95 | end 96 | 97 | table.insert(arglist, arr_token[nf]) 98 | elseif cmd == "endfor" or cmd == "endblock" then 99 | --no param 100 | if nf > 1 then return "",{} end 101 | elseif cmd == "extend" or cmd == "block" then 102 | --only 1 param 103 | if nf > 2 then return "",{} end 104 | table.insert(arglist, arr_token[2]) 105 | end 106 | return cmd, arglist 107 | end 108 | 109 | local function print_node(node, prefix) 110 | if node.node_type == NODE_FOR then 111 | print(prefix .. " " .. node.content[2]) 112 | else 113 | print(prefix .. " " .. node.content) 114 | end 115 | end 116 | 117 | function lutem:parse_file(filename, path) 118 | srclines = {} 119 | local f, err = io.open(self.path_root_..filename, 'r') 120 | if f == nil then return -1,"parse file error "..filename end 121 | 122 | for line in f:lines() do 123 | table.insert(srclines, line .. "\n") 124 | end 125 | f:close() 126 | 127 | --compile it 128 | local node = nil 129 | local extend_from = nil 130 | local cur_block = new_ast_node(NODE_BLOCK, nil, "__root") 131 | local cur_parent = cur_block 132 | local cur_text = new_ast_node(NODE_TEXT, cur_parent, "") 133 | self.blocks_["__root"] = cur_parent 134 | self.node_root_ = cur_parent 135 | 136 | local cur_lno = 1 137 | local lex_bstart = '{[{%%]' 138 | local pos_s, pos_e, pos_tmp 139 | local last = 1 140 | local i,j 141 | local bstack = {} --block / for stack 142 | local pre, word, cmd, arglist 143 | local skip_block = 0 144 | table.insert(bstack, cur_parent) 145 | for lno,text in ipairs(srclines) do 146 | while (last <= #text) do 147 | --skip extended block 148 | if skip_block == 1 then 149 | i, j = string.find(text, "{%%[ ]*endblock[ ]*%%}", last) 150 | if i == nil then 151 | break 152 | else 153 | last = i 154 | end 155 | end 156 | 157 | pos_s = string.find(text, "{[{%%]", last) 158 | if pos_s == nil then 159 | 160 | if #(cur_text.content) < 1000 then 161 | cur_text.content = cur_text.content .. string.sub(text, last) 162 | else 163 | table.insert(cur_parent.child_, cur_text) 164 | cur_text = new_ast_node(NODE_TEXT, cur_parent, string.sub(text, last)) 165 | end 166 | break 167 | end 168 | 169 | --while found {{ or {% 170 | 171 | cur_text.content = cur_text.content .. string.sub(text, last, pos_s-1) 172 | table.insert(cur_parent.child_, cur_text) 173 | cur_text = new_ast_node(NODE_TEXT, cur_parent, "") 174 | pre = string.sub(text, pos_s, pos_s + 1) 175 | last = pos_s + 2 176 | if pre == '{{' then 177 | i, j = string.find(text, "[ ]*'[^']+'[ ]*}}", last) 178 | if i == last then 179 | word = string.match(text, "'[^']+'", i, j-2) 180 | node = new_ast_node(NODE_RAW, cur_parent, string.sub(word, 2, -2)) 181 | else 182 | i, j = string.find(text, "[ ]*[%w._]+[ ]*}}", last) 183 | if i ~= last then return -1, "expr error: "..cur_lno end 184 | word = string.match(text, "[%w._]+", i, j-2) 185 | node = new_ast_node(NODE_EXPR, cur_parent, word) 186 | end 187 | last = j + 1 188 | table.insert(cur_parent.child_, node) 189 | else 190 | -- parse command 191 | i, j = string.find(text, "[%w/._%- ]+%%}", last) 192 | if i ~= last then return -1, "command error "..cur_lno end 193 | cmd, arglist = parse_instr(string.sub(text, i, j-2)) 194 | if cmd == "" then return -1, "command syntax error "..cur_lno end 195 | last = j + 1 196 | 197 | if cmd == "for" then 198 | node = new_ast_node(NODE_FOR, cur_parent, arglist) 199 | table.insert(cur_parent.child_, node) 200 | cur_parent = node 201 | table.insert(bstack, node) 202 | 203 | elseif cmd == "endfor" then 204 | if #bstack < 1 or bstack[#bstack].node_type ~= NODE_FOR then 205 | return -1, "endfor syntax error "..cur_lno 206 | end 207 | table.remove(bstack) 208 | cur_parent = bstack[#bstack] 209 | elseif cmd == "block" then 210 | if self.blocks_[arglist[1]] ~= nil then 211 | node = self.blocks_[arglist[1]] 212 | skip_block = 1 213 | else 214 | node = new_ast_node(NODE_BLOCK, cur_parent, arglist[1]) 215 | self.blocks_[arglist[1]] = node 216 | end 217 | table.insert(cur_parent.child_, node) 218 | cur_parent = node 219 | table.insert(bstack, node) 220 | elseif cmd == "endblock" then 221 | if #bstack < 1 or bstack[#bstack].node_type ~= NODE_BLOCK then 222 | return -1, "endblock error: "..cur_lno 223 | end 224 | table.remove(bstack) 225 | cur_parent = bstack[#bstack] 226 | skip_block = 0 227 | elseif cmd == "extend" then 228 | if self.involved_file ~= nil then 229 | return -1, "extend duplicated: "..cur_lno 230 | end 231 | if cur_parent.content ~= "__root" 232 | or #cur_parent.child_ > 2 233 | or #bstack > 1 then 234 | return -1, "extend error: "..cur_lno 235 | end 236 | 237 | table.insert(self.file_queue_, arglist[1]) 238 | end 239 | end 240 | 241 | --end while 242 | end 243 | 244 | cur_lno = cur_lno + 1 245 | last = 1 246 | end 247 | 248 | table.insert(cur_parent.child_, cur_text) 249 | if #bstack > 1 then return -1, print_node(bstack[#bstack], "unmatch block") end 250 | return 0 251 | end 252 | 253 | 254 | function lutem:load(filename, path) 255 | self.involve_file_[filename] = 1 256 | self.path_root_ = path 257 | table.insert(self.file_queue_, filename) 258 | self.queue_pos_ = 1 259 | while self.queue_pos_ <= #self.file_queue_ do 260 | local ret,reason = self:parse_file(self.file_queue_[self.queue_pos_]) 261 | if ret == -1 then 262 | return -1,reason 263 | end 264 | self.queue_pos_ = self.queue_pos_ + 1 265 | end 266 | self.state = 1 267 | return 0 268 | end 269 | 270 | -- get expression value. 271 | -- support plain variable or table field access 272 | -- Example: {{ varname }}, {{ tbl.sub.field }} 273 | function lutem:get_expr_val(expr) 274 | local flist = {} 275 | --table field split by . e.g: xxx.yyy.zzz 276 | for k in string.gmatch(expr, "[%w_]+") do 277 | table.insert(flist, k) 278 | end 279 | -- plain variable 280 | if #flist == 1 then 281 | if self.args_[expr] == nil then return "" end 282 | return tostring(self.args_[expr]) 283 | end 284 | -- table field access 285 | local val = nil 286 | local tbl = self.args_ 287 | for _, v in ipairs(flist) do 288 | if type(tbl) ~= "table" then return "" end 289 | val = tbl[v] 290 | if val == nil then return "" end 291 | tbl = val 292 | end 293 | if val == nil or type(val) == "table" then return "" end 294 | return tostring(val) 295 | end 296 | 297 | 298 | function lutem:render_node(node) 299 | if node.node_type == NODE_TEXT or node.node_type == NODE_RAW then 300 | table.insert(self.output_, node.content) 301 | elseif node.node_type == NODE_EXPR then 302 | table.insert(self.output_, self:get_expr_val(node.content)) 303 | elseif node.node_type == NODE_BLOCK then 304 | self:render_block(node) 305 | elseif node.node_type == NODE_FOR then 306 | self:render_loop(node) 307 | else 308 | table.insert(self.output_, node.content) 309 | end 310 | end 311 | 312 | function lutem:render_block(block) 313 | for _, node in ipairs(block.child_) do 314 | self:render_node(node) 315 | end 316 | end 317 | 318 | function lutem:render_loop(block) 319 | iter_tbl = {} 320 | kname = block.content[1] 321 | vname = block.content[1] 322 | tbl_name = block.content[2] 323 | for k, v in ipairs(self.args_[tbl_name]) do 324 | table.insert(iter_tbl, {key=k, val=v}) 325 | end 326 | 327 | for _, elem in ipairs(iter_tbl) do 328 | self.args_[kname] = elem.key 329 | self.args_[vname] = elem.val 330 | for _, node in ipairs(block.child_) do 331 | self:render_node(node) 332 | end 333 | end 334 | end 335 | 336 | function lutem:render(args) 337 | if self.state ~= 1 then return "", -1 end 338 | self.args_ = args 339 | self:render_block(self.node_root_) 340 | return table.concat(self.output_, '') 341 | end 342 | 343 | 344 | return lutem 345 | -------------------------------------------------------------------------------- /test.lua: -------------------------------------------------------------------------------- 1 | 2 | require "lutem" 3 | 4 | 5 | tmpl = lutem:new() 6 | ret,errmsg = tmpl:load("test_sub.tmpl", "./") 7 | args = {} 8 | args.bigul = {1,2,3} 9 | args.users = { 10 | {username="linlu", url="/#1"}, 11 | {username="zhi2", url="/#2"}, 12 | {username="daly", url="/#3"} 13 | } 14 | args.css= {color="\"color:red\""} 15 | if ret == 0 then 16 | result = tmpl:render(args) 17 | print(result) 18 | else 19 | print(errmsg) 20 | end 21 | 22 | tmp2 = lutem:new() 23 | ret,errmsg = tmp2:load("test.tmpl") 24 | if ret == 0 then print(tmp2:render(args)) end 25 | -------------------------------------------------------------------------------- /test.tmpl: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | 6 | {% for aval in bigul %} 7 |