summaryrefslogtreecommitdiff
path: root/macros/luatex/generic/penlight/pl/template.lua
diff options
context:
space:
mode:
Diffstat (limited to 'macros/luatex/generic/penlight/pl/template.lua')
-rw-r--r--macros/luatex/generic/penlight/pl/template.lua202
1 files changed, 202 insertions, 0 deletions
diff --git a/macros/luatex/generic/penlight/pl/template.lua b/macros/luatex/generic/penlight/pl/template.lua
new file mode 100644
index 0000000000..9961a1bdfe
--- /dev/null
+++ b/macros/luatex/generic/penlight/pl/template.lua
@@ -0,0 +1,202 @@
+--- A template preprocessor.
+-- Originally by [Ricki Lake](http://lua-users.org/wiki/SlightlyLessSimpleLuaPreprocessor)
+--
+-- There are two rules:
+--
+-- * lines starting with # are Lua
+-- * otherwise, `$(expr)` is the result of evaluating `expr`
+--
+-- Example:
+--
+-- # for i = 1,3 do
+-- $(i) Hello, Word!
+-- # end
+-- ===>
+-- 1 Hello, Word!
+-- 2 Hello, Word!
+-- 3 Hello, Word!
+--
+-- Other escape characters can be used, when the defaults conflict
+-- with the output language.
+--
+-- > for _,n in pairs{'one','two','three'} do
+-- static int l_${n} (luaState *state);
+-- > end
+--
+-- See @{03-strings.md.Another_Style_of_Template|the Guide}.
+--
+-- Dependencies: `pl.utils`
+-- @module pl.template
+
+local utils = require 'pl.utils'
+
+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, newline)
+ local s = 1
+ for term, executed, e in chunk:gmatch(exec_pat) do
+ executed = '('..strsub(executed,2,-2)..')'
+ append(pieces, APPENDER..format("%q", strsub(chunk,s, term - 1)))
+ append(pieces, APPENDER..format("__tostring(%s or '')", executed))
+ s = e
+ end
+ 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,newline)
+ local exec_pat = "()"..inline_escape.."(%b"..brackets..")()"
+
+ local esc_pat = esc.."+([^\n]*\n?)"
+ local esc_pat1, esc_pat2 = "^"..esc_pat, "\n"..esc_pat
+ local pieces, s = {"return function()\nlocal __R_size, __R_table, __tostring = 0, {}, __tostring", n = 1}, 1
+ while true do
+ local _, e, lua = strfind(chunk,esc_pat1, s)
+ if not e then
+ local ss
+ ss, e, lua = strfind(chunk,esc_pat2, s)
+ parseDollarParen(pieces, strsub(chunk,s, ss), exec_pat, newline)
+ if not e then break end
+ end
+ if strsub(lua, -1, -1) == "\n" then lua = strsub(lua, 1, -2) end
+ append(pieces, "\n"..lua)
+ s = e + 1
+ end
+ append(pieces, "\nreturn __R_table\nend")
+
+ -- 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`).
+-- * `_brackets`: bracket pair that wraps inline Lua expressions, default is '()'.
+-- * `_escape`: character marking Lua lines, default is '#'
+-- * `_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 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 + 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, {
+ 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
+-- (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
+ setmetatable(env, {__index = parent})
+ end
+ setmetatable(self.env, {__index = env})
+
+ local res, out = xpcall(self.fn, debug.traceback)
+ if not res then
+ if self.code and db then print(self.code) end
+ return nil, out, self.code
+ end
+ return table.concat(out), nil, self.code
+end
+
+--- compiles the template.
+-- 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
+-- @tab[opt] opts the compilation options to use
+-- @return template object, or `nil + error + source_code`
+-- @usage
+-- 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, short = parseHashLines(str,inline_escape,inline_brackets,escape,opts.newline)
+ local env = { __tostring = tostring }
+ local fn, err = utils.load(code, chunk_name,'t',env)
+ 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 = opts.debug and code or nil,
+ }
+end
+
+return template