if not modules then modules = { } end modules ['trac-lmx'] = {
version = 1.002,
comment = "companion to trac-lmx.mkiv",
author = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
copyright = "PRAGMA ADE / ConTeXt Development Team",
license = "see context related readme files"
}
-- this one will be adpated to the latest helpers
local type, tostring, rawget, loadstring, pcall = type, tostring, rawget, loadstring, pcall
local format, sub, gsub = string.format, string.sub, string.gsub
local concat = table.concat
local collapsespaces = string.collapsespaces
local P, Cc, Cs, C, Carg, lpegmatch = lpeg.P, lpeg.Cc, lpeg.Cs, lpeg.C, lpeg.Carg, lpeg.match
local joinpath, replacesuffix, pathpart, filesuffix = file.join, file.replacesuffix, file.pathpart, file.suffix
local allocate = utilities.storage.allocate
local setmetatableindex = table.setmetatableindex
----- trace_templates = false trackers .register("lmx.templates", function(v) trace_templates = v end)
local trace_variables = false trackers .register("lmx.variables", function(v) trace_variables = v end)
local cache_templates = true directives.register("lmx.cache.templates",function(v) cache_templates = v end)
local cache_files = true directives.register("lmx.cache.files", function(v) cache_files = v end)
local report_lmx = logs.reporter("lmx")
local report_error = logs.reporter("lmx","error")
lmx = lmx or { }
local lmx = lmx
-- This will change: we will just pass the global defaults as argument, but then we need
-- to rewrite some older code or come up with an ugly trick.
local lmxvariables = {
['title-default'] = 'ConTeXt LMX File',
['color-background-green'] = '#4F6F6F',
['color-background-blue'] = '#6F6F8F',
['color-background-yellow'] = '#8F8F6F',
['color-background-purple'] = '#8F6F8F',
['color-background-body'] = '#808080',
['color-background-main'] = '#3F3F3F',
}
local lmxinherited = {
['title'] = 'title-default',
['color-background-one'] = 'color-background-green',
['color-background-two'] = 'color-background-blue',
['color-background-three'] = 'color-background-one',
['color-background-four'] = 'color-background-two',
}
lmx.variables = lmxvariables
lmx.inherited = lmxinherited
setmetatableindex(lmxvariables,function(t,k)
k = lmxinherited[k]
while k do
local v = rawget(lmxvariables,k)
if v then
return v
end
k = lmxinherited[k]
end
end)
function lmx.set(key,value)
lmxvariables[key] = value
end
function lmx.get(key)
return lmxvariables[key] or ""
end
lmx.report = report_lmx
-- helpers
-- the variables table is an empty one that gets linked to a defaults table
-- that gets passed with a creation (first time only) and that itself links
-- to one that gets passed to the converter
local variables = { } -- we assume no nesting
local result = { } -- we assume no nesting
local function do_print(one,two,...)
if two then
result[#result+1] = concat { one, two, ... }
else
result[#result+1] = one
end
end
-- Although it does not make much sense for most elements, we provide a mechanism
-- to print wrapped content, something that is more efficient when we are constructing
-- tables.
local html = { }
lmx.html = html
function html.td(str)
if type(str) == "table" then
for i=1,#str do -- spoils t !
str[i] = format("
%s | ",str[i] or "")
end
result[#result+1] = concat(str)
else
result[#result+1] = format("%s | ",str or "")
end
end
function html.th(str)
if type(str) == "table" then
for i=1,#str do -- spoils t !
str[i] = format("%s | ",str[i])
end
result[#result+1] = concat(str)
else
result[#result+1] = format("%s | ",str or "")
end
end
function html.a(text,url)
result[#result+1] = format("%s",url,text)
end
setmetatableindex(html,function(t,k)
local f = format("<%s>%%s%s>",k,k)
local v = function(str) result[#result+1] = format(f,str or "") end
t[k] = v
return v
end)
-- Loading templates:
local function loadedfile(name)
name = resolvers and resolvers.findfile and resolvers.findfile(name) or name
local data = io.loaddata(name)
if not data or data == "" then
report_lmx("file %a is empty",name)
end
return data
end
local function loadedsubfile(name)
return io.loaddata(resolvers and resolvers.findfile and resolvers.findfile(name) or name)
end
lmx.loadedfile = loadedfile
-- A few helpers (the next one could end up in l-lpeg):
local usedpaths = { }
local givenpath = nil
local do_nested_include = nil
local pattern = lpeg.replacer {
["&"] = "&",
[">"] = ">",
["<"] = "<",
['"'] = """,
}
local function do_escape(str)
return lpegmatch(pattern,str) or str
end
local function do_variable(str)
local value = variables[str]
if not trace_variables then
-- nothing
elseif type(value) == "string" then
if #value > 80 then
report_lmx("variable %a is set to: %s ...",str,collapsespaces(sub(value,1,80)))
else
report_lmx("variable %a is set to: %s",str,collapsespaces(value))
end
elseif type(value) == "nil" then
report_lmx("variable %a is set to: %s",str,"")
else
report_lmx("variable %a is set to: %S",str,value)
end
if type(value) == "function" then -- obsolete ... will go away
return value(str)
else
return value
end
end
local function do_type(str)
if str and str ~= "" then
result[#result+1] = format("%s",do_escape(str))
end
end
local function do_fprint(str,...)
if str and str ~= "" then
result[#result+1] = format(str,...)
end
end
local function do_eprint(str,...)
if str and str ~= "" then
result[#result+1] = lpegmatch(pattern,format(str,...))
end
end
local function do_print_variable(str)
local str = do_variable(str) -- variables[str]
if str and str ~= "" then
result[#result+1] = str
end
end
local function do_type_variable(str)
local str = do_variable(str) -- variables[str]
if str and str ~= "" then
result[#result+1] = format("%s",do_escape(str))
end
end
local function do_include(filename,option)
local data = loadedsubfile(filename)
if (not data or data == "") and givenpath then
data = loadedsubfile(joinpath(givenpath,filename))
end
if (not data or data == "") and type(usedpaths) == "table" then
for i=1,#usedpaths do
data = loadedsubfile(joinpath(usedpaths[i],filename))
if data and data ~= "" then
break
end
end
end
if not data or data == "" then
data = format("",filename)
report_lmx("include file %a is empty",filename)
else
-- report_lmx("included file: %s",filename)
data = do_nested_include(data)
end
if filesuffix(filename,"css") and option == "strip" then -- new
data = lmx.stripcss(data)
end
return data
end
-- Flushers:
lmx.print = do_print
lmx.type = do_type
lmx.eprint = do_eprint
lmx.fprint = do_fprint
lmx.escape = do_escape
lmx.urlescape = url.escape
lmx.variable = do_variable
lmx.include = do_include
lmx.inject = do_print
lmx.finject = do_fprint
lmx.einject = do_eprint
lmx.pv = do_print_variable
lmx.tv = do_type_variable
-- The next functions set up the closure.
function lmx.initialize(d,v)
if not v then
setmetatableindex(d,lmxvariables)
if variables ~= d then
setmetatableindex(variables,d)
if trace_variables then
report_lmx("using chain: variables => given defaults => lmx variables")
end
elseif trace_variables then
report_lmx("using chain: variables == given defaults => lmx variables")
end
elseif d ~= v then
setmetatableindex(v,d)
if d ~= lmxvariables then
setmetatableindex(d,lmxvariables)
if variables ~= v then
setmetatableindex(variables,v)
if trace_variables then
report_lmx("using chain: variables => given variables => given defaults => lmx variables")
end
elseif trace_variables then
report_lmx("using chain: variables == given variables => given defaults => lmx variables")
end
else
if variables ~= v then
setmetatableindex(variables,v)
if trace_variables then
report_lmx("using chain: variabes => given variables => given defaults")
end
elseif trace_variables then
report_lmx("using chain: variables == given variables => given defaults")
end
end
else
setmetatableindex(v,lmxvariables)
if variables ~= v then
setmetatableindex(variables,v)
if trace_variables then
report_lmx("using chain: variables => given variables => lmx variables")
end
elseif trace_variables then
report_lmx("using chain: variables == given variables => lmx variables")
end
end
result = { }
end
function lmx.finalized()
local collapsed = concat(result)
result = { } -- free memory
return collapsed
end
function lmx.getvariables()
return variables
end
function lmx.reset()
-- obsolete
end
-- Creation: (todo: strip )
-- local template = [[
-- return function(defaults,variables)
--
-- -- initialize
--
-- lmx.initialize(defaults,variables)
--
-- -- interface
--
-- local definitions = { }
-- local variables = lmx.getvariables()
-- local html = lmx.html
-- local inject = lmx.print
-- local finject = lmx.fprint
-- local einject = lmx.eprint
-- local escape = lmx.escape
-- local verbose = lmx.type
--
-- -- shortcuts (sort of obsolete as there is no gain)
--
-- local p = lmx.print
-- local f = lmx.fprint
-- local v = lmx.variable
-- local e = lmx.escape
-- local t = lmx.type
-- local pv = lmx.pv
-- local tv = lmx.tv
--
-- -- generator
--
-- %s
--
-- -- finalize
--
-- return lmx.finalized()
--
-- end
-- ]]
local template = [[
-- interface
local html = lmx.html
local inject = lmx.print
local finject = lmx.fprint -- better use the following
local einject = lmx.eprint -- better use the following
local injectf = lmx.fprint
local injecte = lmx.eprint
local injectfmt = lmx.fprint
local injectesc = lmx.eprint
local escape = lmx.escape
local verbose = lmx.type
local i_n_j_e_c_t = lmx.print
-- shortcuts (sort of obsolete as there is no gain)
local p = lmx.print
local f = lmx.fprint
local v = lmx.variable
local e = lmx.escape
local t = lmx.type
local pv = lmx.pv
local tv = lmx.tv
local lmx_initialize = lmx.initialize
local lmx_finalized = lmx.finalized
local lmx_getvariables = lmx.getvariables
-- generator
return function(defaults,variables)
lmx_initialize(defaults,variables)
local definitions = { }
local variables = lmx_getvariables()
%s -- the action: appends to result
return lmx_finalized()
end
]]
local function savedefinition(definitions,tag,content)
definitions[tag] = content
return ""
end
local function getdefinition(definitions,tag)
return definitions[tag] or ""
end
local whitespace = lpeg.patterns.whitespace
local optionalspaces = whitespace^0
local dquote = P('"')
local begincomment = P("")
local beginembedxml = P("")
local endembedxml = P("?>")
local beginembedcss = P("/*")
local endembedcss = P("*/")
local gobbledendxml = (optionalspaces * endembedxml) / ""
----- argumentxml = (1-gobbledendxml)^0
local argumentxml = (whitespace^1 + dquote * C((1-dquote)^1) * dquote + C((1-gobbledendxml-whitespace)^1))^0
local gobbledendcss = (optionalspaces * endembedcss) / ""
----- argumentcss = (1-gobbledendcss)^0
local argumentcss = (whitespace^1 + dquote * C((1-dquote)^1) * dquote + C((1-gobbledendcss-whitespace)^1))^0
local commentxml = (begincomment * (1-endcomment)^0 * endcomment) / ""
local beginluaxml = (beginembedxml * P("lua")) / ""
local endluaxml = endembedxml / ""
local luacodexml = beginluaxml
* (1-endluaxml)^1
* endluaxml
local beginluacss = (beginembedcss * P("lua")) / ""
local endluacss = endembedcss / ""
local luacodecss = beginluacss
* (1-endluacss)^1
* endluacss
local othercode = (1-beginluaxml-beginluacss)^1 / " i_n_j_e_c_t[==[%0]==] "
local includexml = ((beginembedxml * P("lmx-include") * optionalspaces) / "")
* (argumentxml / do_include)
* gobbledendxml
local includecss = ((beginembedcss * P("lmx-include") * optionalspaces) / "")
* (argumentcss / do_include)
* gobbledendcss
local definexml_b = ((beginembedxml * P("lmx-define-begin") * optionalspaces) / "")
* argumentxml
* gobbledendxml
local definexml_e = ((beginembedxml * P("lmx-define-end") * optionalspaces) / "")
* argumentxml
* gobbledendxml
local definexml_c = C((1-definexml_e)^0)
local definexml = (Carg(1) * C(definexml_b) * definexml_c * definexml_e) / savedefinition
local resolvexml = ((beginembedxml * P("lmx-resolve") * optionalspaces) / "")
* ((Carg(1) * C(argumentxml)) / getdefinition)
* gobbledendxml
local definecss_b = ((beginembedcss * P("lmx-define-begin") * optionalspaces) / "")
* argumentcss
* gobbledendcss
local definecss_e = ((beginembedcss * P("lmx-define-end") * optionalspaces) / "")
* argumentcss
* gobbledendcss
local definecss_c = C((1-definecss_e)^0)
local definecss = (Carg(1) * C(definecss_b) * definecss_c * definecss_e) / savedefinition
local resolvecss = ((beginembedcss * P("lmx-resolve") * optionalspaces) / "")
* ((Carg(1) * C(argumentcss)) / getdefinition)
* gobbledendcss
local pattern_1 = Cs((commentxml + includexml + includecss + P(1))^0) -- get rid of xml comments asap
local pattern_2 = Cs((definexml + resolvexml + definecss + resolvecss + P(1))^0)
local pattern_3 = Cs((luacodexml + luacodecss + othercode)^0)
local cache = { }
local function lmxerror(str)
report_error(str)
return html.tt(str)
end
local function wrapper(converter,defaults,variables)
local outcome, message = pcall(converter,defaults,variables)
if not outcome then
return lmxerror(format("error in conversion: %s",message))
else
return message
end
end
do_nested_include = function(data) -- also used in include
return lpegmatch(pattern_1,data)
end
function lmxnew(data,defaults,nocache,path) -- todo: use defaults in calling routines
data = data or ""
local known = cache[data]
if not known then
givenpath = path
usedpaths = lmxvariables.includepath or { }
if type(usedpaths) == "string" then
usedpaths = { usedpaths }
end
data = lpegmatch(pattern_1,data)
data = lpegmatch(pattern_2,data,1,{})
data = lpegmatch(pattern_3,data)
local converted = loadstring(format(template,data))
if converted then
converted = converted()
end
defaults = defaults or { }
local converter
if converted then
converter = function(variables)
return wrapper(converted,defaults,variables)
end
else
report_error("error in:\n%s\n:",data)
converter = function() lmxerror("error in template") end
end
known = {
data = defaults.trace and data or "",
variables = defaults,
converter = converter,
}
if cache_templates and nocache ~= false then
cache[data] = known
end
elseif variables then
known.variables = variables
end
return known, known.variables
end
local function lmxresult(self,variables)
if self then
local converter = self.converter
if converter then
local converted = converter(variables)
if trace_variables then -- will become templates
report_lmx("converted size: %s",#converted)
end
return converted or lmxerror("no result from converter")
else
return lmxerror("invalid converter")
end
else
return lmxerror("invalid specification")
end
end
lmx.new = lmxnew
lmx.result = lmxresult
local loadedfiles = { }
function lmx.convertstring(templatestring,variables,nocache,path)
return lmxresult(lmxnew(templatestring,nil,nocache,path),variables)
end
function lmx.convertfile(templatefile,variables,nocache)
if trace_variables then -- will become templates
report_lmx("converting file %a",templatefile)
end
local converter = loadedfiles[templatefile]
if not converter then
converter = lmxnew(loadedfile(templatefile),nil,nocache,pathpart(templatefile))
loadedfiles[templatefile] = converter
end
return lmxresult(converter,variables)
end
function lmxconvert(templatefile,resultfile,variables,nocache) -- or (templatefile,variables)
if trace_variables then -- will become templates
report_lmx("converting file %a",templatefile)
end
if not variables and type(resultfile) == "table" then
variables = resultfile
end
local converter = loadedfiles[templatefile]
if not converter then
converter = lmxnew(loadedfile(templatefile),nil,nocache,pathpart(templatefile))
if cache_files then
loadedfiles[templatefile] = converter
end
end
local result = lmxresult(converter,variables)
if resultfile then
io.savedata(resultfile,result)
else
return result
end
end
lmx.convert = lmxconvert
-- helpers
local nocomment = (beginembedcss * (1 - endembedcss)^1 * endembedcss) / ""
local nowhitespace = whitespace^1 / " " -- ""
local semistripped = whitespace^1 / "" * P(";")
local stripper = Cs((nocomment + semistripped + nowhitespace + 1)^1)
function lmx.stripcss(str)
return lpegmatch(stripper,str)
end
function lmx.color(r,g,b,a)
if r > 1 then
r = 1
end
if g > 1 then
g = 1
end
if b > 1 then
b = 1
end
if not a then
a= 0
elseif a > 1 then
a = 1
end
if a > 0 then
return format("rgba(%s%%,%s%%,%s%%,%s)",r*100,g*100,b*100,a)
else
return format("rgb(%s%%,%s%%,%s%%)",r*100,g*100,b*100)
end
end
-- these can be overloaded
lmx.lmxfile = string.itself
lmx.htmfile = string.itself
lmx.popupfile = os.launch
function lmxmake(name,variables)
local lmxfile = lmx.lmxfile(name)
local htmfile = lmx.htmfile(name)
if lmxfile == htmfile then
htmfile = replacesuffix(lmxfile,"html")
end
lmxconvert(lmxfile,htmfile,variables)
return htmfile
end
lmxmake = lmx.make
function lmx.show(name,variables)
local htmfile = lmxmake(name,variables)
lmx.popupfile(htmfile)
return htmfile
end
-- Command line (will become mtx-lmx):
if arg then
if arg[1] == "--show" then if arg[2] then lmx.show (arg[2]) end
elseif arg[1] == "--convert" then if arg[2] then lmx.convert(arg[2], arg[3] or "temp.html") end
end
end
-- Test 1:
-- inspect(lmx.result(lmx.new(io.loaddata("t:/sources/context-timing.lmx"))))
-- Test 2:
-- local str = [[
--
--
-- some content a
-- some content b
--
--
--
--
--
-- |
-- |
-- |
-- |
-- |
--
-- ]]
-- local defaults = { trace = true, a = 3, b = 3 }
-- local result = lmx.new(str,defaults)
-- inspect(result.data)
-- inspect(result.converter(defaults))
-- inspect(result.converter { a = 1 })
-- inspect(lmx.result(result, { b = 2 }))
-- inspect(lmx.result(result, { a = 20000, b = 40000 }))