diff options
author | Steve J Donovan <steve.j.donovan@gmail.com> | 2017-07-16 16:34:09 +0300 |
---|---|---|
committer | GitHub <noreply@github.com> | 2017-07-16 16:34:09 +0300 |
commit | 81e004870997121d642e2b92bad2762f4b6e3beb (patch) | |
tree | ab2f55a6416ff7c0a45b52569fd9e5db91735206 | |
parent | 0ba9c24000d3cad1d1d81a3dd233584b0bbd2950 (diff) | |
parent | 14f1c9c4e58e651d23e46058e9bdf1d157659b6f (diff) |
Merge pull request #250 from Tieske/feat/improve-template
feat(template) newline option, better error reporting, cleanup
-rw-r--r-- | lua/pl/template.lua | 129 | ||||
-rw-r--r-- | tests/test-substitute.lua | 41 | ||||
-rw-r--r-- | tests/test-template.lua | 156 |
3 files changed, 233 insertions, 93 deletions
diff --git a/lua/pl/template.lua b/lua/pl/template.lua index 05a3274..3de5eda 100644 --- a/lua/pl/template.lua +++ b/lua/pl/template.lua @@ -30,11 +30,11 @@ local utils = require 'pl.utils' -local append,format,strsub,strfind = table.insert,string.format,string.sub,string.find +local append,format,strsub,strfind,strgsub = table.insert,string.format,string.sub,string.find,string.gsub local APPENDER = "\n__R_size = __R_size + 1; __R_table[__R_size] = " -local function parseDollarParen(pieces, chunk, exec_pat) +local function parseDollarParen(pieces, chunk, exec_pat, newline) local s = 1 for term, executed, e in chunk:gmatch(exec_pat) do executed = '('..strsub(executed,2,-2)..')' @@ -42,10 +42,18 @@ local function parseDollarParen(pieces, chunk, exec_pat) append(pieces, APPENDER..format("(%s or '')", executed)) s = e end - append(pieces, APPENDER..format("%q", strsub(chunk,s))) + local r + if newline then + r = format("%q", strgsub(strsub(chunk,s),"\n","")) + else + r = format("%q", strsub(chunk,s)) + end + if r ~= '""' then + append(pieces, APPENDER..r) + end end -local function parseHashLines(chunk,inline_escape,brackets,esc) +local function parseHashLines(chunk,inline_escape,brackets,esc,newline) local exec_pat = "()"..inline_escape.."(%b"..brackets..")()" local esc_pat = esc.."+([^\n]*\n?)" @@ -55,19 +63,31 @@ local function parseHashLines(chunk,inline_escape,brackets,esc) local ss, e, lua = strfind(chunk,esc_pat1, s) if not e then ss, e, lua = strfind(chunk,esc_pat2, s) - parseDollarParen(pieces, strsub(chunk,s, ss), exec_pat) + parseDollarParen(pieces, strsub(chunk,s, ss), exec_pat, newline) if not e then break end end append(pieces, "\n"..lua) s = e + 1 end append(pieces, "\nreturn __R_table\nend") - return table.concat(pieces) + + -- let's check for a special case where there is nothing to template, but it's + -- just a single static string + local short = false + if (#pieces == 3) and (pieces[2]:find(APPENDER, 1, true) == 1) then + pieces = { "return " .. pieces[2]:sub(#APPENDER+1,-1) } + short = true + end + -- if short == true, the generated function will not return a table of strings, + -- but a single string + return table.concat(pieces), short end local template = {} --- expand the template using the specified environment. +-- This function will compile and render the template. For more performant +-- recurring usage use the two step approach by using `compile` and `ct:render`. -- There are six special fields in the environment table `env` -- -- * `_parent`: continue looking up in this table (e.g. `_parent=_G`). @@ -76,34 +96,38 @@ local template = {} -- * `_inline_escape`: character marking inline Lua expression, default is '$'. -- * `_chunk_name`: chunk name for loaded templates, used if there -- is an error in Lua code. Default is 'TMP'. --- * `_debug`: if thruthy, the generated code will be printed upon a render error +-- * `_debug`: if truthy, the generated code will be printed upon a render error -- -- @string str the template string -- @tab[opt] env the environment --- @return `rendered template + nil + code`, or `nil + error + code`. The last return value --- `code` is only returned if the debug option is used. +-- @return `rendered template + nil + source_code`, or `nil + error + source_code`. The last +-- return value (`source_code`) is only returned if the debug option is used. function template.substitute(str,env) env = env or {} - local t, err = template.compile(str, - rawget(env,"_chunk_name"), - rawget(env,"_escape"), - rawget(env,"_inline_escape"), - rawget(env,"_brackets"), - rawget(env,"_debug")) + local t, err = template.compile(str, { + chunk_name = rawget(env,"_chunk_name"), + escape = rawget(env,"_escape"), + inline_escape = rawget(env,"_inline_escape"), + inline_brackets = rawget(env,"_brackets"), + newline = nil, + debug = rawget(env,"_debug") + }) if not t then return t, err end return t:render(env, rawget(env,"_parent"), rawget(env,"_debug")) end --- executes the previously compiled template and renders it. +-- @function ct:render -- @tab[opt] env the environment. --- @tab[opt] parent continue looking up in this table (e.g. `_parent=_G`). --- @bool[opt] db if thruthy, it will print the code upon a render error. Note: --- the template must have been compiled with the debug option as well! (only --- here for backward compatibility, as the function will return the generated --- code anyway if available) --- @return `rendered template + nil + code`, or `nil + error + code`. The last return value --- `code` is only returned if the template was compiled with the debug option. +-- @tab[opt] parent continue looking up in this table (e.g. `parent=_G`). +-- @bool[opt] db if thruthy, it will print the code upon a render error +-- (provided the template was compiled with the debug option). +-- @return `rendered template + nil + source_code`, or `nil + error + source_code`. The last return value +-- (`source_code`) is only returned if the template was compiled with the debug option. +-- @usage +-- local ct, err = template.compile(my_template) +-- local rendered , err = ct:render(my_env, parent) local render = function(self, env, parent, db) env = env or {} if parent then -- parent is a bit silly, but for backward compatibility retained @@ -114,39 +138,62 @@ local render = function(self, env, parent, db) local res, out = xpcall(self.fn, debug.traceback) if not res then if self.code and db then print(self.code) end - return nil, err, self.code + return nil, out, self.code end return table.concat(out), nil, self.code end --- compiles the template. --- Returns an object that can repeatedly be run without parsing the template --- again. +-- Returns an object that can repeatedly be rendered without parsing/compiling +-- the template again. +-- The options passed in the `opts` table support the following options: +-- +-- * `chunk_name`: chunk name for loaded templates, used if there +-- is an error in Lua code. Default is 'TMP'. +-- * `escape`: character marking Lua lines, default is '#' +-- * `inline_escape`: character marking inline Lua expression, default is '$'. +-- * `inline_brackets`: bracket pair that wraps inline Lua expressions, default is '()'. +-- * `newline`: string to replace newline characters, default is `nil` (not replacing newlines). +-- * `debug`: if truthy, the generated source code will be retained within the compiled template object, default is `nil`. +-- -- @string str the template string --- @string[opt] chunk_name chunk name for loaded templates, used if there is an error in Lua code. Default is 'TMP'. --- @string[opt] escape character marking Lua lines, default is '#'. --- @string[opt] inline_escape character marking inline Lua expression, default is '$'. --- @string[opt] inline_brackets bracket pair that wraps inline Lua expressions, default is '()'. --- @bool[opt] db if thruthy, the generated code will be printed upon rendering errors --- @return template object, or nil + error +-- @tab[opt] opts the compilation options to use +-- @return template object, or `nil + error + source_code` -- @usage --- local t, err = template.compile(my_template) --- local rendered , err = t:render(my_env, parent) -function template.compile(str, chunk_name, escape, inline_escape, inline_brackets, db) - chunk_name = chunk_name or 'TMP' - escape = escape or '#' - inline_escape = inline_escape or '$' - inline_brackets = inline_brackets or '()' +-- local ct, err = template.compile(my_template) +-- local rendered , err = ct:render(my_env, parent) +function template.compile(str, opts) + opts = opts or {} + local chunk_name = opts.chunk_name or 'TMP' + local escape = opts.escape or '#' + local inline_escape = opts.inline_escape or '$' + local inline_brackets = opts.inline_brackets or '()' - local code = parseHashLines(str,inline_escape,inline_brackets,escape) + local code, short = parseHashLines(str,inline_escape,inline_brackets,escape,opts.newline) local env = {} local fn, err = utils.load(code, chunk_name,'t',env) - if not fn then return nil, err end + if not fn then return nil, err, code end + + if short then + -- the template returns a single constant string, let's optimize for that + local constant_string = fn() + return { + fn = fn(), + env = env, + render = function(self) -- additional params can be ignored + -- skip the metatable magic and error handling in the render + -- function above for this special case + return constant_string, nil, self.code + end, + code = opts.debug and code or nil, + } + end + return { fn = fn(), env = env, render = render, - code = db and code or nil, + code = opts.debug and code or nil, } end diff --git a/tests/test-substitute.lua b/tests/test-substitute.lua deleted file mode 100644 index 6ed36f6..0000000 --- a/tests/test-substitute.lua +++ /dev/null @@ -1,41 +0,0 @@ -local subst = require 'pl.template'.substitute -local List = require 'pl.List' -local asserteq = require 'pl.test'.asserteq - -asserteq(subst([[ -# for i = 1,2 do -<p>Hello $(tostring(i))</p> -# end -]],_G),[[ -<p>Hello 1</p> -<p>Hello 2</p> -]]) - -asserteq(subst([[ -<ul> -# for name in ls:iter() do - <li>$(name)</li> -#end -</ul> -]],{ls = List{'john','alice','jane'}}),[[ -<ul> - <li>john</li> - <li>alice</li> - <li>jane</li> -</ul> -]]) - --- can change the default escape from '#' so we can do C/C++ output. --- note that the environment can have a parent field. -asserteq(subst([[ -> for i,v in ipairs{'alpha','beta','gamma'} do - cout << obj.${v} << endl; -> end -]],{_parent=_G, _brackets='{}', _escape='>'}),[[ - cout << obj.alpha << endl; - cout << obj.beta << endl; - cout << obj.gamma << endl; -]]) - --- handle templates with a lot of substitutions -asserteq(subst(("$(x)\n"):rep(300), {x = "y"}), ("y\n"):rep(300)) diff --git a/tests/test-template.lua b/tests/test-template.lua index e6cf7d3..3cda83d 100644 --- a/tests/test-template.lua +++ b/tests/test-template.lua @@ -1,4 +1,55 @@ local template = require 'pl.template' +local subst = template.substitute +local List = require 'pl.List' +local asserteq = require 'pl.test'.asserteq +local utils = require 'pl.utils' + + + +asserteq(subst([[ +# for i = 1,2 do +<p>Hello $(tostring(i))</p> +# end +]],_G),[[ +<p>Hello 1</p> +<p>Hello 2</p> +]]) + + + +asserteq(subst([[ +<ul> +# for name in ls:iter() do + <li>$(name)</li> +#end +</ul> +]],{ls = List{'john','alice','jane'}}),[[ +<ul> + <li>john</li> + <li>alice</li> + <li>jane</li> +</ul> +]]) + + + +-- can change the default escape from '#' so we can do C/C++ output. +-- note that the environment can have a parent field. +asserteq(subst([[ +> for i,v in ipairs{'alpha','beta','gamma'} do + cout << obj.${v} << endl; +> end +]],{_parent=_G, _brackets='{}', _escape='>'}),[[ + cout << obj.alpha << endl; + cout << obj.beta << endl; + cout << obj.gamma << endl; +]]) + + + +-- handle templates with a lot of substitutions +asserteq(subst(("$(x)\n"):rep(300), {x = "y"}), ("y\n"):rep(300)) + -------------------------------------------------- @@ -16,8 +67,8 @@ local my_env = { } local res, err = template.substitute(tmpl, my_env) -print(res, err) -assert(res == [[<ul> +--print(res, err) +asserteq(res, [[<ul> <li>1 = ONE</li> <li>2 = TWO</li> <li>3 = THREE</li> @@ -42,8 +93,8 @@ local my_env = { } local res, err = template.substitute(tmpl, my_env) -print(res, err) -assert(res == [[ +--print(res, err) +asserteq(res, [[ <ul> <li>1 = ONE</li> <li>2 = TWO</li> @@ -52,6 +103,7 @@ assert(res == [[ ]]) + -------------------------------------------------- -- Test reusing a compiled template local tmpl = [[ @@ -66,10 +118,10 @@ local my_env = { ipairs = ipairs, T = {'one','two','three'} } -local t, err = template.compile(tmpl, nil, nil, nil, nil, true) +local t, err = template.compile(tmpl, { debug = true }) local res, err, code = t:render(my_env) -print(res, err, code) -assert(res == [[ +--print(res, err, code) +asserteq(res, [[ <ul> <li>1 = ONE</li> <li>2 = TWO</li> @@ -78,18 +130,100 @@ assert(res == [[ ]]) --- reuse with different env +-- now reuse with different env local my_env = { ipairs = ipairs, T = {'four','five','six'} } -local t, err = template.compile(tmpl, nil, nil, nil, nil, true) +local t, err = template.compile(tmpl, { debug = true }) local res, err, code = t:render(my_env) -print(res, err, code) -assert(res == [[ +--print(res, err, code) +asserteq(res, [[ <ul> <li>1 = FOUR</li> <li>2 = FIVE</li> <li>3 = SIX</li> </ul> ]]) + + + +-------------------------------------------------- +-- Test the newline parameter +local tmpl = [[ +some list: $(T[1]:upper()) +# for i = 2, #T do +,$(T[i]:upper()) +# end +]] + +local my_env = { + ipairs = ipairs, + T = {'one','two','three'} +} +local t, err = template.compile(tmpl, { debug = true, newline = "" }) +local res, err, code = t:render(my_env) +--print(res, err, code) +asserteq(res, [[some list: ONE,TWO,THREE]]) + + + +-------------------------------------------------- +-- Test template run-time error +local tmpl = [[ +header: $("hello" * 10) +]] + +local t, err = template.compile(tmpl, { debug = true, newline = "" }) +local res, err, code = t:render() +--print(res, err, code) +assert(res == nil, "expected nil here because of the runtime error") +asserteq(type(err), "string") +asserteq(type(utils.load(code)), "function") + + + +-------------------------------------------------- +-- Test template compile-time error +local tmpl = [[ +header: $(this doesn't work) +]] + +local my_env = { + ipairs = ipairs, + T = {'one','two','three'} +} +local t, err, code = template.compile(tmpl, { debug = true, newline = "" }) +--print(t, err, code) +assert(t==nil, "expected t to be nil here because of the syntax error") +asserteq(type(err), "string") +asserteq(type(code), "string") + + + +-------------------------------------------------- +-- Test using template being a single static string +local tmpl = [[ +<ul> +<p>a paragraph</p> +<p>a paragraph</p> +</ul> +]] + +local t, err = template.compile(tmpl, { debug = true }) +local res, err, code = t:render(my_env) +--print(res, err, code) + +asserteq(res, [[<ul> +<p>a paragraph</p> +<p>a paragraph</p> +</ul> +]]) +asserteq(code, [[return "<ul>\ +<p>a paragraph</p>\ +<p>a paragraph</p>\ +</ul>\ +"]]) + + +print("template: success") |