local W=require("\112\108\046\117\116\105\108\115")local h,K,Q,N,y=table.insert,string.format,string.sub,string.find,string.gsub local X="\n\095\095\082\095\115\105\122\101 \061 \095\095\082\095\115\105\122\101 \043 \049\059 \095\095\082\095\116\097\098\108\101\091\095\095\082\095\115\105\122\101\093 \061 "local function A(W,N,A,k)local Y=1 for y,A,k in N:gmatch(A)do A="\040"..(Q(A,2,-2).."\041")h(W,X..K("\037\113",Q(N,Y,y-1)))h(W,X..K("\095\095\116\111\115\116\114\105\110\103\040\037\115 \111\114 \'\'\041",A))Y=k end local j if k then j=K("\037\113",y(Q(N,Y),"\n",""))else j=K("\037\113",Q(N,Y))end if j~="\"\""then h(W,X..j)end end local function k(W,K,y,k,Y)local j="\040\041"..(K..("\040\037\098"..(y.."\041\040\041")))local F=k.."\043\040\091\094\n\093\042\n\063\041"local G,n="\094"..F,"\n"..F local I,r={"\114\101\116\117\114\110 \102\117\110\099\116\105\111\110\040\041\n\108\111\099\097\108 \095\095\082\095\115\105\122\101\044 \095\095\082\095\116\097\098\108\101\044 \095\095\116\111\115\116\114\105\110\103 \061 \048\044 \123\125\044 \095\095\116\111\115\116\114\105\110\103";n=1},1 while true do local K,y,X=N(W,G,r)if not y then local h h,y,X=N(W,n,r)A(I,Q(W,r,h),j,Y)if not y then break end end if Q(X,-1,-1)=="\n"then X=Q(X,1,-2)end h(I,"\n"..X)r=y+1 end h(I,"\n\114\101\116\117\114\110 \095\095\082\095\116\097\098\108\101\n\101\110\100")local s=false if#I==3 and I[2]:find(X,1,true)==1 then I={"\114\101\116\117\114\110 "..I[2]:sub(#X+1,-1)}s=true end return table.concat(I),s end local Y={}function Y.substitute(W,h)h=h or{}local K,Q=Y.compile(W,{chunk_name=rawget(h,"\095\099\104\117\110\107\095\110\097\109\101");escape=rawget(h,"\095\101\115\099\097\112\101");inline_escape=rawget(h,"\095\105\110\108\105\110\101\095\101\115\099\097\112\101"),inline_brackets=rawget(h,"\095\098\114\097\099\107\101\116\115");newline=nil,debug=rawget(h,"\095\100\101\098\117\103")})if not K then return K,Q end return K:render(h,rawget(h,"\095\112\097\114\101\110\116"),rawget(h,"\095\100\101\098\117\103"))end local j=function(W,h,K,Q)h=h or{}if K then setmetatable(h,{__index=K})end setmetatable(W.env,{__index=h})local N,y=xpcall(W.fn,debug.traceback)if not N then if W.code and Q then print(W.code)end return nil,y,W.code end return table.concat(y),nil,W.code end function Y.compile(h,K)K=K or{}local Q=K.chunk_name or"\084\077\080"local N=K.escape or"\035"local y=K.inline_escape or"\036"local X=K.inline_brackets or"\040\041"local A,Y=k(h,y,X,N,K.newline)local F={__tostring=tostring}local G,n=W.load(A,Q,"\116",F)if not G then return nil,n,A end if Y then local W=G()return{fn=G(),env=F;render=function(h)return W,nil,h.code end;code=K.debug and A or nil}end return{fn=G(),env=F;render=j,code=K.debug and A or nil}end return Y
--- 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
