Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/stevedonovan/Penlight.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSteve J Donovan <steve.j.donovan@gmail.com>2017-07-16 16:34:09 +0300
committerGitHub <noreply@github.com>2017-07-16 16:34:09 +0300
commit81e004870997121d642e2b92bad2762f4b6e3beb (patch)
treeab2f55a6416ff7c0a45b52569fd9e5db91735206
parent0ba9c24000d3cad1d1d81a3dd233584b0bbd2950 (diff)
parent14f1c9c4e58e651d23e46058e9bdf1d157659b6f (diff)
Merge pull request #250 from Tieske/feat/improve-template
feat(template) newline option, better error reporting, cleanup
-rw-r--r--lua/pl/template.lua129
-rw-r--r--tests/test-substitute.lua41
-rw-r--r--tests/test-template.lua156
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")