Permalink
Cannot retrieve contributors at this time
Name already in use
A tag already exists with the provided branch name. Many Git commands accept both tag and branch names, so creating this branch may cause unexpected behavior. Are you sure you want to create this branch?
parcle/lib/Parclate.lua
Go to fileThis commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
420 lines (404 sloc)
12.3 KB
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| -- import Lua built in dependencies | |
| local string = require( 'string' ) | |
| local table = require( 'table' ) | |
| local pairs, setmetatable, tostring, type , unpack = | |
| pairs, setmetatable, tostring, type , unpack | |
| -- determine version | |
| -- We use that to determine if "setfenv" or "_ENV" must be used | |
| local v52 = (_VERSION=='Lua 5.2') and true or false | |
| --[[_ __ _ _ __ ___ ___ | |
| | '_ \ / _` | '__/ __|/ _ \ | |
| | |_) | (_| | | \__ \ __/ | |
| | .__/ \__,_|_| |___/\___| | |
| |_| --]] | |
| -- #private : helper to disect arguments of tags | |
| local parse_args = function ( s, tag, empty ) | |
| local args = {} | |
| local cmds = {} | |
| local tag_t = {tag=tag, empty=empty} | |
| string.gsub(s, '([^%s="]+)=(["\'])(.-)%2', function (w, _, a) | |
| local ns,cmd = string.match(w,'^(l):(%w+)') | |
| if ns and cmd then | |
| if 'strip' == cmd then | |
| tag_t['strip'] = true | |
| elseif 'attrs' == cmd then | |
| tag_t['attrs'] = a | |
| else | |
| cmds[cmd] = a | |
| end | |
| else | |
| args[w] = a | |
| end | |
| end) | |
| if next(cmds) then | |
| tag_t['cmd'] = cmds | |
| end | |
| if next(args) then | |
| tag_t['arg'] = args | |
| end | |
| return tag_t | |
| end | |
| -- #private : main loop for parsing the template string into the table conversion | |
| local parse = function ( s ) | |
| local stack = {} | |
| local top = {} | |
| table.insert(stack, top) | |
| local ni,c,label,xarg,empty | |
| local i, j = 1, 1 | |
| while true do | |
| ni,j,c,label,xarg,empty = string.find(s, '<(%/?)([%w:]+)(.-)(%/?)>', i) | |
| if not ni then break end | |
| if i ~= ni then -- if text is longer than 0 chars | |
| table.insert(top, string.sub(s, i, ni-1)) -- insert text chunk | |
| end | |
| if empty == '/' then -- empty element tag | |
| if '' ~= xarg then | |
| table.insert(top, parse_args(xarg, label, true)) | |
| else | |
| table.insert(top, {tag=label, empty=true}) | |
| end | |
| elseif c == '' then -- start tag | |
| if '' ~= xarg then | |
| top = parse_args(xarg, label, false) | |
| else | |
| top = {tag=label, empty=false} | |
| end | |
| table.insert(stack, top) -- new level | |
| else -- end tag | |
| local toclose = table.remove(stack) -- remove top | |
| top = stack[#stack] | |
| if #stack < 1 then | |
| error('nothing to close with <'..label..'>') | |
| end | |
| if toclose.tag ~= label then | |
| error('trying to close <'..toclose.tag..'> with <'..label..'>') | |
| end | |
| table.insert(top, toclose) | |
| end | |
| i = j+1 | |
| end | |
| if #stack > 1 then | |
| error('unclosed <'..stack[#stack].tag..'>') | |
| end | |
| return stack[1] | |
| end | |
| --[[ _ _ _ | |
| ___ ___ _ __(_) __ _| (_)_______ | |
| / __|/ _ \ '__| |/ _` | | |_ / _ \ | |
| \__ \ __/ | | | (_| | | |/ / __/ | |
| |___/\___|_| |_|\__,_|_|_/___\___| --]] | |
| -- #public : serializes the representation back into an (x)html template | |
| local serialize = function (self) | |
| local cl = {} | |
| local s_tag -- pre-define as local -> called recursively | |
| s_tag = function (t) | |
| if t.tag then | |
| table.insert(cl, string.format('<%s', t.tag)) | |
| if t.arg then | |
| for k,v in pairs(t.arg) do | |
| table.insert(cl, string.format(' %s="%s"', k, v)) | |
| end | |
| end | |
| if t.strip then table.insert(cl, ' l:strip=""') end | |
| if t.attrs then | |
| table.insert(cl, string.format(' l:attrs="%s"', t.attrs)) | |
| end | |
| if t.cmd then | |
| for c,e in pairs(t.cmd) do | |
| table.insert(cl, string.format(' l:%s="%s"', c, e)) | |
| end | |
| end | |
| table.insert(cl, t.empty and ' />' or '>') | |
| if t.empty then return end | |
| end | |
| for i=1,#t do | |
| local v=t[i] | |
| if 'table' == type(v) then | |
| s_tag(v) | |
| elseif 'string' == type(v) then | |
| table.insert(cl, v) | |
| else | |
| error('There is an error in the representation of the template') | |
| end | |
| end | |
| -- close tag | |
| if t.tag then | |
| table.insert(cl, string.format('</%s>', t.tag)) | |
| end | |
| return | |
| end -- recursive lexical s_tag | |
| s_tag(self) | |
| return table.concat(cl,'') | |
| end | |
| --[[ _ _ | |
| ___ ___ _ __ ___ _ __ (_) | ___ | |
| / __/ _ \| '_ ` _ \| '_ \| | |/ _ \ | |
| | (_| (_) | | | | | | |_) | | | __/ | |
| \___\___/|_| |_| |_| .__/|_|_|\___| | |
| |_| --]] | |
| -- #private: helper to create lua function source code from the arguments | |
| local compile_command = function(cmd, c_buf) | |
| for c,exp in pairs(cmd) do | |
| if 'if' == c then | |
| table.insert(c_buf, string.format('\tif %s then\n\t\t', exp) ) | |
| elseif 'for' == c then | |
| table.insert(c_buf, string.format('\tfor %s do\n\t\t', exp) ) | |
| end | |
| end | |
| end | |
| -- #private: helper to close open functions in the compiled chunk | |
| local compile_command_end = function(cmd, c_buf) | |
| for c,_ in pairs(cmd) do | |
| if 'if' == c or 'for' == c then | |
| table.insert(c_buf, '\tend\n') | |
| end | |
| end | |
| end | |
| -- #private: combine the last set of adjacent strings into one chunk | |
| local compile_buffer = function(c_buf, buffer, f_args, cnt) | |
| if 0==#buffer and 0==#f_args then return cnt end | |
| if 0 == #f_args then | |
| table.insert(c_buf, | |
| ' insert(x,[[' .. table.concat(buffer,'') .. ']])\n' | |
| ) | |
| else | |
| table.insert(c_buf, | |
| ' insert(x, format([[' .. | |
| table.concat(buffer,'') .. | |
| ']],'.. table.concat(f_args, ',') ..'))\n' | |
| ) | |
| -- f_args={} creates a new reference, table.remove preserves it | |
| while #f_args>0 do table.remove(f_args) end | |
| end | |
| while #buffer>0 do table.remove(buffer) end | |
| return cnt+1 | |
| end | |
| -- #private : renders just the chunk, that actully flushes the template | |
| -- @return : string that represents a function in lua syntax | |
| local compile_template = function (r) | |
| local c_buf = {} -- buffer that holds entire template | |
| local buffer = {} -- buffer that holds last set of adjacent string | |
| local f_args = {} -- holds the string.format arguments for "buffer" | |
| local chunk_cnt = 0 -- helps to determine initial length of buffer table | |
| local c_tag -- predeclare local for recursive calls | |
| c_tag = function(t) | |
| if t.cmd then | |
| chunk_cnt = compile_buffer(c_buf, buffer, f_args, chunk_cnt) | |
| compile_command(t.cmd, c_buf) | |
| end | |
| if t.tag and not t.strip then | |
| table.insert(buffer, string.format('<%s', t.tag)) | |
| if t.arg then | |
| for k,v in pairs(t.arg) do | |
| table.insert(buffer, string.format(' %s="%s"', k, v)) | |
| end | |
| end | |
| if t.attrs then | |
| chunk_cnt = compile_buffer(c_buf, buffer, f_args, chunk_cnt) | |
| table.insert(c_buf, string.format( | |
| '\tfor _at,_atv in pairs(%s) do\n' .. | |
| '\t\tinsert(x, format([=[ %%s="%%s"]=], _at, _atv))\n' .. | |
| '\tend\n', t.attrs)) | |
| chunk_cnt = chunk_cnt+1 | |
| end | |
| table.insert(buffer, t.empty and ' />' or '>') | |
| end | |
| if t.empty then | |
| if t.cmd then | |
| compile_command_end(t.cmd, c_buf) | |
| end | |
| return | |
| end | |
| for i=1,#t do | |
| local v=t[i] | |
| if 'table' == type(v) then | |
| c_tag(v) | |
| elseif 'string' == type(v) then | |
| local s = string.gsub(v, '%%','%%%%') | |
| s = string.gsub(s, '${([^}]+)}?', function (f) | |
| table.insert( f_args, f ) | |
| return '%s' | |
| end) | |
| table.insert(buffer, s) | |
| else | |
| error('There is an error in the representation of the template') | |
| end | |
| end | |
| -- close tag | |
| if t.tag and not t.strip then | |
| table.insert(buffer, string.format('</%s>', t.tag)) | |
| end | |
| if t.cmd then | |
| chunk_cnt = compile_buffer(c_buf, buffer, f_args, chunk_cnt) | |
| compile_command_end(t.cmd, c_buf) | |
| end | |
| return | |
| end | |
| -- start the actual execution | |
| c_tag(r) | |
| -- add last chunk to c_buf table | |
| chunk_cnt = compile_buffer(c_buf, buffer, f_args, chunk_cnt) | |
| table.insert(c_buf, " return concat(x,'')") | |
| table.insert(c_buf,1,'}\n') | |
| -- prefill the array with a safe amount of empty slots to avoid rehashing | |
| -- - excessive 'for' loops will make that less useful | |
| local empties = 1+math.pow(2, (math.ceil(math.log(chunk_cnt)/math.log(2))+1)) | |
| for i=1,empties do | |
| local addy = (i==1) and "''" or "''," | |
| table.insert(c_buf,1,addy) | |
| end | |
| table.insert(c_buf,1,' local x={') | |
| return table.concat(c_buf) | |
| end | |
| -- #public: create a table that represents just the compiled template ready to | |
| -- be filled with variables and rendered to a string | |
| local compile = function(self) | |
| -- holds all the protected immutable functions of the template | |
| local funcs = { | |
| format = string.format, pairs = pairs, ipairs = ipairs, | |
| concat = table.concat, insert = table.insert, tostring = tostring | |
| } | |
| -- the mutable part of the template that holds the values | |
| local t = {} | |
| -- prepare the compiled chunk (render function) and set into environment | |
| local chunk = (v52) and | |
| assert( loadin(t, compile_template(self) )) | |
| or | |
| setfenv(assert( loadstring( compile_template(self) )), t) | |
| return setmetatable(t, { | |
| __tostring = chunk, | |
| -- does flushing thje variables really makes sense? | |
| __call = function(self) | |
| for k,v in pairs(self) do | |
| if not funcs[k] then | |
| self[k] = nil | |
| end | |
| end | |
| end, | |
| -- access functions from the funcs table, user (template) variables from | |
| -- the self instance | |
| __index = function(self,k) | |
| if 'nil' ~= type(funcs[k]) then | |
| return rawget( funcs, k ) | |
| else | |
| return rawget( self, k ) | |
| end | |
| end, | |
| -- protect functions in the funcs table, don't allow identically named | |
| -- vars in the instance table | |
| __newindex = function(self, k, v) | |
| if 'nil' ~= type(funcs[k]) then | |
| error('<'..k..'> cannot be set on the template' ) | |
| else | |
| rawset( self, k, v ) | |
| end | |
| end | |
| }) | |
| end | |
| --[[ __ _ _ | |
| | |_ ___ / _(_) | ___ | |
| | __/ _ \ | |_| | |/ _ \ | |
| | || (_) | | _| | | __/ | |
| \__\___/ |_| |_|_|\___| --]] | |
| -- #public: generate the string for a file which is a compiled template | |
| local to_file = function(self) | |
| if v52 then | |
| return string.format( | |
| [[local f={format=string.format,pairs=pairs,ipairs=ipairs, | |
| concat=table.concat,insert=table.insert,tostring=tostring} | |
| return setmetatable({}, { | |
| __tostring=function( _ENV) %s end, | |
| __call=function(s) for k,v in pairs(s) do if not f[k] then s[k]=nil end end end, | |
| __index=function(s,k) if 'nil'==type(f[k]) then return rawget(s,k) | |
| else return rawget(f,k) end end, | |
| __newindex=function(s,k,v) if 'nil'==type(f[k]) then rawset(s,k,v) | |
| else error('<'..k..'> cannot be set on the table' ) end end})]], | |
| compile_template(self) ) | |
| else | |
| return string.format( | |
| [[local t,f={},{format=string.format,pairs=pairs,ipairs=ipairs, | |
| concat=table.concat,insert=table.insert,tostring=tostring} | |
| return setmetatable(t,{ | |
| __tostring=setfenv(function() %s end,t), | |
| __call=function(s) for k,v in pairs(s) do if not f[k] then s[k]=nil end end end, | |
| __index=function(s,k) if 'nil'==type(f[k]) then return rawget(s,k) | |
| else return rawget(f,k) end end, | |
| __newindex=function(s,k,v) if 'nil'==type(f[k]) then rawset(s,k,v) | |
| else error('<'..k..'> cannot be set on the table' ) end end})]], | |
| compile_template(self) ) | |
| end | |
| end | |
| --[[ _ _ | |
| __| | ___| |__ _ _ __ _ | |
| / _` |/ _ \ '_ \| | | |/ _` | | |
| | (_| | __/ |_) | |_| | (_| | | |
| \__,_|\___|_.__/ \__,_|\__, | | |
| |___/ --]] | |
| -- #private : helper ti iterate over representation in a sane order | |
| local cmp=function(a,b) | |
| if type(a)~=type(b) then | |
| return tostring(a)>tostring(b) | |
| else | |
| return a<b | |
| end | |
| end | |
| -- #public : a pretty printer, helps to see errors in the table representation | |
| -- can be called by print(instance) | |
| local print_r -- pre-define as local -> called recursively | |
| print_r = function( self, indent, done ) | |
| local cl = {} | |
| local done = done or {} | |
| local indent = indent or '' | |
| local s_index= {} | |
| for key, value in pairs(self) do | |
| table.insert(s_index,key) | |
| end | |
| table.sort(s_index,cmp) | |
| local nextIndent -- Storage for next indentation value | |
| -- deal with the keys,args and commands first | |
| for i,key in pairs(s_index) do | |
| local value = self[key] | |
| if 'table' == type (value) and not done [value] then | |
| nextIndent = nextIndent or | |
| (indent .. string.rep(' ',string.len(tostring (key))+2)) | |
| -- Shortcut conditional allocation | |
| done [value] = true | |
| table.insert(cl, indent .. '[' .. tostring (key) .. '] => {\n') | |
| table.insert(cl, print_r (value, nextIndent, done)) | |
| table.insert(cl, indent .. '}' .. '\n') | |
| else | |
| table.insert(cl, | |
| indent .. '[' .. tostring (key) .. "] => '" .. tostring (value).."'\n" | |
| ) | |
| end | |
| end | |
| return table.concat(cl, '') | |
| end | |
| --[[ _ | |
| ___| | __ _ ___ ___ | |
| / __| |/ _` / __/ __| | |
| | (__| | (_| \__ \__ \ | |
| \___|_|\__,_|___/___/ --]] | |
| -- setup constructor ...() | |
| return setmetatable({ | |
| serialize = serialize, | |
| to_file = to_file, | |
| compile = compile, | |
| debug = print_r | |
| }, { | |
| -- constructor | |
| __call = function( self, s ) | |
| if 'string' ~= type(s) then | |
| error('Parclates constructor expects a string argument.\n'.. | |
| 'expected <string> but got <' .. type(s) ..'>' | |
| ) | |
| elseif not s then | |
| error('Parclate constructor must be called with an argument!') | |
| end | |
| self.__index = self | |
| self.__tostring = to_file | |
| self.__call = compile | |
| return setmetatable(parse(s), self) | |
| end | |
| }) | |
| -- vim: ts=4 sw=4 sts=4 sta tw=80 list |