if not modules then modules = { } end modules ['cldf-ini'] = { version = 1.001, comment = "companion to cldf-ini.mkiv", author = "Hans Hagen, PRAGMA-ADE, Hasselt NL", copyright = "PRAGMA ADE / ConTeXt Development Team", license = "see context related readme files" } -- This started as an experiment: generating context code at the lua end. After all -- it is surprisingly simple to implement due to metatables. I was wondering if -- there was a more natural way to deal with commands at the lua end. Of course it's -- a bit slower but often more readable when mixed with lua code. It can also be handy -- when generating documents from databases or when constructing large tables or so. -- -- maybe optional checking against interface -- currently no coroutine trickery -- we could always use prtcatcodes (context.a_b_c) but then we loose protection -- tflush needs checking ... sort of weird that it's not a table -- __flushlines is an experiment and rather ugly so it will go away -- -- tex.print == line with endlinechar appended -- todo: context("%bold{total: }%s",total) -- todo: context.documentvariable("title") local tex = tex context = context or { } local context = context local format, gsub, validstring, stripstring = string.format, string.gsub, string.valid, string.strip local next, type, tostring, tonumber, setmetatable, unpack, select = next, type, tostring, tonumber, setmetatable, unpack, select local insert, remove, concat = table.insert, table.remove, table.concat local lpegmatch, lpegC, lpegS, lpegP, lpegV, lpegCc, lpegCs, patterns = lpeg.match, lpeg.C, lpeg.S, lpeg.P, lpeg.V, lpeg.Cc, lpeg.Cs, lpeg.patterns local formatters = string.formatters -- using formatteds is slower in this case local loaddata = io.loaddata local texsprint = tex.sprint local textprint = tex.tprint local texprint = tex.print local texwrite = tex.write local texgetcount = tex.getcount local isnode = node.is_node -- after 0.65 just node.type local writenode = node.write local copynodelist = node.copy_list local catcodenumbers = catcodes.numbers local ctxcatcodes = catcodenumbers.ctxcatcodes local prtcatcodes = catcodenumbers.prtcatcodes local texcatcodes = catcodenumbers.texcatcodes local txtcatcodes = catcodenumbers.txtcatcodes local vrbcatcodes = catcodenumbers.vrbcatcodes local xmlcatcodes = catcodenumbers.xmlcatcodes local flush = texsprint local flushdirect = texprint local flushraw = texwrite local report_context = logs.reporter("cld","tex") local report_cld = logs.reporter("cld","stack") local processlines = true -- experiments.register("context.processlines", function(v) processlines = v end) -- for tracing it's easier to have two stacks local _stack_f_, _n_f_ = { }, 0 local _stack_n_, _n_n_ = { }, 0 local function _store_f_(ti) _n_f_ = _n_f_ + 1 _stack_f_[_n_f_] = ti return _n_f_ end local function _store_n_(ti) _n_n_ = _n_n_ + 1 _stack_n_[_n_n_] = ti return _n_n_ end local function _flush_f_(n) local sn = _stack_f_[n] if not sn then report_cld("data with id %a cannot be found on stack",n) else local tn = type(sn) if tn == "function" then if not sn() and texgetcount("@@trialtypesetting") == 0 then -- @@trialtypesetting is private! _stack_f_[n] = nil else -- keep, beware, that way the stack can grow end else if texgetcount("@@trialtypesetting") == 0 then -- @@trialtypesetting is private! writenode(sn) _stack_f_[n] = nil else writenode(copynodelist(sn)) -- keep, beware, that way the stack can grow end end end end local function _flush_n_(n) local sn = _stack_n_[n] if not sn then report_cld("data with id %a cannot be found on stack",n) elseif texgetcount("@@trialtypesetting") == 0 then -- @@trialtypesetting is private! writenode(sn) _stack_n_[n] = nil else writenode(copynodelist(sn)) -- keep, beware, that way the stack can grow end end function context.restart() _stack_f_, _n_f_ = { }, 0 _stack_n_, _n_n_ = { }, 0 end context._stack_f_ = _stack_f_ context._store_f_ = _store_f_ context._flush_f_ = _flush_f_ _cldf_ = _flush_f_ context._stack_n_ = _stack_n_ context._store_n_ = _store_n_ context._flush_n_ = _flush_n_ _cldn_ = _flush_n_ -- Should we keep the catcodes with the function? local catcodestack = { } local currentcatcodes = ctxcatcodes local contentcatcodes = ctxcatcodes local catcodes = { ctx = ctxcatcodes, ctxcatcodes = ctxcatcodes, context = ctxcatcodes, prt = prtcatcodes, prtcatcodes = prtcatcodes, protect = prtcatcodes, tex = texcatcodes, texcatcodes = texcatcodes, plain = texcatcodes, txt = txtcatcodes, txtcatcodes = txtcatcodes, text = txtcatcodes, vrb = vrbcatcodes, vrbcatcodes = vrbcatcodes, verbatim = vrbcatcodes, xml = xmlcatcodes, xmlcatcodes = xmlcatcodes, } local function pushcatcodes(c) insert(catcodestack,currentcatcodes) currentcatcodes = (c and catcodes[c] or tonumber(c)) or currentcatcodes contentcatcodes = currentcatcodes end local function popcatcodes() currentcatcodes = remove(catcodestack) or currentcatcodes contentcatcodes = currentcatcodes end context.pushcatcodes = pushcatcodes context.popcatcodes = popcatcodes -- -- -- local newline = patterns.newline local space = patterns.spacer local spacing = newline * space^0 local content = lpegC((1-spacing)^1) -- texsprint local emptyline = space^0 * newline^2 -- texprint("") local endofline = space^0 * newline * space^0 -- texsprint(" ") local simpleline = endofline * lpegP(-1) -- local verbose = lpegC((1-space-newline)^1) local beginstripper = (lpegS(" \t")^1 * newline^1) / "" local endstripper = beginstripper * lpegP(-1) local justaspace = space * lpegCc("") local justanewline = newline * lpegCc("") local function n_content(s) flush(contentcatcodes,s) end local function n_verbose(s) flush(vrbcatcodes,s) end local function n_endofline() flush(currentcatcodes," \r") end local function n_emptyline() flushdirect(currentcatcodes,"\r") end local function n_simpleline() flush(currentcatcodes," \r") end local n_exception = "" -- better a table specification function context.newtexthandler(specification) -- can also be used for verbose specification = specification or { } -- local s_catcodes = specification.catcodes -- local f_before = specification.before local f_after = specification.after -- local f_endofline = specification.endofline or n_endofline local f_emptyline = specification.emptyline or n_emptyline local f_simpleline = specification.simpleline or n_simpleline local f_content = specification.content or n_content local f_space = specification.space -- local p_exception = specification.exception -- if s_catcodes then f_content = function(s) flush(s_catcodes,s) end end -- local pattern if f_space then if p_exception then local content = lpegC((1-spacing-p_exception)^1) pattern = ( justaspace / f_space + justanewline / f_endofline + p_exception + content / f_content )^0 else local content = lpegC((1-space-endofline)^1) pattern = ( justaspace / f_space + justanewline / f_endofline + content / f_content )^0 end else if p_exception then local content = lpegC((1-spacing-p_exception)^1) pattern = simpleline / f_simpleline + ( emptyline / f_emptyline + endofline / f_endofline + p_exception + content / f_content )^0 else local content = lpegC((1-spacing)^1) pattern = simpleline / f_simpleline + ( emptyline / f_emptyline + endofline / f_endofline + content / f_content )^0 end end -- if f_before then pattern = (P(true) / f_before) * pattern end -- if f_after then pattern = pattern * (P(true) / f_after) end -- return function(str) return lpegmatch(pattern,str) end, pattern end function context.newverbosehandler(specification) -- a special variant for e.g. cdata in lxml-tex specification = specification or { } -- local f_line = specification.line or function() flushdirect("\r") end local f_space = specification.space or function() flush(" ") end local f_content = specification.content or n_verbose local f_before = specification.before local f_after = specification.after -- local pattern = justanewline / f_line -- so we get call{} + verbose / f_content + justaspace / f_space -- so we get call{} -- if specification.strip then pattern = beginstripper^0 * (endstripper + pattern)^0 else pattern = pattern^0 end -- if f_before then pattern = (lpegP(true) / f_before) * pattern end -- if f_after then pattern = pattern * (lpegP(true) / f_after) end -- return function(str) return lpegmatch(pattern,str) end, pattern end local flushlines = context.newtexthandler { content = n_content, endofline = n_endofline, emptyline = n_emptyline, simpleline = n_simpleline, } context.__flushlines = flushlines -- maybe context.helpers.flushtexlines context.__flush = flush context.__flushdirect = flushdirect -- The next variant is only used in rare cases (buffer to mp): local printlines_ctx = ( (newline) / function() texprint("") end + (1-newline)^1 / function(s) texprint(ctxcatcodes,s) end * newline^-1 )^0 local printlines_raw = ( (newline) / function() texprint("") end + (1-newline)^1 / function(s) texprint(s) end * newline^-1 )^0 function context.printlines(str,raw) -- todo: see if via file is useable if raw then lpegmatch(printlines_raw,str) else lpegmatch(printlines_ctx,str) end end -- This is the most reliable way to deal with nested buffers and other -- catcode sensitive data. local methodhandler = resolvers.methodhandler function context.viafile(data,tag) if data and data ~= "" then local filename = resolvers.savers.byscheme("virtual",validstring(tag,"viafile"),data) -- context.startregime { "utf" } context.input(filename) -- context.stopregime() end end -- -- -- "{" .. ti .. "}" is somewhat slower in a cld-mkiv run than "{",ti,"}" local containseol = patterns.containseol local function writer(parent,command,first,...) -- already optimized before call local t = { first, ... } flush(currentcatcodes,command) -- todo: ctx|prt|texcatcodes local direct = false for i=1,#t do local ti = t[i] local typ = type(ti) if direct then if typ == "string" or typ == "number" then flush(currentcatcodes,ti) else -- node.write report_context("error: invalid use of direct in %a, only strings and numbers can be flushed directly, not %a",command,typ) end direct = false elseif ti == nil then -- nothing elseif ti == "" then flush(currentcatcodes,"{}") elseif typ == "string" then -- is processelines seen ? if processlines and lpegmatch(containseol,ti) then flush(currentcatcodes,"{") local flushlines = parent.__flushlines or flushlines flushlines(ti) flush(currentcatcodes,"}") elseif currentcatcodes == contentcatcodes then flush(currentcatcodes,"{",ti,"}") else flush(currentcatcodes,"{") flush(contentcatcodes,ti) flush(currentcatcodes,"}") end elseif typ == "number" then -- numbers never have funny catcodes flush(currentcatcodes,"{",ti,"}") elseif typ == "table" then local tn = #ti if tn == 0 then local done = false for k, v in next, ti do if done then if v == "" then flush(currentcatcodes,",",k,'=') else flush(currentcatcodes,",",k,"={",v,"}") end else if v == "" then flush(currentcatcodes,"[",k,"=") else flush(currentcatcodes,"[",k,"={",v,"}") end done = true end end if done then flush(currentcatcodes,"]") else flush(currentcatcodes,"[]") end elseif tn == 1 then -- some 20% faster than the next loop local tj = ti[1] if type(tj) == "function" then flush(currentcatcodes,"[\\cldf{",_store_f_(tj),"}]") else flush(currentcatcodes,"[",tj,"]") end else -- is concat really faster than flushes here? probably needed anyway (print artifacts) for j=1,tn do local tj = ti[j] if type(tj) == "function" then ti[j] = "\\cldf{" .. _store_f_(tj) .. "}" end end flush(currentcatcodes,"[",concat(ti,","),"]") end elseif typ == "function" then flush(currentcatcodes,"{\\cldf{",_store_f_(ti),"}}") -- todo: ctx|prt|texcatcodes elseif typ == "boolean" then if ti then flushdirect(currentcatcodes,"\r") else direct = true end elseif typ == "thread" then report_context("coroutines not supported as we cannot yield across boundaries") elseif isnode(ti) then -- slow flush(currentcatcodes,"{\\cldn{",_store_n_(ti),"}}") else report_context("error: %a gets a weird argument %a",command,ti) end end end local generics = { } context.generics = generics local function indexer(parent,k) if type(k) == "string" then local c = "\\" .. tostring(generics[k] or k) local f = function(first,...) if first == nil then flush(currentcatcodes,c) else return writer(parent,c,first,...) end end parent[k] = f return f else return context -- catch end end -- Potential optimization: after the first call we know if there will be an -- argument. Of course there is the side effect that for instance abuse like -- context.NC(str) fails as well as optional arguments. So, we don't do this -- in practice. We just keep the next trick commented. The gain on some -- 100000 calls is not that large: 0.100 => 0.95 which is neglectable. -- -- local function constructor(parent,k,c,first,...) -- if first == nil then -- local f = function() -- flush(currentcatcodes,c) -- end -- parent[k] = f -- return f() -- else -- local f = function(...) -- return writer(parent,c,...) -- end -- parent[k] = f -- return f(first,...) -- end -- end -- -- local function indexer(parent,k) -- local c = "\\" .. tostring(generics[k] or k) -- local f = function(...) -- return constructor(parent,k,c,...) -- end -- parent[k] = f -- return f -- end -- only for internal usage: function context.constructcsonly(k) -- not much faster than the next but more mem efficient local c = "\\" .. tostring(generics[k] or k) rawset(context, k, function() flush(prtcatcodes,c) end) end function context.constructcs(k) local c = "\\" .. tostring(generics[k] or k) rawset(context, k, function(first,...) if first == nil then flush(prtcatcodes,c) else return writer(context,c,first,...) end end) end local function caller(parent,f,a,...) if not parent then -- so we don't need to test in the calling (slower but often no issue) elseif f ~= nil then local typ = type(f) if typ == "string" then if a then flush(contentcatcodes,formatters[f](a,...)) -- was currentcatcodes elseif processlines and lpegmatch(containseol,f) then local flushlines = parent.__flushlines or flushlines flushlines(f) else flush(contentcatcodes,f) end elseif typ == "number" then if a then flush(currentcatcodes,f,a,...) else flush(currentcatcodes,f) end elseif typ == "function" then -- ignored: a ... flush(currentcatcodes,"{\\cldf{",_store_f_(f),"}}") -- todo: ctx|prt|texcatcodes elseif typ == "boolean" then if f then if a ~= nil then local flushlines = parent.__flushlines or flushlines flushlines(a) else flushdirect(currentcatcodes,"\n") -- no \r, else issues with \startlines ... use context.par() otherwise end else if a ~= nil then -- no command, same as context(a,...) writer(parent,"",a,...) else -- ignored end end elseif typ == "thread" then report_context("coroutines not supported as we cannot yield across boundaries") elseif isnode(f) then -- slow -- writenode(f) flush(currentcatcodes,"\\cldn{",_store_n_(f),"}") else report_context("error: %a gets a weird argument %a","context",f) end end end local defaultcaller = caller setmetatable(context, { __index = indexer, __call = caller } ) -- now we tweak unprotect and protect function context.unprotect() -- at the lua end insert(catcodestack,currentcatcodes) currentcatcodes = prtcatcodes contentcatcodes = currentcatcodes -- at the tex end flush("\\unprotect") end function context.protect() -- at the tex end flush("\\protect") -- at the lua end currentcatcodes = remove(catcodestack) or currentcatcodes contentcatcodes = currentcatcodes end function context.sprint(...) -- takes catcodes as first argument flush(...) end function context.fprint(catcodes,fmt,first,...) if type(catcodes) == "number" then if first then flush(catcodes,formatters[fmt](first,...)) else flush(catcodes,fmt) end else if fmt then flush(formatters[catcodes](fmt,first,...)) else flush(catcodes) end end end function tex.fprint(fmt,first,...) -- goodie if first then flush(currentcatcodes,formatters[fmt](first,...)) else flush(currentcatcodes,fmt) end end -- logging local trace_stack = { } local normalflush = flush local normalflushdirect = flushdirect local normalflushraw = flushraw local normalwriter = writer local currenttrace = nil local nofwriters = 0 local nofflushes = 0 local visualizer = lpeg.replacer { { "\n","<>" }, { "\r","<>" }, } statistics.register("traced context", function() if nofwriters > 0 or nofflushes > 0 then return format("writers: %s, flushes: %s, maxstack: %s",nofwriters,nofflushes,_n_f_) end end) local tracedwriter = function(parent,...) -- also catcodes ? nofwriters = nofwriters + 1 local savedflush = flush local savedflushdirect = flushdirect -- unlikely to be used here local t, n = { "w : - : " }, 1 local traced = function(normal,catcodes,...) -- todo: check for catcodes local s = concat({...}) s = lpegmatch(visualizer,s) n = n + 1 t[n] = s normal(catcodes,...) end flush = function(...) traced(normalflush, ...) end flushdirect = function(...) traced(normalflushdirect,...) end normalwriter(parent,...) flush = savedflush flushdirect = savedflushdirect currenttrace(concat(t)) end -- we could reuse collapsed local traced = function(normal,one,two,...) nofflushes = nofflushes + 1 if two then -- only catcodes if 'one' is number normal(one,two,...) local catcodes = type(one) == "number" and one local arguments = catcodes and { two, ... } or { one, two, ... } local collapsed, c = { formatters["f : %s : "](catcodes or '-') }, 1 for i=1,#arguments do local argument = arguments[i] local argtype = type(argument) c = c + 1 if argtype == "string" then collapsed[c] = lpegmatch(visualizer,argument) elseif argtype == "number" then collapsed[c] = argument else collapsed[c] = formatters["<<%S>>"](argument) end end currenttrace(concat(collapsed)) else -- no catcodes normal(one) local argtype = type(one) if argtype == "string" then currenttrace(formatters["f : - : %s"](lpegmatch(visualizer,one))) elseif argtype == "number" then currenttrace(formatters["f : - : %s"](one)) else currenttrace(formatters["f : - : <<%S>>"](one)) end end end local tracedflush = function(...) traced(normalflush, ...) end local tracedflushdirect = function(...) traced(normalflushdirect,...) end local function pushlogger(trace) trace = trace or report_context insert(trace_stack,currenttrace) currenttrace = trace -- flush = tracedflush flushdirect = tracedflushdirect writer = tracedwriter -- context.__flush = flush context.__flushdirect = flushdirect -- return flush, writer, flushdirect end local function poplogger() currenttrace = remove(trace_stack) if not currenttrace then flush = normalflush flushdirect = normalflushdirect writer = normalwriter -- context.__flush = flush context.__flushdirect = flushdirect end return flush, writer, flushdirect end local function settracing(v) if v then return pushlogger(report_context) else return poplogger() end end -- todo: share flushers so that we can define in other files trackers.register("context.trace",settracing) context.pushlogger = pushlogger context.poplogger = poplogger context.settracing = settracing -- -- untested, no time now: -- -- local tracestack, tracestacktop = { }, false -- -- function context.pushtracing(v) -- insert(tracestack,tracestacktop) -- if type(v) == "function" then -- pushlogger(v) -- v = true -- else -- pushlogger() -- end -- tracestacktop = v -- settracing(v) -- end -- -- function context.poptracing() -- poplogger() -- tracestacktop = remove(tracestack) or false -- settracing(tracestacktop) -- end function context.getlogger() return flush, writer, flush_direct end local trace_cld = false trackers.register("context.files", function(v) trace_cld = v end) function context.runfile(filename) local foundname = resolvers.findtexfile(file.addsuffix(filename,"cld")) or "" if foundname ~= "" then local ok = dofile(foundname) if type(ok) == "function" then if trace_cld then report_context("begin of file %a (function call)",foundname) end ok() if trace_cld then report_context("end of file %a (function call)",foundname) end elseif ok then report_context("file %a is processed and returns true",foundname) else report_context("file %a is processed and returns nothing",foundname) end else report_context("unknown file %a",filename) end end function context.loadfile(filename) context(stripstring(loaddata(resolvers.findfile(filename)))) end -- some functions function context.direct(first,...) if first ~= nil then return writer(context,"",first,...) end end -- context.delayed (todo: lines) local delayed = { } context.delayed = delayed -- creates function (maybe also store them) local function indexer(parent,k) local f = function(...) local a = { ... } return function() return context[k](unpack(a)) end end parent[k] = f return f end local function caller(parent,...) -- todo: nodes local a = { ... } return function() return context(unpack(a)) end end -- local function indexer(parent,k) -- local f = function(a,...) -- if not a then -- return function() -- return context[k]() -- end -- elseif select("#",...) == 0 then -- return function() -- return context[k](a) -- end -- elseif a then -- local t = { ... } -- return function() -- return context[k](a,unpack(t)) -- end -- end -- end -- parent[k] = f -- return f -- end -- -- local function caller(parent,a,...) -- todo: nodes -- if not a then -- return function() -- return context() -- end -- elseif select("#",...) == 0 then -- return function() -- return context(a) -- end -- elseif a then -- local t = { ... } -- return function() -- return context(a,unpack(t)) -- end -- end -- end setmetatable(delayed, { __index = indexer, __call = caller } ) -- context.nested (todo: lines) local nested = { } context.nested = nested -- creates strings local function indexer(parent,k) local f = function(...) local t, savedflush, n = { }, flush, 0 flush = function(c,f,s,...) -- catcodes are ignored n = n + 1 t[n] = s and concat{f,s,...} or f -- optimized for #args == 1 end context[k](...) flush = savedflush return concat(t) end parent[k] = f return f end local function caller(parent,...) local t, savedflush, n = { }, flush, 0 flush = function(c,f,s,...) -- catcodes are ignored n = n + 1 t[n] = s and concat{f,s,...} or f -- optimized for #args == 1 end context(...) flush = savedflush return concat(t) end setmetatable(nested, { __index = indexer, __call = caller } ) -- verbatim function context.newindexer(catcodes) local handler = { } local function indexer(parent,k) local command = context[k] local f = function(...) local savedcatcodes = contentcatcodes contentcatcodes = catcodes command(...) contentcatcodes = savedcatcodes end parent[k] = f return f end local function caller(parent,...) local savedcatcodes = contentcatcodes contentcatcodes = catcodes defaultcaller(parent,...) contentcatcodes = savedcatcodes end setmetatable(handler, { __index = indexer, __call = caller } ) return handler end context.verbatim = context.newindexer(vrbcatcodes) context.puretext = context.newindexer(txtcatcodes) -------.protected = context.newindexer(prtcatcodes) -- formatted local formatted = { } context.formatted = formatted -- local function indexer(parent,k) -- local command = context[k] -- local f = function(fmt,...) -- command(formatters[fmt](...)) -- end -- parent[k] = f -- return f -- end local function indexer(parent,k) if type(k) == "string" then local c = "\\" .. tostring(generics[k] or k) local f = function(first,second,...) if first == nil then flush(currentcatcodes,c) elseif second then return writer(parent,c,formatters[first](second,...)) else return writer(parent,c,first) end end parent[k] = f return f else return context -- catch end end -- local function caller(parent,...) -- context.fprint(...) -- end local function caller(parent,catcodes,fmt,first,...) if type(catcodes) == "number" then if first then flush(catcodes,formatters[fmt](first,...)) else flush(catcodes,fmt) end else if fmt then flush(formatters[catcodes](fmt,first,...)) else flush(catcodes) end end end setmetatable(formatted, { __index = indexer, __call = caller } ) -- metafun (this will move to another file) local metafun = { } context.metafun = metafun local mpdrawing = "\\MPdrawing" local function caller(parent,f,a,...) if not parent then -- skip elseif f then local typ = type(f) if typ == "string" then if a then flush(currentcatcodes,mpdrawing,"{",formatters[f](a,...),"}") else flush(currentcatcodes,mpdrawing,"{",f,"}") end elseif typ == "number" then if a then flush(currentcatcodes,mpdrawing,"{",f,a,...,"}") else flush(currentcatcodes,mpdrawing,"{",f,"}") end elseif typ == "function" then -- ignored: a ... flush(currentcatcodes,mpdrawing,"{\\cldf{",store_(f),"}}") elseif typ == "boolean" then -- ignored: a ... if f then flush(currentcatcodes,mpdrawing,"{^^M}") else report_context("warning: %a gets argument 'false' which is currently unsupported","metafun") end else report_context("error: %a gets a weird argument %a","metafun",tostring(f)) end end end setmetatable(metafun, { __call = caller } ) function metafun.start() context.resetMPdrawing() end function metafun.stop() context.MPdrawingdonetrue() context.getMPdrawing() end function metafun.color(name) return formatters[ [[\MPcolor{%s}]] ](name) end -- metafun.delayed local delayed = { } metafun.delayed = delayed local function indexer(parent,k) local f = function(...) local a = { ... } return function() return metafun[k](unpack(a)) end end parent[k] = f return f end local function caller(parent,...) local a = { ... } return function() return metafun(unpack(a)) end end setmetatable(delayed, { __index = indexer, __call = caller } ) -- helpers: function context.concat(...) context(concat(...)) end -- templates local single = lpegP("%") local double = lpegP("%%") local lquoted = lpegP("%[") local rquoted = lpegP("]%") local start = [[ local texescape = lpeg.patterns.texescape local lpegmatch = lpeg.match return function(variables) return ]] local stop = [[ end ]] local replacer = lpegP { "parser", parser = lpegCs(lpegCc(start) * lpegV("step") * (lpegCc("..") * lpegV("step"))^0 * lpegCc(stop)), unquoted = (lquoted/'') * ((lpegC((1-rquoted)^1)) / "lpegmatch(texescape,variables['%0'] or '')" ) * (rquoted/''), escape = double/'%%', key = (single/'') * ((lpegC((1-single)^1)) / "(variables['%0'] or '')" ) * (single/''), step = lpegV("unquoted") + lpegV("escape") + lpegV("key") + lpegCc("\n[===[") * (1 - lpegV("unquoted") - lpegV("escape") - lpegV("key"))^1 * lpegCc("]===]\n"), } local templates = { } local function indexer(parent,k) local v = lpegmatch(replacer,k) if not v then v = "error: no valid template (1)" else v = loadstring(v) if type(v) ~= "function" then v = "error: no valid template (2)" else v = v() if not v then v = "error: no valid template (3)" end end end if type(v) == "function" then local f = function(first,second) if second then pushcatcodes(first) flushlines(v(second)) popcatcodes() else flushlines(v(first)) end end parent[k] = f return f else return function() flush(v) end end end local function caller(parent,k,...) return parent[k](...) end setmetatable(templates, { __index = indexer, __call = caller } ) function context.template(template,...) context(templates[template](...)) end context.templates = templates -- The above is a bit over the top as we could also stick to a simple context.replace -- which is fast enough anyway, but the above fits in nicer, also with the catcodes. -- -- local replace = utilities.templates.replace -- -- function context.template(template,variables) -- context(replace(template,variables)) -- end